From f2572356f19a70f42e72978f16475619d38193f0 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Sun, 18 Aug 2024 07:45:30 +0200 Subject: [PATCH] Print warnings on local variables to stderr --- .../instrument/ReplDebuggerInstrument.java | 60 +++++++++++----- .../instrument/DebugServerWithScriptTest.java | 70 +++++++++++++++++++ .../org/enso/test/utils/ContextUtils.java | 31 ++++++-- 3 files changed, 138 insertions(+), 23 deletions(-) create mode 100644 engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/instrument/DebugServerWithScriptTest.java 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 aaf8f1d64bc5..32fa05fc6497 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 @@ -17,10 +17,13 @@ import com.oracle.truffle.api.interop.UnsupportedMessageException; import com.oracle.truffle.api.nodes.RootNode; import java.io.IOException; +import java.io.OutputStream; import java.net.URI; +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.compiler.context.FramePointer; import org.enso.interpreter.node.EnsoRootNode; import org.enso.interpreter.node.expression.builtin.debug.DebugBreakpointNode; @@ -49,8 +52,11 @@ /** The Instrument implementation for the interactive debugger REPL. */ @TruffleInstrument.Registration(id = DebugServerInfo.INSTRUMENT_NAME) public final class ReplDebuggerInstrument extends TruffleInstrument { - private static final OptionKey ENABLE_OPTION = new OptionKey(false); - private static final OptionKey FN_OPTION = new OptionKey(""); + /** Option for {@link DebugServerInfo#ENABLE_OPTION} */ + private static final OptionKey ENABLE_OPTION = new OptionKey<>(false); + + /** Option for {@link DebugServerInfo#FN_OPTION} */ + private static final OptionKey FN_OPTION = new OptionKey<>(""); /** * Called by Truffle when this instrument is installed. @@ -68,7 +74,7 @@ protected void onCreate(Env env) { ctx -> ctx.getInstrumentedNode() instanceof DebugBreakpointNode ? new ReplExecutionEventNodeImpl( - false, ctx, handler, env.getLogger(ReplExecutionEventNodeImpl.class)) + null, ctx, handler, env.getLogger(ReplExecutionEventNodeImpl.class)) : null; filter = SourceSectionFilter.newBuilder().tagIs(DebuggerTags.AlwaysHalt.class).build(); @@ -126,10 +132,10 @@ private static class ReplExecutionEventNodeImpl extends ExecutionEventNode private EventContext eventContext; private DebuggerMessageHandler handler; private TruffleLogger logger; - private final boolean atExit; + private final OutputStream atExit; private ReplExecutionEventNodeImpl( - boolean atExit, + OutputStream atExit, EventContext eventContext, DebuggerMessageHandler handler, TruffleLogger logger) { @@ -139,7 +145,7 @@ private ReplExecutionEventNodeImpl( this.atExit = atExit; } - private Object getValue(MaterializedFrame frame, FramePointer ptr) { + private Object getValue(MaterializedFrame frame, FramePointer ptr, boolean onlyWarnings) { var raw = getProperFrame(frame, ptr).getValue(ptr.frameSlotIdx()); if (WarningsLibrary.getUncached().hasWarnings(raw)) { try { @@ -162,7 +168,7 @@ private Object getValue(MaterializedFrame frame, FramePointer ptr) { // go on } } - return raw; + return onlyWarnings ? null : raw; } private MaterializedFrame getProperFrame(MaterializedFrame frame, FramePointer ptr) { @@ -175,11 +181,19 @@ private MaterializedFrame getProperFrame(MaterializedFrame frame, FramePointer p @Override public Map listBindings() { + return listBindings(false); + } + + public Map listBindings(boolean onlyWarnings) { 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 = + getValue(nodeState.getLastScope().getFrame(), entry.getValue(), onlyWarnings); + if (valueOrNull != null) { + result.put(entry.getKey(), valueOrNull); + } } return result; } @@ -232,7 +246,7 @@ public void exit() { */ @Override protected void onEnter(VirtualFrame frame) { - if (!atExit) { + 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] @@ -244,8 +258,8 @@ protected void onEnter(VirtualFrame frame) { @Override public void onReturnValue(VirtualFrame frame, Object result) { - if (atExit) { - startSession(getRootNode(), frame); + if (atExit != null) { + startSession(getRootNode(), frame, result); } } @@ -270,17 +284,16 @@ protected Object onUnwind(VirtualFrame frame, Object info) { return nodeState.getLastReturn(); } - private void startSession(RootNode root, VirtualFrame frame) { + 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) { - var 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); + nodeState = new ReplExecutionEventNodeState(toReturn, lastScope); startSessionImpl(); } } @@ -290,9 +303,20 @@ 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); + } + } + } exit(); } } @@ -335,7 +359,7 @@ private final class AtTheEndOfMethod implements ExecutionEventNodeFactory { @Override public ExecutionEventNode create(EventContext ctx) { var log = env.getLogger(ReplExecutionEventNodeImpl.class); - return new ReplExecutionEventNodeImpl(true, ctx, handler, log); + return new ReplExecutionEventNodeImpl(env.err(), ctx, handler, log); } } } diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/instrument/DebugServerWithScriptTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/instrument/DebugServerWithScriptTest.java new file mode 100644 index 000000000000..7d0103fe3600 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/instrument/DebugServerWithScriptTest.java @@ -0,0 +1,70 @@ +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.assertTrue; + +import java.io.ByteArrayOutputStream; +import org.enso.polyglot.debugger.DebugServerInfo; +import org.enso.test.utils.ContextUtils; +import org.graalvm.polyglot.Context; +import org.hamcrest.core.AllOf; +import org.junit.Test; + +public class DebugServerWithScriptTest { + private interface WithContext { + void action(Context ctx, Object out, Object err) throws Exception; + } + + private void withContext(String script, WithContext action) throws Exception { + var out = new ByteArrayOutputStream(); + var err = new ByteArrayOutputStream(); + + var b = ContextUtils.defaultContextBuilder().out(out).err(err); + + b.option(DebugServerInfo.FN_OPTION, "ScriptTest.inspect"); + + try (var ctx = b.build()) { + action.action(ctx, out, err); + } + } + + @Test + public void propertyListingVariables() throws Exception { + withContext( + ":list\n:q", + (ctx, out, err) -> { + var r = + ContextUtils.evalModule( + ctx, + """ + from Standard.Base import all + + inspect = + j = 1 + d = Warning.attach "doubled value" 2 + t = j + d + v = [j, d, t] + v + """, + "ScriptTest", + "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( + "Error contains some warnings", + err.toString(), + AllOf.allOf( + containsString("d = 2"), + containsString("t = 3"), + containsString("doubled value"), + not(containsString("j = 1")))); + }); + } +} 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 db432e99cd69..3c3aebadfbc7 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 static Value createValue(Context ctx, String src) { /** * 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 mainMethod = module.invokeMember(Module.GET_METHOD, assocType, methodName); + return mainMethod.execute(assocType); } public static org.enso.compiler.core.ir.Module compileModule(Context ctx, String src) {