diff --git a/CHANGELOG.md b/CHANGELOG.md index c1f6b7eaa2d..8b6b5ac179c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,13 @@ [10725]: https://github.com/enso-org/enso/pull/10725 [10820]: https://github.com/enso-org/enso/pull/10820 -# Enso 2023.3 +#### Enso Language & Runtime + +- [Print out warnings associated with local variables][10842] + +[10842]: https://github.com/enso-org/enso/pull/10842 + +# Enso 2024.3 #### Enso Language & Runtime diff --git a/engine/common/src/main/java/org/enso/common/ContextFactory.java b/engine/common/src/main/java/org/enso/common/ContextFactory.java index 796ab95c3ba..da53df9e0dc 100644 --- a/engine/common/src/main/java/org/enso/common/ContextFactory.java +++ b/engine/common/src/main/java/org/enso/common/ContextFactory.java @@ -30,6 +30,8 @@ import org.slf4j.event.Level; * @param options additional options for the Context * @param executionEnvironment optional name of the execution environment to use during execution * @param warningsLimit maximal number of warnings reported to the user + * @param checkForWarnings name of method to check for warnings + * @param enableDebugServer enable debug (e.g. REPL) server */ public final class ContextFactory { private String projectRoot; @@ -47,8 +49,10 @@ public final class ContextFactory { private boolean useGlobalIrCacheLocation = true; private boolean enableAutoParallelism; private String executionEnvironment; + private String checkForWarnings; private int warningsLimit = 100; private java.util.Map options = java.util.Collections.emptyMap(); + private boolean enableDebugServer; private ContextFactory() {} @@ -141,6 +145,16 @@ public final class ContextFactory { return this; } + public ContextFactory checkForWarnings(String fqnOfMethod) { + this.checkForWarnings = fqnOfMethod; + return this; + } + + public ContextFactory enableDebugServer(boolean b) { + this.enableDebugServer = b; + return this; + } + public Context build() { if (executionEnvironment != null) { options.put("enso.ExecutionEnvironment", executionEnvironment); @@ -169,6 +183,12 @@ public final class ContextFactory { .out(out) .err(err) .in(in); + if (checkForWarnings != null) { + builder.option(DebugServerInfo.METHOD_BREAKPOINT_OPTION, checkForWarnings); + } + if (enableDebugServer) { + builder.option(DebugServerInfo.ENABLE_OPTION, "true"); + } if (messageTransport != null) { builder.serverTransport(messageTransport); } @@ -239,5 +259,4 @@ public final class ContextFactory { .allowAccessInheritance(true) .build(); } - } diff --git a/engine/polyglot-api/src/main/java/org/enso/polyglot/debugger/DebugServerInfo.java b/engine/common/src/main/java/org/enso/common/DebugServerInfo.java similarity index 52% rename from engine/polyglot-api/src/main/java/org/enso/polyglot/debugger/DebugServerInfo.java rename to engine/common/src/main/java/org/enso/common/DebugServerInfo.java index 9665c7a795d..184181edb64 100644 --- a/engine/polyglot-api/src/main/java/org/enso/polyglot/debugger/DebugServerInfo.java +++ b/engine/common/src/main/java/org/enso/common/DebugServerInfo.java @@ -1,8 +1,12 @@ -package org.enso.polyglot.debugger; +package org.enso.common; -/** Container for Runtime Server related constants. */ +/** Container for debug server related constants. */ public class DebugServerInfo { + private DebugServerInfo() { + + } public static final String URI = "enso://debug-server"; public static final String INSTRUMENT_NAME = "enso-debug-server"; public static final String ENABLE_OPTION = INSTRUMENT_NAME + ".enable"; + public static final String METHOD_BREAKPOINT_OPTION = INSTRUMENT_NAME + ".method-break-point"; } diff --git a/engine/runner/src/main/java/org/enso/runner/Main.java b/engine/runner/src/main/java/org/enso/runner/Main.java index 212b612f3e1..ea2365f38a4 100644 --- a/engine/runner/src/main/java/org/enso/runner/Main.java +++ b/engine/runner/src/main/java/org/enso/runner/Main.java @@ -25,6 +25,7 @@ import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionGroup; import org.apache.commons.cli.Options; import org.enso.common.ContextFactory; +import org.enso.common.DebugServerInfo; import org.enso.common.HostEnsoUtils; import org.enso.common.LanguageInfo; import org.enso.distribution.DistributionManager; @@ -37,7 +38,6 @@ import org.enso.pkg.PackageManager$; import org.enso.pkg.Template; import org.enso.polyglot.Module; import org.enso.polyglot.PolyglotContext; -import org.enso.polyglot.debugger.DebugServerInfo; import org.enso.polyglot.debugger.DebuggerSessionManagerEndpoint; import org.enso.profiling.sampler.NoopSampler; import org.enso.profiling.sampler.OutputStreamSampler; @@ -688,6 +688,24 @@ public class Main { } var projectMode = fileAndProject._1(); var file = fileAndProject._2(); + var mainFile = file; + if (projectMode) { + var result = PackageManager$.MODULE$.Default().loadPackage(file); + if (result.isSuccess()) { + @SuppressWarnings("unchecked") + var pkg = (org.enso.pkg.Package) result.get(); + + mainFile = pkg.mainFile(); + if (!mainFile.exists()) { + println("Main file does not exist."); + throw exitFail(); + } + } else { + println(result.failed().get().getMessage()); + throw exitFail(); + } + } + var projectRoot = fileAndProject._3(); var options = new HashMap(); @@ -714,7 +732,9 @@ public class Main { } if (enableDebugServer) { factory.messageTransport(replTransport()); - options.put(DebugServerInfo.ENABLE_OPTION, "true"); + factory.enableDebugServer(true); + } else { + factory.checkForWarnings(mainFile.getName().replace(".enso", "") + ".main"); } var context = new PolyglotContext(factory.build()); @@ -724,12 +744,6 @@ public class Main { var s = (scala.util.Success) result; @SuppressWarnings("unchecked") var pkg = (org.enso.pkg.Package) s.get(); - var main = pkg.mainFile(); - if (!main.exists()) { - println("Main file does not exist."); - context.context().close(); - throw exitFail(); - } var mainModuleName = pkg.moduleNameForFile(pkg.mainFile()).toString(); runPackage(context, mainModuleName, file, additionalArgs); } else { @@ -879,11 +893,20 @@ public class Main { if (!res.isNull()) { var textRes = res.isString() ? res.asString() : res.toString(); println(textRes); + if (res.isException()) { + try { + throw res.throwException(); + } catch (PolyglotException e) { + if (e.isExit()) { + throw doExit(e.getExitStatus()); + } + } + } } } } catch (PolyglotException e) { if (e.isExit()) { - doExit(e.getExitStatus()); + throw doExit(e.getExitStatus()); } else { printPolyglotException(e, rootPkgPath); throw exitFail(); @@ -917,14 +940,13 @@ public class Main { .replace("$mainMethodName", mainMethodName); var replModuleName = "Internal_Repl_Module___"; var projectRoot = projectPath != null ? projectPath : ""; - var options = Collections.singletonMap(DebugServerInfo.ENABLE_OPTION, "true"); var context = new PolyglotContext( ContextFactory.create() .projectRoot(projectRoot) .messageTransport(replTransport()) - .options(options) + .enableDebugServer(true) .logLevel(logLevel) .logMasking(logMasking) .enableIrCaches(enableIrCaches) diff --git a/engine/runtime-instrument-repl-debugger/src/main/java/org/enso/interpreter/instrument/ReplDebuggerInstrument.java b/engine/runtime-instrument-repl-debugger/src/main/java/org/enso/interpreter/instrument/ReplDebuggerInstrument.java index 9ec70f21012..46efd90482e 100644 --- a/engine/runtime-instrument-repl-debugger/src/main/java/org/enso/interpreter/instrument/ReplDebuggerInstrument.java +++ b/engine/runtime-instrument-repl-debugger/src/main/java/org/enso/interpreter/instrument/ReplDebuggerInstrument.java @@ -4,20 +4,34 @@ import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.TruffleLogger; import com.oracle.truffle.api.TruffleStackTrace; import com.oracle.truffle.api.debug.DebuggerTags; +import com.oracle.truffle.api.exception.AbstractTruffleException; import com.oracle.truffle.api.frame.MaterializedFrame; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.instrumentation.EventContext; import com.oracle.truffle.api.instrumentation.ExecutionEventNode; -import com.oracle.truffle.api.instrumentation.Instrumenter; +import com.oracle.truffle.api.instrumentation.ExecutionEventNodeFactory; import com.oracle.truffle.api.instrumentation.SourceSectionFilter; +import com.oracle.truffle.api.instrumentation.StandardTags; import com.oracle.truffle.api.instrumentation.TruffleInstrument; +import com.oracle.truffle.api.interop.ExceptionType; import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.InvalidArrayIndexException; +import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.interop.UnsupportedMessageException; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; +import com.oracle.truffle.api.nodes.RootNode; import java.io.IOException; +import java.io.OutputStream; import java.net.URI; -import java.util.Collections; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; +import java.util.logging.Level; +import org.enso.common.DebugServerInfo; import org.enso.compiler.context.FramePointer; +import org.enso.interpreter.node.EnsoRootNode; import org.enso.interpreter.node.expression.builtin.debug.DebugBreakpointNode; import org.enso.interpreter.node.expression.builtin.text.util.ToJavaStringNode; import org.enso.interpreter.node.expression.debug.CaptureResultScopeNode; @@ -25,9 +39,12 @@ import org.enso.interpreter.node.expression.debug.EvalNode; import org.enso.interpreter.runtime.EnsoContext; import org.enso.interpreter.runtime.callable.CallerInfo; import org.enso.interpreter.runtime.callable.function.Function; +import org.enso.interpreter.runtime.data.hash.HashMapToVectorNode; import org.enso.interpreter.runtime.data.text.Text; +import org.enso.interpreter.runtime.data.vector.ArrayLikeAtNode; +import org.enso.interpreter.runtime.data.vector.ArrayLikeLengthNode; import org.enso.interpreter.runtime.state.State; -import org.enso.polyglot.debugger.DebugServerInfo; +import org.enso.interpreter.runtime.warning.WarningsLibrary; import org.graalvm.options.OptionDescriptor; import org.graalvm.options.OptionDescriptors; import org.graalvm.options.OptionKey; @@ -39,7 +56,13 @@ import scala.util.Right; /** The Instrument implementation for the interactive debugger REPL. */ @TruffleInstrument.Registration(id = DebugServerInfo.INSTRUMENT_NAME) -public class ReplDebuggerInstrument extends TruffleInstrument { +public final class ReplDebuggerInstrument extends TruffleInstrument { + /** Option for {@link DebugServerInfo#ENABLE_OPTION} */ + private static final OptionKey ENABLE_OPTION = new OptionKey<>(false); + + /** * Option for {@link DebugServerInfo#METHOD_BREAKPOINT_OPTION} */ + private static final OptionKey METHOD_BREAKPOINT_OPTION = new OptionKey<>(""); + /** * Called by Truffle when this instrument is installed. * @@ -47,41 +70,59 @@ public class ReplDebuggerInstrument extends TruffleInstrument { */ @Override protected void onCreate(Env env) { - SourceSectionFilter filter = - SourceSectionFilter.newBuilder().tagIs(DebuggerTags.AlwaysHalt.class).build(); - DebuggerMessageHandler handler = new DebuggerMessageHandler(); - try { - MessageEndpoint client = env.startServer(URI.create(DebugServerInfo.URI), handler); - if (client != null) { - handler.setClient(client); - Instrumenter instrumenter = env.getInstrumenter(); - instrumenter.attachExecutionEventFactory( - filter, - ctx -> - ctx.getInstrumentedNode() instanceof DebugBreakpointNode - ? new ReplExecutionEventNodeImpl( - ctx, handler, env.getLogger(ReplExecutionEventNodeImpl.class)) - : null); - } else { + SourceSectionFilter filter = null; + ExecutionEventNodeFactory factory = null; + + if (env.getOptions().get(ENABLE_OPTION)) { + factory = + ctx -> + ctx.getInstrumentedNode() instanceof DebugBreakpointNode + ? new ReplExecutionEventNodeImpl( + null, ctx, handler, env.getLogger(ReplExecutionEventNodeImpl.class)) + : null; + + filter = SourceSectionFilter.newBuilder().tagIs(DebuggerTags.AlwaysHalt.class).build(); + env.getInstrumenter().attachExecutionEventFactory(filter, factory); + } + if (env.getOptions().get(METHOD_BREAKPOINT_OPTION) instanceof String replMethodName + && !replMethodName.isEmpty()) { + factory = new AtTheEndOfMethod(handler, env); + + filter = + SourceSectionFilter.newBuilder() + .tagIs(StandardTags.RootBodyTag.class) + .rootNameIs(replMethodName::equals) + .build(); + env.getInstrumenter().attachExecutionEventFactory(filter, factory); + } + + if (factory != null || filter != null) { + try { + MessageEndpoint client = env.startServer(URI.create(DebugServerInfo.URI), handler); + if (client != null) { + handler.setClient(client); + } + } catch (MessageTransport.VetoException e) { env.getLogger(ReplDebuggerInstrument.class) - .warning("ReplDebuggerInstrument was initialized, " + "but no client connected"); + .warning( + "ReplDebuggerInstrument was initialized, " + + "but client connection has been vetoed"); + } catch (IOException e) { + throw new RuntimeException(e); } - } catch (MessageTransport.VetoException e) { - env.getLogger(ReplDebuggerInstrument.class) - .warning( - "ReplDebuggerInstrument was initialized, " + "but client connection has been vetoed"); - } catch (IOException e) { - throw new RuntimeException(e); } } @Override protected OptionDescriptors getOptionDescriptors() { - return OptionDescriptors.create( - Collections.singletonList( - OptionDescriptor.newBuilder(new OptionKey<>(""), DebugServerInfo.ENABLE_OPTION) - .build())); + var options = new ArrayList(); + options.add(OptionDescriptor.newBuilder(ENABLE_OPTION, DebugServerInfo.ENABLE_OPTION).build()); + options.add( + OptionDescriptor.newBuilder( + METHOD_BREAKPOINT_OPTION, DebugServerInfo.METHOD_BREAKPOINT_OPTION) + .build()); + return OptionDescriptors.create(options); } /** The actual node that's installed as a probe on any node the instrument was launched for. */ @@ -96,16 +137,31 @@ public class ReplDebuggerInstrument extends TruffleInstrument { private EventContext eventContext; private DebuggerMessageHandler handler; private TruffleLogger logger; + private final OutputStream atExit; private ReplExecutionEventNodeImpl( - EventContext eventContext, DebuggerMessageHandler handler, TruffleLogger logger) { + OutputStream atExit, + EventContext eventContext, + DebuggerMessageHandler handler, + TruffleLogger logger) { this.eventContext = eventContext; this.handler = handler; this.logger = logger; + this.atExit = atExit; } - private Object getValue(MaterializedFrame frame, FramePointer ptr) { - return getProperFrame(frame, ptr).getValue(ptr.frameSlotIdx()); + private Object readValue( + MaterializedFrame frame, FramePointer ptr, boolean onlyWarningsOrErrors) { + var raw = getProperFrame(frame, ptr).getValue(ptr.frameSlotIdx()); + if (WarningsLibrary.getUncached().hasWarnings(raw)) { + return formatObject(raw); + } + if (onlyWarningsOrErrors) { + if (!InteropLibrary.getUncached().isException(raw)) { + return null; + } + } + return raw; } private MaterializedFrame getProperFrame(MaterializedFrame frame, FramePointer ptr) { @@ -118,11 +174,19 @@ public class ReplDebuggerInstrument extends TruffleInstrument { @Override public Map listBindings() { + return listBindings(false); + } + + public Map listBindings(boolean onlyWarningsOrErrors) { Map flatScope = nodeState.getLastScope().getLocalScope().flattenBindings(); Map result = new HashMap<>(); for (Map.Entry entry : flatScope.entrySet()) { - result.put(entry.getKey(), getValue(nodeState.getLastScope().getFrame(), entry.getValue())); + var valueOrNull = + readValue(nodeState.getLastScope().getFrame(), entry.getValue(), onlyWarningsOrErrors); + if (valueOrNull != null) { + result.put(entry.getKey(), valueOrNull); + } } return result; } @@ -155,12 +219,32 @@ public class ReplDebuggerInstrument extends TruffleInstrument { } } - private Object formatObject(Object o) { - if (o instanceof Text) { - return toJavaStringNode.execute((Text) o); - } else { - return o; + private Object formatObject(Object raw) { + if (raw instanceof Text) { + return toJavaStringNode.execute((Text) raw); } + if (WarningsLibrary.getUncached().hasWarnings(raw)) { + try { + var sb = new StringBuilder(); + sb.append(WarningsLibrary.getUncached().removeWarnings(raw)); + var mappedWarnings = WarningsLibrary.getUncached().getWarnings(raw, true); + var pairs = HashMapToVectorNode.getUncached().execute(mappedWarnings); + var size = ArrayLikeLengthNode.getUncached().executeLength(pairs); + for (var i = 0L; i < size; i++) { + try { + var pair = ArrayLikeAtNode.getUncached().executeAt(pairs, i); + var value = ArrayLikeAtNode.getUncached().executeAt(pair, 1); + sb.append("\n ! ").append(value); + } catch (InvalidArrayIndexException ex) { + // go on + } + } + return sb.toString(); + } catch (UnsupportedMessageException e) { + // go on + } + } + return raw; } @Override @@ -175,12 +259,21 @@ public class ReplDebuggerInstrument extends TruffleInstrument { */ @Override protected void onEnter(VirtualFrame frame) { - CallerInfo lastScope = Function.ArgumentsHelper.getCallerInfo(frame.getArguments()); - Object lastReturn = EnsoContext.get(this).getNothing(); - // Note [Safe Access to State in the Debugger Instrument] - monadicState = Function.ArgumentsHelper.getState(frame.getArguments()); - nodeState = new ReplExecutionEventNodeState(lastReturn, lastScope); - startSession(); + if (atExit == null) { + CallerInfo lastScope = Function.ArgumentsHelper.getCallerInfo(frame.getArguments()); + Object lastReturn = EnsoContext.get(this).getNothing(); + // Note [Safe Access to State in the Debugger Instrument] + monadicState = Function.ArgumentsHelper.getState(frame.getArguments()); + nodeState = new ReplExecutionEventNodeState(lastReturn, lastScope); + startSessionImpl(); + } + } + + @Override + public void onReturnValue(VirtualFrame frame, Object result) { + if (atExit != null) { + startSession(getRootNode(), frame, result); + } } /* Note [Safe Access to State in the Debugger Instrument] @@ -204,14 +297,43 @@ public class ReplDebuggerInstrument extends TruffleInstrument { return nodeState.getLastReturn(); } + private void startSession(RootNode root, VirtualFrame frame, Object toReturn) { + CallerInfo lastScope = Function.ArgumentsHelper.getCallerInfo(frame.getArguments()); + if (lastScope == null && root instanceof EnsoRootNode enso) { + lastScope = + new CallerInfo(frame.materialize(), enso.getLocalScope(), enso.getModuleScope()); + } + if (lastScope != null) { + // Note [Safe Access to State in the Debugger Instrument] + monadicState = Function.ArgumentsHelper.getState(frame.getArguments()); + nodeState = new ReplExecutionEventNodeState(toReturn, lastScope); + startSessionImpl(); + } + } + @CompilerDirectives.TruffleBoundary - private void startSession() { + private void startSessionImpl() { if (handler.hasClient()) { handler.startSession(this); } else { - logger.warning( - "Debugger session starting, " - + "but no client connected, will terminate the session immediately"); + if (atExit == null) { + logger.warning( + "Debugger session starting, " + + "but no client connected, will terminate the session immediately"); + } else { + for (var b : listBindings(true).entrySet()) { + var line = b.getKey() + " = " + b.getValue() + "\n"; + try { + atExit.write(line.getBytes(StandardCharsets.UTF_8)); + } catch (IOException ex) { + logger.log(Level.SEVERE, line, ex); + } + if (InteropLibrary.getUncached().isException(b.getValue())) { + var wrappingError = new ErrorDetected(nodeState.lastReturn); + nodeState = new ReplExecutionEventNodeState(wrappingError, nodeState.getLastScope()); + } + } + } exit(); } } @@ -241,4 +363,49 @@ public class ReplDebuggerInstrument extends TruffleInstrument { } } } + + private final class AtTheEndOfMethod implements ExecutionEventNodeFactory { + private final DebuggerMessageHandler handler; + private final TruffleInstrument.Env env; + + AtTheEndOfMethod(DebuggerMessageHandler h, TruffleInstrument.Env env) { + this.handler = h; + this.env = env; + } + + @Override + public ExecutionEventNode create(EventContext ctx) { + var log = env.getLogger(ReplExecutionEventNodeImpl.class); + return new ReplExecutionEventNodeImpl(env.err(), ctx, handler, log); + } + } + + @ExportLibrary(delegateTo = "delegate", value = InteropLibrary.class) + static final class ErrorDetected extends AbstractTruffleException implements TruffleObject { + final Object delegate; + + ErrorDetected(Object delegate) { + this.delegate = delegate; + } + + @ExportMessage + boolean isException() { + return true; + } + + @ExportMessage + ExceptionType getExceptionType() { + return ExceptionType.EXIT; + } + + @ExportMessage + RuntimeException throwException() { + throw this; + } + + @ExportMessage + int getExceptionExitStatus() { + return 173; + } + } } diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/InsightForEnsoTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/InsightForEnsoTest.java index 0e7e48627ce..4f6466d6370 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/InsightForEnsoTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/InsightForEnsoTest.java @@ -93,9 +93,9 @@ public class InsightForEnsoTest { assertNotEquals("4th step: " + msgs, -1, msgs.indexOf("n=2 v=60 acc=function")); assertNotEquals( - "Uninitialized variables are seen as JavaScript null: " + msgs, + "Uninitialized variables (seen as JavaScript null) aren't there: " + msgs, -1, - msgs.indexOf("n=null v=null acc=function")); + msgs.indexOf("null")); } @Test @@ -123,16 +123,19 @@ public class InsightForEnsoTest { Source.newBuilder( "enso", """ + id x = x + init_first_switch_arg x = x + type Complex Number re im - switch n:Complex = Complex.Number n.im n.re - switch_lazy (~n:Complex) = Complex.Number n.im n.re + switch f=(init_first_switch_arg id) n:Complex = Complex.Number (f n.im) (f n.re) + switch_lazy f=(init_first_switch_arg id) (~n:Complex) = Complex.Number (f n.im) (f n.re) - alloc1 a b = Complex.switch (Complex.Number a b) - alloc2 a b = Complex.switch (..Number a b) - alloc3 a b = Complex.switch_lazy (Complex.Number a b) - alloc4 a b = Complex.switch_lazy (..Number a b) + alloc1 a b = Complex.switch n=(Complex.Number a b) + alloc2 a b = Complex.switch n=(..Number a b) + alloc3 a b = Complex.switch_lazy n=(Complex.Number a b) + alloc4 a b = Complex.switch_lazy n=(..Number a b) """, "complex.enso") .build(); @@ -153,19 +156,27 @@ public class InsightForEnsoTest { var firstCons = msgs.indexOf("complex::complex.Complex::Number"); var secondCons = msgs.lastIndexOf("complex::complex.Complex::Number"); - var switchCall = msgs.indexOf("complex::complex.Complex.type::switch"); + var switchInitCall = msgs.indexOf("complex::complex::init_first_switch_arg"); - assertNotEquals(msgs, -1, switchCall); + assertNotEquals(msgs, -1, switchInitCall); assertNotEquals(msgs, -1, firstCons); assertNotEquals(msgs, -1, secondCons); assertTrue( "First constructor call must be sooner than second:\n" + msgs, firstCons < secondCons); if (useAutoscoping || lazy) { - assertTrue("Switch call first and then both constructors:\n" + msgs, switchCall < firstCons); + assertTrue( + "Switch call (" + + switchInitCall + + ") first and then both constructors (" + + firstCons + + "):\n" + + msgs, + switchInitCall < firstCons); } else { - assertTrue("First constructor sooner than switch call:\n" + msgs, firstCons < switchCall); - assertTrue("Switch call sooner than second constructor:\n" + msgs, switchCall < secondCons); + assertTrue("First constructor sooner than switch call:\n" + msgs, firstCons < switchInitCall); + assertTrue( + "Switch call sooner than second constructor:\n" + msgs, switchInitCall < secondCons); } } } diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/PolyglotErrorTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/PolyglotErrorTest.java index 6be980549af..c5b89b7921c 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/PolyglotErrorTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/PolyglotErrorTest.java @@ -67,7 +67,8 @@ public class PolyglotErrorTest { 3 -> panic3 4 -> panic4 5 -> panic5 - _ -> panic6 + 6 -> panic6 + _ -> panic7 panic1 = PolyglotErrorTest.bar (TypeCa.Ca 'x') @@ -83,6 +84,12 @@ public class PolyglotErrorTest { panic5 = PolyglotErrorTest.bar (TypeCe.Ce "Foo") panic6 = PolyglotErrorTest.bar (TypeCe.Ce 44) + panic7 = + j = Error.throw 1 + d = Error.throw 2 + t = j + d + v = [j, d, t] + v """; var src = Source.newBuilder("enso", code, "test.enso").build(); var module = ctx.eval(src); @@ -144,4 +151,18 @@ public class PolyglotErrorTest { assertEquals( "[[Error in method `to_text` of [Ce 44]: Expected Text but got 44]]", v.asString()); } + + @Test + public void panic7() { + var r = panic.execute(7); + assertTrue("Got array back: " + r, r.hasArrayElements()); + assertEquals("Got three elements", 3, r.getArraySize()); + assertTrue("Error 1 at 0th" + r, r.getArrayElement(0).isException()); + assertTrue("Error 2 at 1st" + r, r.getArrayElement(1).isException()); + assertTrue("Error 1 at 2nd " + r, r.getArrayElement(2).isException()); + + assertEquals("(Error: 1)", r.getArrayElement(0).toString()); + assertEquals("(Error: 2)", r.getArrayElement(1).toString()); + assertEquals("(Error: 1)", r.getArrayElement(2).toString()); + } } diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/RootNamesTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/RootNamesTest.java index 9960f915866..14a594e0d16 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/RootNamesTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/RootNamesTest.java @@ -87,15 +87,12 @@ public class RootNamesTest { .map(l -> l.substring(7)) .collect(Collectors.toSet()); - assertEquals("Few closures: " + closures, 3, closures.size()); + assertEquals("Few closures: " + closures, 2, closures.size()); assertTrue( "Fully qualified name for method: " + closures, closures.contains("factorial::factorial::fac")); assertTrue( "Name with dots for local method: " + closures, closures.contains("factorial.fac.acc")); - assertTrue( - "Prefixed with dot name for argument thunk: " + closures, - closures.contains("factorial.fac.acc")); } @Test diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/instrument/DebugServerInspectTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/instrument/DebugServerInspectTest.java new file mode 100644 index 00000000000..2c197634697 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/instrument/DebugServerInspectTest.java @@ -0,0 +1,144 @@ +package org.enso.interpreter.test.instrument; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import org.enso.common.DebugServerInfo; +import org.enso.test.utils.ContextUtils; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.PolyglotException; +import org.hamcrest.core.AllOf; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public class DebugServerInspectTest { + private static Context ctx; + private static ByteArrayOutputStream out = new ByteArrayOutputStream(); + private static ByteArrayOutputStream err = new ByteArrayOutputStream(); + + @BeforeClass + public static void initContext() throws Exception { + var b = ContextUtils.defaultContextBuilder().out(out).err(err); + b.option(DebugServerInfo.METHOD_BREAKPOINT_OPTION, "ScriptTest.inspect"); + ctx = b.build(); + } + + @AfterClass + public static void closeContext() throws Exception { + ctx.close(); + ctx = null; + out = null; + err = null; + } + + @Before + public void cleanSteams() { + out.reset(); + err.reset(); + } + + @Test + public void listingVariablesWithWarnings() throws Exception { + var code = + """ + from Standard.Base import all + + inspect = + j = 1 + d = Warning.attach "doubled value" 2 + t = j + d + v = [j, d, t] + v + """; + var r = ContextUtils.evalModule(ctx, code, "ScriptTest.enso", "inspect"); + assertTrue("Got array back: " + r, r.hasArrayElements()); + assertEquals("Got three elements", 3, r.getArraySize()); + assertEquals("One", 1, r.getArrayElement(0).asInt()); + assertEquals("Two", 2, r.getArrayElement(1).asInt()); + assertEquals("Three", 3, r.getArrayElement(2).asInt()); + assertEquals("No output printed", "", out.toString()); + assertThat( + "Stderr contains some warnings", + err.toString(), + AllOf.allOf( + containsString("d = 2"), + containsString("t = 3"), + containsString("doubled value"), + not(containsString("j = 1")))); + + var at1 = err.toString().indexOf("d = 2"); + assertNotEquals("d = 2 found", -1, at1); + var at2 = err.toString().indexOf("d = 2", at1 + 1); + assertEquals("d = 2 not found for the second time", -1, at2); + } + + @Test + public void panicOnError() throws Exception { + var code = + """ + from Standard.Base import all + + inspect = + j = 1 + d = Error.throw 2 + t = j + d + v = [j, d, t] + v + """; + var r = ContextUtils.evalModule(ctx, code, "ScriptTest.enso", "inspect"); + assertTrue("Got error back: " + r, r.isException()); + try { + throw r.throwException(); + } catch (PolyglotException ex) { + assertTrue("It is exit exception", ex.isExit()); + assertEquals("Error code is right", 173, ex.getExitStatus()); + } + assertTrue("It is also an array value", r.hasArrayElements()); + assertEquals("Three elements", 3, r.getArraySize()); + assertFalse("No error at 0", r.getArrayElement(0).isException()); + assertTrue("No error at 1", r.getArrayElement(1).isException()); + assertTrue("No error at 2", r.getArrayElement(2).isException()); + assertEquals("(Error: 2)", r.getArrayElement(1).toString()); + assertEquals("(Error: 2)", r.getArrayElement(2).toString()); + assertEquals("No output printed", "", out.toString()); + assertThat( + "Stderr contains some errors", + err.toString(), + AllOf.allOf( + containsString("d = Error:2"), + containsString("t = Error:2"), + not(containsString("j = 1")))); + } + + @Test + public void panicOnUnusedError() throws Exception { + var code = + """ + from Standard.Base import all + + inspect = + j = 1 + d = Error.throw 2 + j + """; + var r = ContextUtils.evalModule(ctx, code, "ScriptTest.enso", "inspect"); + assertTrue("Got error back: " + r, r.isException()); + assertEquals("But it is also the right value", 1, r.asInt()); + assertEquals( + "Compilation warning printed", + "ScriptTest:5:5: warning: Unused variable d.", + out.toString().trim()); + assertThat( + "Stderr contains some errors", + err.toString(), + AllOf.allOf(containsString("d = Error:2"), not(containsString("j = 1")))); + } +} diff --git a/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/InterpreterTest.scala b/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/InterpreterTest.scala index 08a896f9aa7..14e09ce0d95 100644 --- a/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/InterpreterTest.scala +++ b/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/InterpreterTest.scala @@ -8,11 +8,11 @@ import org.enso.interpreter.test.instruments.{ } import org.enso.interpreter.test.instruments.CodeLocationsTestInstrument.LocationsEventListener import org.enso.polyglot.debugger.{ - DebugServerInfo, DebuggerSessionManagerEndpoint, ReplExecutor, SessionManager } +import org.enso.common.DebugServerInfo import org.enso.common.LanguageInfo import org.enso.common.RuntimeOptions import org.enso.polyglot.{Function, PolyglotContext} diff --git a/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/DebugServerTest.scala b/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/DebugServerTest.scala new file mode 100644 index 00000000000..8f0e66a6205 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/DebugServerTest.scala @@ -0,0 +1,187 @@ +package org.enso.interpreter.test.instrument + +import org.enso.interpreter.test.{InterpreterContext, InterpreterTest} +import org.enso.polyglot.debugger.ObjectRepresentation +import org.enso.common.DebugServerInfo +import org.graalvm.polyglot.Context +import org.scalatest.{BeforeAndAfter, EitherValues, Inside} + +class DebugServerTest + extends InterpreterTest + with BeforeAndAfter + with EitherValues + with Inside { + + override def subject: String = "Repl" + + override def contextModifiers: Option[Context#Builder => Context#Builder] = + Some(b => { + b.option(DebugServerInfo.METHOD_BREAKPOINT_OPTION, "Test.main") + }) + + override def specify(implicit + interpreterContext: InterpreterContext + ): Unit = { + + "initialize properly" in { + val code = + """ + |import Standard.Base.Runtime.Debug + | + |main = "hi" + |""".stripMargin + setSessionManager(executor => executor.exit()) + eval(code) + } + + "be able to execute arbitrary code in the caller scope" in { + val code = + """ + |import Standard.Base.Data.Numbers + | + |main = + | x = 1 + | y = 2 + |""".stripMargin + var evalResult: Either[Exception, ObjectRepresentation] = + null + setSessionManager { executor => + evalResult = executor.evaluate("x + y") + executor.exit() + } + eval(code) shouldEqual 3 + evalResult.fold(_.toString, _.toString) shouldEqual "3" + } + + "be able to define its local variables" in { + val code = + """ + |import Standard.Base.Data.Numbers + | + |main = + | x = 10 + |""".stripMargin + setSessionManager { executor => + executor.evaluate("y = x + 1") + executor.evaluate("z = y * x") + executor.evaluate("z") + executor.exit() + } + eval(code) shouldEqual 110 + } + + "be able to list local variables in its scope" in { + val code = + """ + |import Standard.Base.Data.Numbers + | + |main = + | x = 10 + | y = 20 + | z = x + y + |""".stripMargin + var scopeResult: Map[String, ObjectRepresentation] = Map() + setSessionManager { executor => + scopeResult = executor.listBindings() + executor.exit() + } + eval(code) + scopeResult.view.mapValues(_.toString).toMap shouldEqual Map( + "x" -> "10", + "y" -> "20", + "z" -> "30" + ) + } + + "be able to list bindings it has created" in { + val code = + """ + |import Standard.Base.Data.Numbers + | + |main = + | x = 10 + | y = 20 + | z = x + y + |""".stripMargin + var scopeResult: Map[String, ObjectRepresentation] = Map() + setSessionManager { executor => + executor.evaluate("x = y + z") + scopeResult = executor.listBindings() + executor.exit() + } + eval(code) + scopeResult.view.mapValues(_.toString).toMap shouldEqual Map( + "x" -> "50", + "y" -> "20", + "z" -> "30" + ) + } + + "handle errors gracefully" in { + val code = + """ + |import Standard.Base.Data.Numbers + | + |main = + | "hi" + |""".stripMargin + var evalResult: Either[Exception, ObjectRepresentation] = + null + setSessionManager { executor => + evalResult = executor.evaluate("1 + undefined") + executor.exit() + } + eval(code) + val errorMsg = + "Compile_Error.Error" + evalResult.left.value.getMessage shouldEqual errorMsg + } + + "handle errors gracefully (pretty print)" in { + val code = + """ + |import Standard.Base.Runtime.Debug + |from Standard.Base.Errors.Common import all + | + |main = + | Debug.breakpoint + |""".stripMargin + var evalResult: Either[Exception, ObjectRepresentation] = + null + setSessionManager { executor => + evalResult = executor.evaluate("1 + undefined") + executor.exit() + } + eval(code) + val errorMsg = + "Compile error: The name `undefined` could not be found." + evalResult.left.value.getMessage shouldEqual errorMsg + } + + "attach language stack traces to the exception" in { + val code = + """ + |import Standard.Base.Panic.Panic + | + |main = + | "hi" + |""".stripMargin + var evalResult: Either[Exception, ObjectRepresentation] = + null + setSessionManager { executor => + evalResult = executor.evaluate("Panic.throw \"Panic\"") + executor.exit() + } + eval(code) + + var lastException: Throwable = evalResult.left.value + while (lastException.getCause != null) { + lastException = lastException.getCause + } + + val traceMethodNames = lastException.getStackTrace.map(_.getMethodName) + traceMethodNames should contain("Panic.throw") + traceMethodNames should contain("Test::Test::main") + } + } +} diff --git a/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/ReplTest.scala b/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/ReplTest.scala index 66227c76a15..8a1a67f602a 100644 --- a/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/ReplTest.scala +++ b/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/ReplTest.scala @@ -1,7 +1,8 @@ package org.enso.interpreter.test.instrument import org.enso.interpreter.test.{InterpreterContext, InterpreterTest} -import org.enso.polyglot.debugger.{DebugServerInfo, ObjectRepresentation} +import org.enso.common.DebugServerInfo +import org.enso.polyglot.debugger.ObjectRepresentation import org.graalvm.polyglot.Context import org.scalatest.{BeforeAndAfter, EitherValues, Inside} @@ -14,7 +15,9 @@ class ReplTest override def subject: String = "Repl" override def contextModifiers: Option[Context#Builder => Context#Builder] = - Some(_.option(DebugServerInfo.ENABLE_OPTION, "true")) + Some(b => { + b.option(DebugServerInfo.ENABLE_OPTION, "true") + }) override def specify(implicit interpreterContext: InterpreterContext diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/function/BlockNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/function/BlockNode.java index cfb26671d9b..994b68b3414 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/function/BlockNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/function/BlockNode.java @@ -25,16 +25,23 @@ public class BlockNode extends ExpressionNode { } /** - * Creates an instance of this node. + * Creates an "root tagged" instance of block node. * * @param expressions the function body * @param returnExpr the return expression from the function * @return a node representing a block expression */ - public static BlockNode build(ExpressionNode[] expressions, ExpressionNode returnExpr) { - return new BlockNode(expressions, returnExpr); + public static BlockNode buildRoot(ExpressionNode[] expressions, ExpressionNode returnExpr) { + return new Root(expressions, returnExpr); } + /** + * Creates a non-instrumented instance of block node. + * + * @param expressions the function body + * @param returnExpr the return expression from the function + * @return a node representing a block expression + */ public static BlockNode buildSilent(ExpressionNode[] expressions, ExpressionNode returnExpr) { return new BlockNode(expressions, returnExpr); } @@ -86,14 +93,17 @@ public class BlockNode extends ExpressionNode { return ss != null ? ss : getRootNode().getSourceSection(); } - @Override - public boolean hasTag(Class tag) { - if (super.hasTag(tag)) { - return true; + private static final class Root extends BlockNode { + Root(ExpressionNode[] expressions, ExpressionNode returnExpr) { + super(expressions, returnExpr); } - if (tag == StandardTags.RootBodyTag.class || tag == StandardTags.RootTag.class) { - return true; + + @Override + public boolean hasTag(Class tag) { + if (super.hasTag(tag)) { + return true; + } + return tag == StandardTags.RootBodyTag.class || tag == StandardTags.RootTag.class; } - return false; } } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/UnresolvedConstructor.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/UnresolvedConstructor.java index e643b64c782..a7304061558 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/UnresolvedConstructor.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/UnresolvedConstructor.java @@ -196,7 +196,7 @@ public final class UnresolvedConstructor implements EnsoObject { expr.setSourceLocation(section.getCharIndex(), section.getCharLength()); } var lang = EnsoLanguage.get(null); - var body = BlockNode.build(new ExpressionNode[0], expr); + var body = BlockNode.buildSilent(new ExpressionNode[0], expr); body.adoptChildren(); var root = ClosureRootNode.build( diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/atom/AtomConstructor.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/atom/AtomConstructor.java index bfd9353b49f..225984082ce 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/atom/AtomConstructor.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/atom/AtomConstructor.java @@ -179,7 +179,7 @@ public final class AtomConstructor implements EnsoObject { if (section != null) { instantiateNode.setSourceLocation(section.getCharIndex(), section.getCharLength()); } - BlockNode instantiateBlock = BlockNode.buildSilent(assignments, instantiateNode); + BlockNode instantiateBlock = BlockNode.buildRoot(assignments, instantiateNode); RootNode rootNode = MethodRootNode.buildConstructor( language, localScope, scopeBuilder.asModuleScope(), instantiateBlock, section, this); diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/HashMapToVectorNode.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/HashMapToVectorNode.java index 993f1739493..547021e1abd 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/HashMapToVectorNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/HashMapToVectorNode.java @@ -33,6 +33,10 @@ public abstract class HashMapToVectorNode extends Node { return HashMapToVectorNodeGen.create(); } + public static HashMapToVectorNode getUncached() { + return HashMapToVectorNodeGen.getUncached(); + } + public abstract Object execute(Object self); @Specialization diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/runtime/IrToTruffle.scala b/engine/runtime/src/main/scala/org/enso/interpreter/runtime/IrToTruffle.scala index d6f46a65939..38a8000e4b1 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/runtime/IrToTruffle.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/runtime/IrToTruffle.scala @@ -1375,7 +1375,7 @@ class IrToTruffle( val statementExprs = block.expressions.map(this.run(_, true)).toArray val retExpr = this.run(block.returnValue, true) - val blockNode = BlockNode.build(statementExprs, retExpr) + val blockNode = BlockNode.buildSilent(statementExprs, retExpr) setLocation(blockNode, block.location) } } @@ -2177,8 +2177,12 @@ class IrToTruffle( lazy val argsExpr = computeArgsAndExpression() def args(): Array[ArgumentDefinition] = slots._2 - def bodyNode(): RuntimeExpression = - BlockNode.build(argsExpr._1.toArray, argsExpr._2) + def bodyNode(): RuntimeExpression = { + val body = BlockNode.buildRoot(Array(), argsExpr._2) + val initVariablesAndThenBody = + BlockNode.buildSilent(argsExpr._1.toArray, body) + initVariablesAndThenBody + } private def computeArgsAndExpression() : (Array[RuntimeExpression], RuntimeExpression) = { diff --git a/lib/java/test-utils/src/main/java/org/enso/test/utils/ContextUtils.java b/lib/java/test-utils/src/main/java/org/enso/test/utils/ContextUtils.java index db432e99cd6..fb05b0ea16b 100644 --- a/lib/java/test-utils/src/main/java/org/enso/test/utils/ContextUtils.java +++ b/lib/java/test-utils/src/main/java/org/enso/test/utils/ContextUtils.java @@ -137,14 +137,35 @@ public final class ContextUtils { /** * Evaluates the given source as if it was in an unnamed module. * + * @param ctx context to evaluate the module at * @param src The source code of the module * @return The value returned from the main method of the unnamed module. */ - public static Value evalModule(Context ctx, String src) { - Value module = ctx.eval(Source.create("enso", src)); - Value assocType = module.invokeMember(Module.GET_ASSOCIATED_TYPE); - Value mainMethod = module.invokeMember(Module.GET_METHOD, assocType, "main"); - return mainMethod.execute(); + public static Value evalModule(Context ctx, CharSequence src) { + return evalModule(ctx, src, null, "main"); + } + + /** + * Evaluates the given source as if it was in a module with given name. + * + * @param ctx context to evaluate the module at + * @param src The source code of the module + * @param name name of the module defining the source + * @param methodName name of main method to invoke + * @return The value returned from the main method of the unnamed module. + */ + public static Value evalModule(Context ctx, CharSequence src, String name, String methodName) { + Source s; + if (name == null) { + s = Source.create("enso", src); + } else { + var b = Source.newBuilder("enso", src, name); + s = b.buildLiteral(); + } + var module = ctx.eval(s); + var assocType = module.invokeMember(Module.GET_ASSOCIATED_TYPE); + var method = module.invokeMember(Module.GET_METHOD, assocType, methodName); + return "main".equals(methodName) ? method.execute() : method.execute(assocType); } public static org.enso.compiler.core.ir.Module compileModule(Context ctx, String src) {