Repl & Debugger (#345)

This commit is contained in:
Marcin Kostrzewa 2019-11-19 16:16:58 +01:00 committed by GitHub
parent 9f3fda1b42
commit 427e784663
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 759 additions and 24 deletions

View File

@ -309,11 +309,14 @@ val truffleRunOptionsSettings = Seq(
) )
lazy val runtime = (project in file("engine/runtime")) lazy val runtime = (project in file("engine/runtime"))
.configs(Benchmark)
.settings( .settings(
version := "0.1", version := "0.1",
commands += WithDebugCommand.withDebug, commands += WithDebugCommand.withDebug,
inConfig(Compile)(truffleRunOptionsSettings), inConfig(Compile)(truffleRunOptionsSettings),
inConfig(Test)(truffleRunOptionsSettings), inConfig(Test)(truffleRunOptionsSettings),
inConfig(Benchmark)(Defaults.testSettings),
inConfig(Benchmark)(truffleRunOptionsSettings),
parallelExecution in Test := false, parallelExecution in Test := false,
logBuffered in Test := false, logBuffered in Test := false,
libraryDependencies ++= jmh ++ Seq( libraryDependencies ++= jmh ++ Seq(
@ -330,9 +333,9 @@ lazy val runtime = (project in file("engine/runtime"))
"org.scalacheck" %% "scalacheck" % "1.14.0" % Test, "org.scalacheck" %% "scalacheck" % "1.14.0" % Test,
"org.scalactic" %% "scalactic" % "3.0.8" % Test, "org.scalactic" %% "scalactic" % "3.0.8" % Test,
"org.scalatest" %% "scalatest" % "3.2.0-SNAP10" % Test, "org.scalatest" %% "scalatest" % "3.2.0-SNAP10" % Test,
"org.graalvm.truffle" % "truffle-api" % graalVersion % Benchmark,
"org.typelevel" %% "cats-core" % "2.0.0-M4" "org.typelevel" %% "cats-core" % "2.0.0-M4"
), )
libraryDependencies ++= jmh
) )
.settings( .settings(
(Compile / javacOptions) ++= Seq( (Compile / javacOptions) ++= Seq(
@ -345,11 +348,8 @@ lazy val runtime = (project in file("engine/runtime"))
.dependsOn(Def.task { (Compile / sourceManaged).value.mkdirs }) .dependsOn(Def.task { (Compile / sourceManaged).value.mkdirs })
.value .value
) )
.configs(Benchmark)
.settings( .settings(
logBuffered := false, logBuffered := false,
inConfig(Benchmark)(Defaults.testSettings),
inConfig(Benchmark)(truffleRunOptionsSettings),
bench := (test in Benchmark).tag(Exclusive).value, bench := (test in Benchmark).tag(Exclusive).value,
benchOnly := Def.inputTaskDyn { benchOnly := Def.inputTaskDyn {
import complete.Parsers.spaceDelimited import complete.Parsers.spaceDelimited
@ -385,8 +385,10 @@ lazy val language_server = project
inConfig(Compile)(truffleRunOptionsSettings), inConfig(Compile)(truffleRunOptionsSettings),
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.graalvm.sdk" % "polyglot-tck" % graalVersion % "provided", "org.graalvm.sdk" % "polyglot-tck" % graalVersion % "provided",
"org.graalvm.truffle" % "truffle-api" % graalVersion % "provided",
"commons-cli" % "commons-cli" % "1.4", "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( .settings(

View File

@ -4,6 +4,7 @@ import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import org.enso.interpreter.Constants import org.enso.interpreter.Constants
import org.enso.interpreter.instrument.ReplDebuggerInstrument
import org.enso.interpreter.runtime.RuntimeOptions import org.enso.interpreter.runtime.RuntimeOptions
import org.graalvm.polyglot.Context import org.graalvm.polyglot.Context
@ -18,14 +19,16 @@ class ContextFactory {
* @param packagesPath Enso packages path * @param packagesPath Enso packages path
* @param in the input stream for standard in * @param in the input stream for standard in
* @param out the output stream for standard out * @param out the output stream for standard out
* @param repl the Repl manager to use for this context
* @return configured Context instance * @return configured Context instance
*/ */
def create( def create(
packagesPath: String = "", packagesPath: String = "",
in: InputStream = System.in, in: InputStream,
out: OutputStream = System.out out: OutputStream,
): Context = repl: Repl
Context ): Context = {
val context = Context
.newBuilder(Constants.LANGUAGE_ID) .newBuilder(Constants.LANGUAGE_ID)
.allowExperimentalOptions(true) .allowExperimentalOptions(true)
.allowAllAccess(true) .allowAllAccess(true)
@ -33,4 +36,10 @@ class ContextFactory {
.out(out) .out(out)
.in(in) .in(in)
.build .build
val instrument = context.getEngine.getInstruments
.get(ReplDebuggerInstrument.INSTRUMENT_ID)
.lookup(classOf[ReplDebuggerInstrument])
instrument.setSessionManager(repl)
context
}
} }

View File

@ -18,7 +18,12 @@ import org.graalvm.polyglot.Context
*/ */
class JupyterKernel extends BaseKernel { class JupyterKernel extends BaseKernel {
private val context: Context = 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 * Evaluates Enso code in the context of Jupyter request

View File

@ -13,6 +13,7 @@ object Main {
private val RUN_OPTION = "run" private val RUN_OPTION = "run"
private val HELP_OPTION = "help" private val HELP_OPTION = "help"
private val NEW_OPTION = "new" private val NEW_OPTION = "new"
private val REPL_OPTION = "repl"
private val JUPYTER_OPTION = "jupyter-kernel" private val JUPYTER_OPTION = "jupyter-kernel"
/** /**
@ -26,6 +27,10 @@ object Main {
.longOpt(HELP_OPTION) .longOpt(HELP_OPTION)
.desc("Displays this message.") .desc("Displays this message.")
.build .build
val repl = Option.builder
.longOpt(REPL_OPTION)
.desc("Runs the Enso REPL.")
.build
val run = Option.builder val run = Option.builder
.hasArg(true) .hasArg(true)
.numberOfArgs(1) .numberOfArgs(1)
@ -50,6 +55,7 @@ object Main {
val options = new Options val options = new Options
options options
.addOption(help) .addOption(help)
.addOption(repl)
.addOption(run) .addOption(run)
.addOption(newOpt) .addOption(newOpt)
.addOption(jupyterOption) .addOption(jupyterOption)
@ -105,12 +111,28 @@ object Main {
} }
mainLocation = main.get mainLocation = main.get
} }
val context = new ContextFactory().create(packagePath) val context = new ContextFactory().create(
packagePath,
System.in,
System.out,
Repl(TerminalIO())
)
val source = Source.newBuilder(Constants.LANGUAGE_ID, mainLocation).build val source = Source.newBuilder(Constants.LANGUAGE_ID, mainLocation).build
context.eval(source) context.eval(source)
exitSuccess() 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. * Main entry point for the CLI program.
* *
@ -134,6 +156,9 @@ object Main {
if (line.hasOption(RUN_OPTION)) { if (line.hasOption(RUN_OPTION)) {
run(line.getOptionValue(RUN_OPTION)) run(line.getOptionValue(RUN_OPTION))
} }
if (line.hasOption(REPL_OPTION)) {
runRepl()
}
if (line.hasOption(JUPYTER_OPTION)) { if (line.hasOption(JUPYTER_OPTION)) {
new JupyterKernel().run(line.getOptionValue(JUPYTER_OPTION)) new JupyterKernel().run(line.getOptionValue(JUPYTER_OPTION))
} }

View File

@ -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)}")
}
}
}
}
}

View File

@ -3,6 +3,7 @@ package org.enso.interpreter;
import com.oracle.truffle.api.CallTarget; import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.Truffle; import com.oracle.truffle.api.Truffle;
import com.oracle.truffle.api.TruffleLanguage; 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.ProvidedTags;
import com.oracle.truffle.api.instrumentation.StandardTags; import com.oracle.truffle.api.instrumentation.StandardTags;
import com.oracle.truffle.api.nodes.RootNode; import com.oracle.truffle.api.nodes.RootNode;
@ -33,6 +34,7 @@ import org.graalvm.options.OptionDescriptors;
contextPolicy = TruffleLanguage.ContextPolicy.SHARED, contextPolicy = TruffleLanguage.ContextPolicy.SHARED,
fileTypeDetectors = FileDetector.class) fileTypeDetectors = FileDetector.class)
@ProvidedTags({ @ProvidedTags({
DebuggerTags.AlwaysHalt.class,
StandardTags.CallTag.class, StandardTags.CallTag.class,
StandardTags.ExpressionTag.class, StandardTags.ExpressionTag.class,
StandardTags.RootTag.class, StandardTags.RootTag.class,

View File

@ -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<String, Object> listBindings() {
Map<String, FramePointer> flatScope = lastScope.getLocalScope().flatten();
Map<String, Object> result = new HashMap<>();
for (Map.Entry<String, FramePointer> 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.
*
* <p>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.
*
* <p>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.
*
* <p>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);
}
}
}

View File

@ -15,13 +15,10 @@ import org.enso.interpreter.runtime.scope.ModuleScope;
public abstract class EnsoRootNode extends RootNode { public abstract class EnsoRootNode extends RootNode {
private final String name; private final String name;
private final SourceSection sourceSection; private final SourceSection sourceSection;
private final FrameSlot stateFrameSlot;
private final LocalScope localScope; private final LocalScope localScope;
private final ModuleScope moduleScope; private final ModuleScope moduleScope;
private @CompilerDirectives.CompilationFinal TruffleLanguage.ContextReference<Context> private @CompilerDirectives.CompilationFinal TruffleLanguage.ContextReference<Context>
contextReference; contextReference;
private @CompilerDirectives.CompilationFinal TruffleLanguage.LanguageReference<Language>
languageReference;
/** /**
* Constructs the root node. * Constructs the root node.
@ -43,8 +40,6 @@ public abstract class EnsoRootNode extends RootNode {
this.localScope = localScope; this.localScope = localScope;
this.moduleScope = moduleScope; this.moduleScope = moduleScope;
this.sourceSection = sourceSection; this.sourceSection = sourceSection;
this.stateFrameSlot =
localScope.getFrameDescriptor().findOrAddFrameSlot("<<state>>", FrameSlotKind.Object);
} }
/** /**
@ -84,7 +79,7 @@ public abstract class EnsoRootNode extends RootNode {
* @return the state frame slot * @return the state frame slot
*/ */
public FrameSlot getStateFrameSlot() { public FrameSlot getStateFrameSlot() {
return this.stateFrameSlot; return localScope.getStateFrameSlot();
} }
/** /**

View File

@ -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));
}
}

View File

@ -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.
*
* <p>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<? extends Tag> 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);
}
}

View File

@ -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));
}
}

View File

@ -20,6 +20,11 @@ import org.enso.interpreter.runtime.state.Stateful;
/** Node running Enso expressions passed to it as strings. */ /** Node running Enso expressions passed to it as strings. */
@NodeInfo(description = "Evaluates code passed to it as string") @NodeInfo(description = "Evaluates code passed to it as string")
public abstract class EvalNode extends BaseNode { public abstract class EvalNode extends BaseNode {
private final boolean shouldCaptureResultScope;
EvalNode(boolean shouldCaptureResultScope) {
this.shouldCaptureResultScope = shouldCaptureResultScope;
}
/** /**
* Creates an instance of this node. * Creates an instance of this node.
@ -27,7 +32,16 @@ public abstract class EvalNode extends BaseNode {
* @return an instance of this node * @return an instance of this node
*/ */
public static EvalNode build() { 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() .get()
.compiler() .compiler()
.runInline(expression, language, localScope, moduleScope); .runInline(expression, language, localScope, moduleScope);
if (shouldCaptureResultScope) {
expr = CaptureResultScopeNode.build(expr);
}
ClosureRootNode framedNode = ClosureRootNode framedNode =
new ClosureRootNode( new ClosureRootNode(
lookupLanguageReference(Language.class).get(), lookupLanguageReference(Language.class).get(),

View File

@ -2,6 +2,7 @@ package org.enso.interpreter.runtime;
import org.enso.interpreter.Language; import org.enso.interpreter.Language;
import org.enso.interpreter.node.expression.builtin.IfZeroNode; 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.debug.DebugEvalNode;
import org.enso.interpreter.node.expression.builtin.error.CatchErrorNode; import org.enso.interpreter.node.expression.builtin.error.CatchErrorNode;
import org.enso.interpreter.node.expression.builtin.error.CatchPanicNode; 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(state, "run", RunStateNode.makeFunction(language));
scope.registerMethod(debug, "eval", DebugEvalNode.makeFunction(language)); scope.registerMethod(debug, "eval", DebugEvalNode.makeFunction(language));
scope.registerMethod(debug, "breakpoint", DebugBreakpointNode.makeFunction(language));
} }
/** /**

View File

@ -2,6 +2,7 @@ package org.enso.interpreter.runtime.scope;
import com.oracle.truffle.api.frame.FrameDescriptor; import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.frame.FrameSlot; import com.oracle.truffle.api.frame.FrameSlot;
import com.oracle.truffle.api.frame.FrameSlotKind;
import org.enso.interpreter.runtime.error.VariableRedefinitionException; import org.enso.interpreter.runtime.error.VariableRedefinitionException;
import java.util.HashMap; import java.util.HashMap;
@ -14,9 +15,10 @@ import java.util.Optional;
* frames. * frames.
*/ */
public class LocalScope { public class LocalScope {
public final Map<String, FrameSlot> items;
private final FrameDescriptor frameDescriptor; private final FrameDescriptor frameDescriptor;
public final LocalScope parent; private final LocalScope parent;
private final Map<String, FrameSlot> items;
private final FrameSlot stateFrameSlot;
/** Creates a root local scope. */ /** Creates a root local scope. */
public LocalScope() { public LocalScope() {
@ -29,9 +31,11 @@ public class LocalScope {
* @param parent the parent scope * @param parent the parent scope
*/ */
public LocalScope(LocalScope parent) { public LocalScope(LocalScope parent) {
items = new HashMap<>(); this.items = new HashMap<>();
frameDescriptor = new FrameDescriptor(); this.frameDescriptor = new FrameDescriptor();
this.parent = parent; this.parent = parent;
this.stateFrameSlot =
frameDescriptor.findOrAddFrameSlot("<<monadic_state>>", FrameSlotKind.Object);
} }
/** /**
@ -97,4 +101,31 @@ public class LocalScope {
} }
return Optional.empty(); 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<String, FramePointer> flattenWithLevel(int level) {
Map<String, FramePointer> parentResult =
parent == null ? new HashMap<>() : getParent().flattenWithLevel(level + 1);
for (Map.Entry<String, FrameSlot> 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<String, FramePointer> flatten() {
return flattenWithLevel(0);
}
} }

View File

@ -3,6 +3,7 @@ package org.enso.interpreter.test
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import org.enso.interpreter.Constants import org.enso.interpreter.Constants
import org.enso.interpreter.instrument.ReplDebuggerInstrument
import org.graalvm.polyglot.{Context, Value} import org.graalvm.polyglot.{Context, Value}
import org.scalatest.{FlatSpec, Matchers} import org.scalatest.{FlatSpec, Matchers}
@ -29,6 +30,12 @@ trait InterpreterRunner {
def parse(code: String): Value = def parse(code: String): Value =
InterpreterException.rethrowPolyglot(eval(code)) InterpreterException.rethrowPolyglot(eval(code))
def getReplInstrument: ReplDebuggerInstrument = {
ctx.getEngine.getInstruments
.get(ReplDebuggerInstrument.INSTRUMENT_ID)
.lookup(classOf[ReplDebuggerInstrument])
}
} }
trait InterpreterTest trait InterpreterTest

View File

@ -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
}
}