diff --git a/build.sbt b/build.sbt index 6225a5f659e..19180793c6c 100644 --- a/build.sbt +++ b/build.sbt @@ -309,11 +309,14 @@ val truffleRunOptionsSettings = Seq( ) lazy val runtime = (project in file("engine/runtime")) + .configs(Benchmark) .settings( version := "0.1", commands += WithDebugCommand.withDebug, inConfig(Compile)(truffleRunOptionsSettings), inConfig(Test)(truffleRunOptionsSettings), + inConfig(Benchmark)(Defaults.testSettings), + inConfig(Benchmark)(truffleRunOptionsSettings), parallelExecution in Test := false, logBuffered in Test := false, libraryDependencies ++= jmh ++ Seq( @@ -330,9 +333,9 @@ lazy val runtime = (project in file("engine/runtime")) "org.scalacheck" %% "scalacheck" % "1.14.0" % Test, "org.scalactic" %% "scalactic" % "3.0.8" % Test, "org.scalatest" %% "scalatest" % "3.2.0-SNAP10" % Test, + "org.graalvm.truffle" % "truffle-api" % graalVersion % Benchmark, "org.typelevel" %% "cats-core" % "2.0.0-M4" - ), - libraryDependencies ++= jmh + ) ) .settings( (Compile / javacOptions) ++= Seq( @@ -345,11 +348,8 @@ lazy val runtime = (project in file("engine/runtime")) .dependsOn(Def.task { (Compile / sourceManaged).value.mkdirs }) .value ) - .configs(Benchmark) .settings( logBuffered := false, - inConfig(Benchmark)(Defaults.testSettings), - inConfig(Benchmark)(truffleRunOptionsSettings), bench := (test in Benchmark).tag(Exclusive).value, benchOnly := Def.inputTaskDyn { import complete.Parsers.spaceDelimited @@ -385,8 +385,10 @@ lazy val language_server = project inConfig(Compile)(truffleRunOptionsSettings), libraryDependencies ++= Seq( "org.graalvm.sdk" % "polyglot-tck" % graalVersion % "provided", + "org.graalvm.truffle" % "truffle-api" % graalVersion % "provided", "commons-cli" % "commons-cli" % "1.4", - "io.github.spencerpark" % "jupyter-jvm-basekernel" % "2.3.0" + "io.github.spencerpark" % "jupyter-jvm-basekernel" % "2.3.0", + "org.jline" % "jline" % "3.1.3" ) ) .settings( diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/ContextFactory.scala b/engine/language-server/src/main/scala/org/enso/languageserver/ContextFactory.scala index ddd86b133df..88d750a7071 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/ContextFactory.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/ContextFactory.scala @@ -4,6 +4,7 @@ import java.io.InputStream import java.io.OutputStream import org.enso.interpreter.Constants +import org.enso.interpreter.instrument.ReplDebuggerInstrument import org.enso.interpreter.runtime.RuntimeOptions import org.graalvm.polyglot.Context @@ -18,14 +19,16 @@ class ContextFactory { * @param packagesPath Enso packages path * @param in the input stream for standard in * @param out the output stream for standard out + * @param repl the Repl manager to use for this context * @return configured Context instance */ def create( packagesPath: String = "", - in: InputStream = System.in, - out: OutputStream = System.out - ): Context = - Context + in: InputStream, + out: OutputStream, + repl: Repl + ): Context = { + val context = Context .newBuilder(Constants.LANGUAGE_ID) .allowExperimentalOptions(true) .allowAllAccess(true) @@ -33,4 +36,10 @@ class ContextFactory { .out(out) .in(in) .build + val instrument = context.getEngine.getInstruments + .get(ReplDebuggerInstrument.INSTRUMENT_ID) + .lookup(classOf[ReplDebuggerInstrument]) + instrument.setSessionManager(repl) + context + } } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/JupyterKernel.scala b/engine/language-server/src/main/scala/org/enso/languageserver/JupyterKernel.scala index 762ed481513..3bacd887884 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/JupyterKernel.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/JupyterKernel.scala @@ -18,7 +18,12 @@ import org.graalvm.polyglot.Context */ class JupyterKernel extends BaseKernel { private val context: Context = - new ContextFactory().create("", getIO.in, getIO.out) + new ContextFactory().create( + "", + getIO.in, + getIO.out, + Repl(SimpleReplIO(getIO.in, getIO.out)) + ) /** * Evaluates Enso code in the context of Jupyter request diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/Main.scala b/engine/language-server/src/main/scala/org/enso/languageserver/Main.scala index a13c70a49c4..ec375c9d6e3 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/Main.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/Main.scala @@ -13,6 +13,7 @@ object Main { private val RUN_OPTION = "run" private val HELP_OPTION = "help" private val NEW_OPTION = "new" + private val REPL_OPTION = "repl" private val JUPYTER_OPTION = "jupyter-kernel" /** @@ -26,6 +27,10 @@ object Main { .longOpt(HELP_OPTION) .desc("Displays this message.") .build + val repl = Option.builder + .longOpt(REPL_OPTION) + .desc("Runs the Enso REPL.") + .build val run = Option.builder .hasArg(true) .numberOfArgs(1) @@ -50,6 +55,7 @@ object Main { val options = new Options options .addOption(help) + .addOption(repl) .addOption(run) .addOption(newOpt) .addOption(jupyterOption) @@ -105,12 +111,28 @@ object Main { } mainLocation = main.get } - val context = new ContextFactory().create(packagePath) - val source = Source.newBuilder(Constants.LANGUAGE_ID, mainLocation).build + val context = new ContextFactory().create( + packagePath, + System.in, + System.out, + Repl(TerminalIO()) + ) + val source = Source.newBuilder(Constants.LANGUAGE_ID, mainLocation).build context.eval(source) exitSuccess() } + /** + * Handles the `--repl` CLI option + */ + private def runRepl(): Unit = { + val dummySourceToTriggerRepl = "@{ @breakpoint[@Debug] }" + val context = + new ContextFactory().create("", System.in, System.out, Repl(TerminalIO())) + context.eval(Constants.LANGUAGE_ID, dummySourceToTriggerRepl) + exitSuccess() + } + /** * Main entry point for the CLI program. * @@ -134,6 +156,9 @@ object Main { if (line.hasOption(RUN_OPTION)) { run(line.getOptionValue(RUN_OPTION)) } + if (line.hasOption(REPL_OPTION)) { + runRepl() + } if (line.hasOption(JUPYTER_OPTION)) { new JupyterKernel().run(line.getOptionValue(JUPYTER_OPTION)) } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/Repl.scala b/engine/language-server/src/main/scala/org/enso/languageserver/Repl.scala new file mode 100644 index 00000000000..f5ae7c4bcd8 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/Repl.scala @@ -0,0 +1,140 @@ +package org.enso.languageserver + +import java.io.{InputStream, OutputStream, PrintStream} +import java.util.Scanner + +import org.enso.interpreter.instrument.ReplDebuggerInstrument +import org.jline.reader.{LineReader, LineReaderBuilder} +import org.jline.terminal.{Terminal, TerminalBuilder} + +import collection.JavaConverters._ +import scala.util.Try + +/** + * Represents user console input. + */ +sealed trait UserInput + +/** + * End of user input. + */ +case object EndOfInput extends UserInput + +/** + * A normal line of user input. + * @param line the contents of the input line + */ +case class Line(line: String) extends UserInput + +/** + * Abstract representation of Repl IO operations + */ +trait ReplIO { + + /** + * Ask user for a line of input, using given prompt + * @param prompt the prompt to display to the user + * @return the user-provided input + */ + def readLine(prompt: String): UserInput + + /** + * Print a line to the REPL. + * @param contents contents of the line to print + */ + def println(contents: String): Unit +} + +/** + * A barebones implementation of [[ReplIO]] based on standard input / output operations. + * @param in input stream to use + * @param out output stream to use + */ +case class SimpleReplIO(in: InputStream, out: OutputStream) extends ReplIO { + private val scanner: Scanner = new Scanner(in) + private val printer: PrintStream = new PrintStream(out) + + /** + * Ask user for a line of input, using given prompt + * @param prompt the prompt to display to the user + * @return the user-provided input + */ + override def readLine(prompt: String): UserInput = { + printer.print(prompt) + Try(scanner.nextLine()).map(Line).getOrElse(EndOfInput) + } + + /** + * Print a line to the REPL. + * @param contents contents of the line to print + */ + override def println(contents: String): Unit = printer.println(contents) +} + +/** + * An implementation of [[ReplIO]] using system terminal capabilities. + */ +case class TerminalIO() extends ReplIO { + private val terminal: Terminal = + TerminalBuilder.builder().system(true).build() + private val lineReader: LineReader = + LineReaderBuilder.builder().terminal(terminal).build() + + /** + * Ask user for a line of input, using given prompt + * @param prompt the prompt to display to the user + * @return the user-provided input + */ + override def readLine(prompt: String): UserInput = + Try(lineReader.readLine(prompt)).map(Line).getOrElse(EndOfInput) + + /** + * Print a line to the REPL. + * @param contents contents of the line to print + */ + override def println(contents: String): Unit = + terminal.writer().println(contents) +} + +/** + * The Repl logic to inject into runtime instrumentation framework. + * + * @param replIO the IO implementation to use with this Repl + */ +case class Repl(replIO: ReplIO) extends ReplDebuggerInstrument.SessionManager { + + /** + * Runs the Repl session by asking for user input and performing + * the requested action with the execution node. + * + * End of input causes exit from the Repl. + * + * @param executionNode the execution node capable of performing + * language-level operations + */ + override def startSession( + executionNode: ReplDebuggerInstrument.ReplExecutionEventNode + ): Unit = { + while (true) { + val input = replIO.readLine("> ") + input match { + case EndOfInput => + executionNode.exit() + return + case Line(line) => + if (line == ":list" || line == ":l") { + val bindings = executionNode.listBindings().asScala + bindings.foreach { + case (varName, value) => + replIO.println(s"$varName = $value") + } + } else if (line == ":quit" || line == ":q") { + executionNode.exit() + return + } else { + replIO.println(s">>> ${executionNode.evaluate(line)}") + } + } + } + } +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/Language.java b/engine/runtime/src/main/java/org/enso/interpreter/Language.java index 1a0db375986..3f1c2282cbf 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/Language.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/Language.java @@ -3,6 +3,7 @@ package org.enso.interpreter; import com.oracle.truffle.api.CallTarget; import com.oracle.truffle.api.Truffle; import com.oracle.truffle.api.TruffleLanguage; +import com.oracle.truffle.api.debug.DebuggerTags; import com.oracle.truffle.api.instrumentation.ProvidedTags; import com.oracle.truffle.api.instrumentation.StandardTags; import com.oracle.truffle.api.nodes.RootNode; @@ -33,6 +34,7 @@ import org.graalvm.options.OptionDescriptors; contextPolicy = TruffleLanguage.ContextPolicy.SHARED, fileTypeDetectors = FileDetector.class) @ProvidedTags({ + DebuggerTags.AlwaysHalt.class, StandardTags.CallTag.class, StandardTags.ExpressionTag.class, StandardTags.RootTag.class, diff --git a/engine/runtime/src/main/java/org/enso/interpreter/instrument/ReplDebuggerInstrument.java b/engine/runtime/src/main/java/org/enso/interpreter/instrument/ReplDebuggerInstrument.java new file mode 100644 index 00000000000..000b34810c3 --- /dev/null +++ b/engine/runtime/src/main/java/org/enso/interpreter/instrument/ReplDebuggerInstrument.java @@ -0,0 +1,203 @@ +package org.enso.interpreter.instrument; + +import com.oracle.truffle.api.debug.DebuggerTags; +import com.oracle.truffle.api.frame.MaterializedFrame; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.instrumentation.*; +import org.enso.interpreter.Language; +import org.enso.interpreter.node.expression.debug.CaptureResultScopeNode; +import org.enso.interpreter.node.expression.debug.EvalNode; +import org.enso.interpreter.runtime.Builtins; +import org.enso.interpreter.runtime.callable.CallerInfo; +import org.enso.interpreter.runtime.callable.function.Function; +import org.enso.interpreter.runtime.scope.FramePointer; +import org.enso.interpreter.runtime.state.Stateful; + +import java.util.HashMap; +import java.util.Map; + +/** The Instrument implementation for the interactive debugger REPL. */ +@TruffleInstrument.Registration( + id = ReplDebuggerInstrument.INSTRUMENT_ID, + services = ReplDebuggerInstrument.class) +public class ReplDebuggerInstrument extends TruffleInstrument { + /** This instrument's registration id. */ + public static final String INSTRUMENT_ID = "enso-repl"; + + /** + * Internal reference type to store session manager and get the current version on each execution + * of this instrument. + */ + private static class SessionManagerReference { + private SessionManager sessionManager; + + /** + * Create a new instanc of this class + * + * @param sessionManager the session manager to initially store + */ + private SessionManagerReference(SessionManager sessionManager) { + this.sessionManager = sessionManager; + } + + /** + * Get the current session manager + * + * @return the current session manager + */ + private SessionManager get() { + return sessionManager; + } + + /** + * Set a new session manager for subsequent {@link #get()} calls. + * + * @param sessionManager the new session manager + */ + private void set(SessionManager sessionManager) { + this.sessionManager = sessionManager; + } + } + + /** An object controlling the execution of REPL. */ + public interface SessionManager { + /** + * Starts a new session with the provided execution node. + * + * @param executionNode the execution node that should be used for the duration of this session. + */ + void startSession(ReplExecutionEventNode executionNode); + } + + private SessionManagerReference sessionManagerReference = + new SessionManagerReference(ReplExecutionEventNode::exit); + + /** + * Called by Truffle when this instrument is installed. + * + * @param env the instrumentation environment + */ + @Override + protected void onCreate(Env env) { + SourceSectionFilter filter = + SourceSectionFilter.newBuilder().tagIs(DebuggerTags.AlwaysHalt.class).build(); + Instrumenter instrumenter = env.getInstrumenter(); + env.registerService(this); + instrumenter.attachExecutionEventFactory( + filter, ctx -> new ReplExecutionEventNode(ctx, sessionManagerReference)); + } + + /** + * Registers the session manager to use whenever this instrument is activated. + * + * @param sessionManager the session manager to use + */ + public void setSessionManager(SessionManager sessionManager) { + this.sessionManagerReference.set(sessionManager); + } + + /** The actual node that's installed as a probe on any node the instrument was launched for. */ + public static class ReplExecutionEventNode extends ExecutionEventNode { + private @Child EvalNode evalNode = EvalNode.buildWithResultScopeCapture(); + + private Object lastReturn; + private Object lastState; + private CallerInfo lastScope; + + private EventContext eventContext; + private SessionManagerReference sessionManagerReference; + + private ReplExecutionEventNode( + EventContext eventContext, SessionManagerReference sessionManagerReference) { + this.eventContext = eventContext; + this.sessionManagerReference = sessionManagerReference; + } + + private Object getValue(MaterializedFrame frame, FramePointer ptr) { + return getProperFrame(frame, ptr).getValue(ptr.getFrameSlot()); + } + + private MaterializedFrame getProperFrame(MaterializedFrame frame, FramePointer ptr) { + MaterializedFrame currentFrame = frame; + for (int i = 0; i < ptr.getParentLevel(); i++) { + currentFrame = Function.ArgumentsHelper.getLocalScope(currentFrame.getArguments()); + } + return currentFrame; + } + + /** + * Lists all the bindings available in the current execution scope. + * + * @return a map, where keys are variable names and values are current values of variables. + */ + public Map listBindings() { + Map flatScope = lastScope.getLocalScope().flatten(); + Map result = new HashMap<>(); + for (Map.Entry entry : flatScope.entrySet()) { + result.put(entry.getKey(), getValue(lastScope.getFrame(), entry.getValue())); + } + return result; + } + + /** + * Evaluates an arbitrary expression in the current execution context. + * + * @param expression the expression to evaluate + * @return the result of evaluating the expression + */ + public Object evaluate(String expression) { + try { + Stateful result = evalNode.execute(lastScope, lastState, expression); + lastState = result.getState(); + CaptureResultScopeNode.WithCallerInfo payload = + (CaptureResultScopeNode.WithCallerInfo) result.getValue(); + lastScope = payload.getCallerInfo(); + lastReturn = payload.getResult(); + return lastReturn; + } catch (Exception e) { + return e; + } + } + + /** + * Terminates this REPL session. + * + *

The last result of {@link #evaluate(String)} (or {@link Builtins#unit()} if {@link + * #evaluate(String)} was not called before) will be returned from the instrumented node. + * + *

This function must always be called at the end of REPL session, as otherwise the program + * will never resume. It's forbidden to use this object after exit has been called. + */ + public void exit() { + throw eventContext.createUnwind(lastReturn); + } + + /** + * Called by Truffle whenever this node starts execution. + * + * @param frame current execution frame + */ + @Override + protected void onEnter(VirtualFrame frame) { + lastScope = Function.ArgumentsHelper.getCallerInfo(frame.getArguments()); + lastReturn = lookupContextReference(Language.class).get().getUnit().newInstance(); + lastState = lastScope.getFrame().getValue(lastScope.getLocalScope().getStateFrameSlot()); + sessionManagerReference.get().startSession(this); + } + + /** + * Called by Truffle whenever an unwind {@see {@link EventContext#createUnwind(Object)}} was + * thrown in the course of REPL execution. + * + *

We use this mechanism to inject the REPL-returned value back into caller code. + * + * @param frame current execution frame + * @param info The unwind's payload. Currently unused. + * @return the object that will become the instrumented node's return value + */ + @Override + protected Object onUnwind(VirtualFrame frame, Object info) { + return new Stateful(lastState, lastReturn); + } + } +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/EnsoRootNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/EnsoRootNode.java index 4a2ea7e544c..5709be194ec 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/EnsoRootNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/EnsoRootNode.java @@ -15,13 +15,10 @@ import org.enso.interpreter.runtime.scope.ModuleScope; public abstract class EnsoRootNode extends RootNode { private final String name; private final SourceSection sourceSection; - private final FrameSlot stateFrameSlot; private final LocalScope localScope; private final ModuleScope moduleScope; private @CompilerDirectives.CompilationFinal TruffleLanguage.ContextReference contextReference; - private @CompilerDirectives.CompilationFinal TruffleLanguage.LanguageReference - languageReference; /** * Constructs the root node. @@ -43,8 +40,6 @@ public abstract class EnsoRootNode extends RootNode { this.localScope = localScope; this.moduleScope = moduleScope; this.sourceSection = sourceSection; - this.stateFrameSlot = - localScope.getFrameDescriptor().findOrAddFrameSlot("<>", FrameSlotKind.Object); } /** @@ -84,7 +79,7 @@ public abstract class EnsoRootNode extends RootNode { * @return the state frame slot */ public FrameSlot getStateFrameSlot() { - return this.stateFrameSlot; + return localScope.getStateFrameSlot(); } /** diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/debug/DebugBreakpointNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/debug/DebugBreakpointNode.java new file mode 100644 index 00000000000..acc409f7842 --- /dev/null +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/debug/DebugBreakpointNode.java @@ -0,0 +1,50 @@ +package org.enso.interpreter.node.expression.builtin.debug; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import org.enso.interpreter.Constants; +import org.enso.interpreter.Language; +import org.enso.interpreter.node.expression.builtin.BuiltinRootNode; +import org.enso.interpreter.node.expression.debug.BreakpointNode; +import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition; +import org.enso.interpreter.runtime.callable.function.Function; +import org.enso.interpreter.runtime.callable.function.FunctionSchema; +import org.enso.interpreter.runtime.state.Stateful; + +/** Root of the builtin Debug.breakpoint function. */ +@NodeInfo( + shortName = "Debug.breakpoint", + description = "Root of the builtin Debug.breakpoint function.") +public class DebugBreakpointNode extends BuiltinRootNode { + private @Child BreakpointNode instrumentableNode = BreakpointNode.build(); + + private DebugBreakpointNode(Language language) { + super(language); + } + + /** + * Executes this node by delegating to its instrumentable child. + * + * @param frame current execution frame + * @return the result of running the instrumentable node + */ + @Override + public Stateful execute(VirtualFrame frame) { + Object state = Function.ArgumentsHelper.getState(frame.getArguments()); + return instrumentableNode.execute(frame, state); + } + + /** + * Wraps this node in a 1-argument function. + * + * @param language current language instance + * @return the function wrapper for this node + */ + public static Function makeFunction(Language language) { + return Function.fromBuiltinRootNodeWithCallerFrameAccess( + new DebugBreakpointNode(language), + FunctionSchema.CallStrategy.ALWAYS_DIRECT, + new ArgumentDefinition( + 0, Constants.THIS_ARGUMENT_NAME, ArgumentDefinition.ExecutionMode.EXECUTE)); + } +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/debug/BreakpointNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/debug/BreakpointNode.java new file mode 100644 index 00000000000..a437309dd42 --- /dev/null +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/debug/BreakpointNode.java @@ -0,0 +1,82 @@ +package org.enso.interpreter.node.expression.debug; + +import com.oracle.truffle.api.debug.DebuggerTags; +import com.oracle.truffle.api.dsl.CachedContext; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.instrumentation.GenerateWrapper; +import com.oracle.truffle.api.instrumentation.InstrumentableNode; +import com.oracle.truffle.api.instrumentation.ProbeNode; +import com.oracle.truffle.api.instrumentation.Tag; +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.nodes.NodeInfo; +import org.enso.interpreter.Language; +import org.enso.interpreter.runtime.Builtins; +import org.enso.interpreter.runtime.Context; +import org.enso.interpreter.runtime.state.Stateful; + +/** A base node serving as an instrumentable marker. */ +@NodeInfo(description = "Instrumentation marker node.") +@GenerateWrapper +public abstract class BreakpointNode extends Node implements InstrumentableNode { + + /** + * Tells Truffle this node is instrumentable. + * + * @return {@code true} – this node is always instrumentable. + */ + @Override + public boolean isInstrumentable() { + return true; + } + + /** + * Execute this node. Does not do anything interesting, the default implementation returns {@link + * Builtins#unit()}. The behavior and return value of this node are assumed to be injected by + * attached instruments. + * + * @param frame current execution frame + * @param state current value of the monadic state + * @return the result of executing this node + */ + public abstract Stateful execute(VirtualFrame frame, Object state); + + @Specialization + Stateful execute( + VirtualFrame frame, Object state, @CachedContext(Language.class) Context context) { + return new Stateful(state, context.getUnit().newInstance()); + } + + /** + * Creates a new instance of this node. + * + * @return a new instance of this node + */ + public static BreakpointNode build() { + return BreakpointNodeGen.create(); + } + + /** + * Informs Truffle about the provided tags. + * + *

This node only provides the {@link DebuggerTags.AlwaysHalt} tag. + * + * @param tag the tag to verify + * @return {@code true} if the tag is {@link DebuggerTags.AlwaysHalt}, {@code false} otherwise + */ + @Override + public boolean hasTag(Class tag) { + return tag == DebuggerTags.AlwaysHalt.class; + } + + /** + * Creates an instrumentable wrapper node for this node. + * + * @param probeNode the probe node to wrap + * @return the wrapper instance wrapping both this and the probe node + */ + @Override + public WrapperNode createWrapper(ProbeNode probeNode) { + return new BreakpointNodeWrapper(this, probeNode); + } +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/debug/CaptureResultScopeNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/debug/CaptureResultScopeNode.java new file mode 100644 index 00000000000..557d8a0fccf --- /dev/null +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/debug/CaptureResultScopeNode.java @@ -0,0 +1,71 @@ +package org.enso.interpreter.node.expression.debug; + +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.nodes.NodeInfo; +import org.enso.interpreter.node.ExpressionNode; +import org.enso.interpreter.node.callable.CaptureCallerInfoNode; +import org.enso.interpreter.runtime.callable.CallerInfo; + +/** Node capturing the runtime execution scope of its child. */ +@NodeInfo(description = "Captures the child's execution scope.") +public class CaptureResultScopeNode extends ExpressionNode { + + /** Value object wrapping the expression return value and the execution scope. */ + public static class WithCallerInfo { + private final CallerInfo callerInfo; + private final Object result; + + private WithCallerInfo(CallerInfo callerInfo, Object result) { + this.callerInfo = callerInfo; + this.result = result; + } + + /** + * Gets the attached caller info object. + * + * @return the caller info (execution scope) captured by this node + */ + public CallerInfo getCallerInfo() { + return callerInfo; + } + + /** + * Gets the attached result object. + * + * @return the result captured by this node + */ + public Object getResult() { + return result; + } + } + + private @Child ExpressionNode expression; + private @Child CaptureCallerInfoNode captureCallerInfoNode = CaptureCallerInfoNode.build(); + + private CaptureResultScopeNode(ExpressionNode expression) { + this.expression = expression; + } + + /** + * Create a new instance of this node. + * + * @param expressionNode the child of this node, the return value of which should be captured + * @return an instance of this node + */ + public static CaptureResultScopeNode build(ExpressionNode expressionNode) { + return new CaptureResultScopeNode(expressionNode); + } + + /** + * Executes the node by running the child expression and capturing the caller info object + * containing current scope information. + * + * @param frame the stack frame for execution + * @return the captured caller info and the return value of the child expression + */ + @Override + public WithCallerInfo executeGeneric(VirtualFrame frame) { + return new WithCallerInfo( + captureCallerInfoNode.execute(frame), expression.executeGeneric(frame)); + } +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/debug/EvalNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/debug/EvalNode.java index 5c6478f6b60..0eb6aafccac 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/debug/EvalNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/debug/EvalNode.java @@ -20,6 +20,11 @@ import org.enso.interpreter.runtime.state.Stateful; /** Node running Enso expressions passed to it as strings. */ @NodeInfo(description = "Evaluates code passed to it as string") public abstract class EvalNode extends BaseNode { + private final boolean shouldCaptureResultScope; + + EvalNode(boolean shouldCaptureResultScope) { + this.shouldCaptureResultScope = shouldCaptureResultScope; + } /** * Creates an instance of this node. @@ -27,7 +32,16 @@ public abstract class EvalNode extends BaseNode { * @return an instance of this node */ public static EvalNode build() { - return EvalNodeGen.create(); + return EvalNodeGen.create(false); + } + + /** + * Creates an instance of this node, with frame capture enabled. + * + * @return an instance of this node + */ + public static EvalNode buildWithResultScopeCapture() { + return EvalNodeGen.create(true); } /** @@ -48,6 +62,9 @@ public abstract class EvalNode extends BaseNode { .get() .compiler() .runInline(expression, language, localScope, moduleScope); + if (shouldCaptureResultScope) { + expr = CaptureResultScopeNode.build(expr); + } ClosureRootNode framedNode = new ClosureRootNode( lookupLanguageReference(Language.class).get(), diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/Builtins.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/Builtins.java index f8535410531..8258dd2e1fc 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/Builtins.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/Builtins.java @@ -2,6 +2,7 @@ package org.enso.interpreter.runtime; import org.enso.interpreter.Language; import org.enso.interpreter.node.expression.builtin.IfZeroNode; +import org.enso.interpreter.node.expression.builtin.debug.DebugBreakpointNode; import org.enso.interpreter.node.expression.builtin.debug.DebugEvalNode; import org.enso.interpreter.node.expression.builtin.error.CatchErrorNode; import org.enso.interpreter.node.expression.builtin.error.CatchPanicNode; @@ -64,6 +65,7 @@ public class Builtins { scope.registerMethod(state, "run", RunStateNode.makeFunction(language)); scope.registerMethod(debug, "eval", DebugEvalNode.makeFunction(language)); + scope.registerMethod(debug, "breakpoint", DebugBreakpointNode.makeFunction(language)); } /** diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/LocalScope.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/LocalScope.java index 8cee5def69c..2fbd97dbf0f 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/LocalScope.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/LocalScope.java @@ -2,6 +2,7 @@ package org.enso.interpreter.runtime.scope; import com.oracle.truffle.api.frame.FrameDescriptor; import com.oracle.truffle.api.frame.FrameSlot; +import com.oracle.truffle.api.frame.FrameSlotKind; import org.enso.interpreter.runtime.error.VariableRedefinitionException; import java.util.HashMap; @@ -14,9 +15,10 @@ import java.util.Optional; * frames. */ public class LocalScope { - public final Map items; private final FrameDescriptor frameDescriptor; - public final LocalScope parent; + private final LocalScope parent; + private final Map items; + private final FrameSlot stateFrameSlot; /** Creates a root local scope. */ public LocalScope() { @@ -29,9 +31,11 @@ public class LocalScope { * @param parent the parent scope */ public LocalScope(LocalScope parent) { - items = new HashMap<>(); - frameDescriptor = new FrameDescriptor(); + this.items = new HashMap<>(); + this.frameDescriptor = new FrameDescriptor(); this.parent = parent; + this.stateFrameSlot = + frameDescriptor.findOrAddFrameSlot("<>", FrameSlotKind.Object); } /** @@ -97,4 +101,31 @@ public class LocalScope { } return Optional.empty(); } + + /** + * Gets the monadic state frame slot for this local scope. + * + * @return the frame slot containing monadic state + */ + public FrameSlot getStateFrameSlot() { + return stateFrameSlot; + } + + private Map flattenWithLevel(int level) { + Map parentResult = + parent == null ? new HashMap<>() : getParent().flattenWithLevel(level + 1); + for (Map.Entry entry : items.entrySet()) { + parentResult.put(entry.getKey(), new FramePointer(level, entry.getValue())); + } + return parentResult; + } + + /** + * Returns a flat representation of the scope, including variables from all parent lexical scopes. + * + * @return a flat representation of this scope + */ + public Map flatten() { + return flattenWithLevel(0); + } } diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/InterpreterTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/InterpreterTest.scala index c635f1cc59b..d912f0f6c6c 100644 --- a/engine/runtime/src/test/scala/org/enso/interpreter/test/InterpreterTest.scala +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/InterpreterTest.scala @@ -3,6 +3,7 @@ package org.enso.interpreter.test import java.io.ByteArrayOutputStream import org.enso.interpreter.Constants +import org.enso.interpreter.instrument.ReplDebuggerInstrument import org.graalvm.polyglot.{Context, Value} import org.scalatest.{FlatSpec, Matchers} @@ -29,6 +30,12 @@ trait InterpreterRunner { def parse(code: String): Value = InterpreterException.rethrowPolyglot(eval(code)) + + def getReplInstrument: ReplDebuggerInstrument = { + ctx.getEngine.getInstruments + .get(ReplDebuggerInstrument.INSTRUMENT_ID) + .lookup(classOf[ReplDebuggerInstrument]) + } } trait InterpreterTest diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/ReplTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/ReplTest.scala new file mode 100644 index 00000000000..ccdbbb6524c --- /dev/null +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/ReplTest.scala @@ -0,0 +1,94 @@ +package org.enso.interpreter.test.instrument + +import org.enso.interpreter.test.InterpreterTest +import collection.JavaConverters._ + +class ReplTest extends InterpreterTest { + "Repl" should "be able to list local variables in its scope" in { + val code = + """ + |@{ + | x = 10; + | y = 20; + | z = x + y; + | @breakpoint[@Debug] + |} + |""".stripMargin + var scopeResult: Map[String, AnyRef] = Map() + getReplInstrument.setSessionManager { executor => + scopeResult = executor.listBindings.asScala.toMap + executor.exit() + } + eval(code) + scopeResult shouldEqual Map("x" -> 10, "y" -> 20, "z" -> 30) + } + + "Repl" should "be able to execute arbitrary code in the caller scope" in { + val code = + """ + |@{ + | x = 1; + | y = 2; + | @breakpoint[@Debug] + |} + |""".stripMargin + var evalResult: AnyRef = null + getReplInstrument.setSessionManager { executor => + evalResult = executor.evaluate("x + y") + executor.exit() + } + eval(code) + evalResult shouldEqual 3 + } + + "Repl" should "return the last evaluated value back to normal execution flow" in { + val code = + """ + |@{ + | a = 5; + | b = 6; + | c = @breakpoint[@Debug]; + | c * a + |} + |""".stripMargin + getReplInstrument.setSessionManager { executor => + executor.evaluate("a + b") + executor.exit() + } + eval(code) shouldEqual 55 + } + + "Repl" should "be able to define its local variables" in { + val code = + """ + |@{ + | x = 10; + | @breakpoint[@Debug] + |} + |""".stripMargin + getReplInstrument.setSessionManager { executor => + executor.evaluate("y = x + 1") + executor.evaluate("z = y * x") + executor.evaluate("z") + executor.exit() + } + eval(code) shouldEqual 110 + } + + "Repl" should "access and modify monadic state" in { + val code = + """ + |@{ + | @put[@State, 10]; + | @breakpoint[@Debug]; + | @get[@State] + |} + |""".stripMargin + getReplInstrument.setSessionManager { executor => + executor.evaluate("x = @get[@State]") + executor.evaluate("@put[@State, x+1]") + executor.exit() + } + eval(code) shouldEqual 11 + } +}