mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 21:12:44 +03:00
Repl & Debugger (#345)
This commit is contained in:
parent
9f3fda1b42
commit
427e784663
14
build.sbt
14
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(
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Context>
|
||||
contextReference;
|
||||
private @CompilerDirectives.CompilationFinal TruffleLanguage.LanguageReference<Language>
|
||||
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("<<state>>", 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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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(),
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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<String, FrameSlot> items;
|
||||
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. */
|
||||
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("<<monadic_state>>", 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<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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user