Implement debbuger server in the instrument (#822)

This commit is contained in:
Radosław Waśko 2020-06-09 16:23:52 +02:00 committed by GitHub
parent a5f6d789b1
commit af8b5f88cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 861 additions and 213 deletions

View File

@ -100,7 +100,6 @@ union ResponsePayload {
EVALUATION_SUCCESS: EvaluationSuccess,
EVALUATION_FAILURE: EvaluationFailure,
LIST_BINDINGS: ListBindingsResult,
SESSION_EXIT: SessionExitSuccess,
SESSION_START: SessionStartNotification
}
@ -114,6 +113,10 @@ table Response {
When a breakpoint is reached, the debugger sends a notification to the client
indicating that a REPL session should be started.
The whole REPL session should live inside the endpoint's function handling this
notification. This means that this function should not return before sending the
session exit request.
#### Notification
```idl
namespace org.enso.polyglot.protocol.debugger;
@ -176,8 +179,10 @@ Terminates this REPL session (and resumes normal program execution).
The last result of Evaluation will be returned from the instrumented node or if
no expressions have been evaluated, unit is returned.
This function must always be called at the end of REPL session, as otherwise the
program will never resume.
This request must always be sent at the end of REPL session, as otherwise the
program will never resume. It does not return any response. It is important to
note that a thread calling `sendBinary` with this message will never return, as
control will be passed to the interpreter.
#### Request
```idl
@ -185,10 +190,3 @@ namespace org.enso.polyglot.protocol.debugger;
table ReplExitRequest {}
```
#### Response
```idl
namespace org.enso.polyglot.protocol.debugger;
table ReplExitSuccess {}
```

View File

@ -0,0 +1,10 @@
package org.enso.polyglot.debugger;
/**
* Container for Runtime Server related constants.
*/
public class DebugServerInfo {
public static final String URI = "enso://debug-server";
public static final String INSTRUMENT_NAME = "enso-debug-server";
public static final String ENABLE_OPTION = INSTRUMENT_NAME + ".enable";
}

View File

@ -23,7 +23,9 @@ object Debugger {
* @param bytes the buffer to deserialize
* @return the deserialized message, if the byte buffer can be deserialized.
*/
def deserializeRequest(bytes: ByteBuffer): Option[Request] =
def deserializeRequest(
bytes: ByteBuffer
): Either[DeserializationFailedException, Request] =
try {
val inMsg = BinaryRequest.getRootAsRequest(bytes)
@ -32,15 +34,22 @@ object Debugger {
val evaluationRequest = inMsg
.payload(new protocol.EvaluationRequest())
.asInstanceOf[protocol.EvaluationRequest]
Some(EvaluationRequest(evaluationRequest.expression()))
Right(EvaluationRequest(evaluationRequest.expression()))
case RequestPayload.LIST_BINDINGS =>
Some(ListBindingsRequest)
Right(ListBindingsRequest)
case RequestPayload.SESSION_EXIT =>
Some(SessionExitRequest)
case _ => None
Right(SessionExitRequest)
case _ =>
Left(new DeserializationFailedException("Unknown payload type"))
}
} catch {
case _: Exception => None
case e: Exception =>
Left(
new DeserializationFailedException(
"Deserialization failed with an exception",
e
)
)
}
/**
@ -49,7 +58,9 @@ object Debugger {
* @param bytes the buffer to deserialize
* @return the deserialized message, if the byte buffer can be deserialized.
*/
def deserializeResponse(bytes: ByteBuffer): Option[Response] =
def deserializeResponse(
bytes: ByteBuffer
): Either[DeserializationFailedException, Response] =
try {
val inMsg = BinaryResponse.getRootAsResponse(bytes)
@ -58,12 +69,12 @@ object Debugger {
val evaluationResult = inMsg
.payload(new protocol.EvaluationSuccess())
.asInstanceOf[protocol.EvaluationSuccess]
Some(EvaluationSuccess(evaluationResult.result()))
Right(EvaluationSuccess(evaluationResult.result()))
case ResponsePayload.EVALUATION_FAILURE =>
val evaluationResult = inMsg
.payload(new protocol.EvaluationFailure())
.asInstanceOf[protocol.EvaluationFailure]
Some(EvaluationFailure(evaluationResult.exception()))
Right(EvaluationFailure(evaluationResult.exception()))
case ResponsePayload.LIST_BINDINGS =>
val bindingsResult = inMsg
.payload(new protocol.ListBindingsResult())
@ -73,15 +84,20 @@ object Debugger {
val binding = bindingsResult.bindings(i)
(binding.name(), binding.value())
}
Some(ListBindingsResult(bindings.toMap))
Right(ListBindingsResult(bindings.toMap))
case ResponsePayload.SESSION_START =>
Some(SessionStartNotification)
case ResponsePayload.SESSION_EXIT =>
Some(SessionExitSuccess)
case _ => None
Right(SessionStartNotification)
case _ =>
Left(new DeserializationFailedException("Unknown payload type"))
}
} catch {
case _: Exception => None
case e: Exception =>
Left(
new DeserializationFailedException(
"Deserialization failed with an exception",
e
)
)
}
/**
@ -209,24 +225,6 @@ object Debugger {
): ByteBuffer =
createListBindingsResult(bindings.asScala.toMap)
/**
* Creates an ExitSuccess message in the form of a ByteBuffer that can be
* sent from the debugger.
*
* @return the serialized message
*/
def createSessionExitSuccess(): ByteBuffer = {
implicit val builder: FlatBufferBuilder = new FlatBufferBuilder(64)
val replyOffset = ResponseFactory.createSessionExitSuccess()
val outMsg = BinaryResponse.createResponse(
builder,
ResponsePayload.SESSION_EXIT,
replyOffset
)
builder.finish(outMsg)
builder.dataBuffer()
}
/**
* Creates an SessionStartNotification message in the form of a ByteBuffer
* that can be sent from the debugger.

View File

@ -0,0 +1,145 @@
package org.enso.polyglot.debugger
import java.nio.ByteBuffer
import org.enso.polyglot.debugger.protocol.{
ExceptionRepresentation,
ObjectRepresentation
}
import org.graalvm.polyglot.io.MessageEndpoint
/**
* Class that can be returned by serverTransport to establish communication
* with the ReplDebuggerInstrument.
*/
class DebuggerSessionManagerEndpoint(
val sessionManager: SessionManager,
val peer: MessageEndpoint
) extends MessageEndpoint {
override def sendText(text: String): Unit = {}
override def sendBinary(data: ByteBuffer): Unit =
Debugger.deserializeResponse(data) match {
case Right(response) =>
handleResponse(response)
case Left(error) =>
throw error
}
override def sendPing(data: ByteBuffer): Unit = peer.sendPong(data)
override def sendPong(data: ByteBuffer): Unit = {}
override def sendClose(): Unit = {}
private val executorStack
: collection.mutable.Stack[ReplExecutorImplementation] =
collection.mutable.Stack.empty
private def currentExecutor: Option[ReplExecutorImplementation] =
executorStack.headOption
private def startNewSession(): Nothing = {
val newExecutor = new ReplExecutorImplementation
executorStack.push(newExecutor)
sessionManager.startSession(newExecutor)
}
private def endMostNestedSession(
requestingExecutor: ReplExecutorImplementation
): Unit = {
if (!currentExecutor.contains(requestingExecutor)) {
throw new IllegalStateException(
"Session termination requested not from the most nested session"
)
} else {
executorStack.pop()
}
}
private def handleResponse(response: Response): Unit =
if (response == SessionStartNotification) {
startNewSession()
} else {
currentExecutor match {
case Some(executor) =>
executor.onResponse(response)
case None =>
throw new IllegalStateException(
s"Unexpected response $response, but no session is running"
)
}
}
private class ReplExecutorImplementation extends ReplExecutor {
var evaluationResult
: Either[ExceptionRepresentation, ObjectRepresentation] = _
override def evaluate(
expression: String
): Either[ExceptionRepresentation, ObjectRepresentation] = {
ensureUsable()
evaluationResult = null
peer.sendBinary(Debugger.createEvaluationRequest(expression))
if (evaluationResult == null)
throw new IllegalStateException(
"DebuggerServer returned but did not send back expected result"
)
else
evaluationResult
}
var bindingsResult: Map[String, ObjectRepresentation] = _
override def listBindings(): Map[String, ObjectRepresentation] = {
ensureUsable()
bindingsResult = null
peer.sendBinary(Debugger.createListBindingsRequest())
if (bindingsResult == null)
throw new IllegalStateException(
"DebuggerServer returned but did not send back expected result"
)
else
bindingsResult
}
var exited: Boolean = false
def ensureUsable(): Unit = {
if (exited) {
throw new IllegalStateException(
"Cannot use the executor after exit() has been called"
)
}
}
override def exit(): Nothing = {
ensureUsable()
// Note [Debugger Session Exit Return]
endMostNestedSession(this)
exited = true
peer.sendBinary(Debugger.createSessionExitRequest())
throw new IllegalStateException(
"DebuggerServer unexpectedly returned from exit"
)
}
/* Note [Debugger Session Exit Return]
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Sending exit request throws an exception that returns control to the
* interpreter. Sending the request using the synchronous sendBinary
* function means that this function will never return. So cleanup has to be
* done before calling it.
*/
def onResponse(response: Response): Unit = {
response match {
case EvaluationSuccess(result) => evaluationResult = Right(result)
case EvaluationFailure(exception) => evaluationResult = Left(exception)
case ListBindingsResult(bindings) => bindingsResult = bindings
case SessionStartNotification =>
throw new IllegalStateException(
"Session start notification sent while the session is already" +
" running"
)
}
}
}
}

View File

@ -0,0 +1,4 @@
package org.enso.polyglot.debugger
class DeserializationFailedException(message: String, cause: Throwable = null)
extends RuntimeException(message, cause)

View File

@ -42,8 +42,3 @@ case class ListBindingsResult(bindings: Map[String, ObjectRepresentation])
* started.
*/
object SessionStartNotification extends Response
/**
* Represents a successful termination of the debugging session.
*/
object SessionExitSuccess extends Response

View File

@ -0,0 +1,66 @@
package org.enso.polyglot.debugger
import org.enso.polyglot.debugger.protocol.{
ExceptionRepresentation,
ObjectRepresentation
}
/**
* Interface for executing Repl commands inside of a Repl session.
*
* A single instance is valid only during the current session, it is provided
* to the SessionManager on start of each session.
*/
trait ReplExecutor {
/**
* Evaluates an arbitrary expression in the current execution context.
*
* @param expression the expression to evaluate
* @return the result of evaluating the expression or error
*/
def evaluate(
expression: String
): Either[ExceptionRepresentation, ObjectRepresentation]
/**
* 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.
*/
def listBindings(): Map[String, ObjectRepresentation]
/**
* Terminates this REPL session.
*
* The last result of [[evaluate]] (or `Unit` if [[evaluate]] 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.
*
* As it brings control back to the interpreter, it never returns.
*/
def exit(): Nothing
}
/**
* Trait that should be implemented by Repl users to define how to handle Repl
* sessions.
*/
trait SessionManager {
/**
* Method that is run when starting each Repl session. The whole session
* lives inside this method. It should always be finished by running
* `executor.exit()`.
*
* @param executor the interface for sending commands to the Repl during the
* session
* @return does not return as it has to be ended by a call to
* `executor.exit()` which brings control back to the interpreter.
*/
def startSession(executor: ReplExecutor): Nothing
}

View File

@ -6,7 +6,6 @@ import org.enso.polyglot.debugger.protocol.{
EvaluationFailure,
EvaluationSuccess,
ListBindingsResult,
SessionExitSuccess,
SessionStartNotification
}
@ -77,19 +76,6 @@ object ResponseFactory {
SessionStartNotification.endSessionStartNotification(builder)
}
/**
* Creates SessionExitSuccess inside a [[FlatBufferBuilder]].
*
* @param builder a class that helps build a FlatBuffer representation of
* complex objects
* @return an offset pointing to the FlatBuffer representation of the
* created object
*/
def createSessionExitSuccess()(implicit builder: FlatBufferBuilder): Int = {
SessionExitSuccess.startSessionExitSuccess(builder)
SessionExitSuccess.endSessionExitSuccess(builder)
}
private def createBinding(name: String, value: Object)(
implicit builder: FlatBufferBuilder
): Int = {

View File

@ -22,7 +22,6 @@ union ResponsePayload {
EVALUATION_SUCCESS: EvaluationSuccess,
EVALUATION_FAILURE: EvaluationFailure,
LIST_BINDINGS: ListBindingsResult,
SESSION_EXIT: SessionExitSuccess,
SESSION_START: SessionStartNotification
}

View File

@ -5,6 +5,3 @@ table SessionStartNotification {}
// Request to terminate the current session.
table SessionExitRequest {}
// Message sent to confirm that the current session has been terminated.
table SessionExitSuccess {}

View File

@ -0,0 +1,24 @@
package org.enso.polyglot.debugger
import org.scalatest.exceptions.{StackDepthException, TestFailedException}
import org.scalactic.source
trait EitherValue {
implicit def convertEitherToRightValueHelper[A, B](
either: Either[A, B]
)(implicit pos: source.Position): EitherRightValueHelper[A, B] =
new EitherRightValueHelper(either, pos)
class EitherRightValueHelper[A, B](
either: Either[A, B],
pos: source.Position
) {
def rightValue: B = either match {
case Right(value) => value
case Left(_) =>
throw new TestFailedException({ _: StackDepthException =>
Some(s"Either right value was expected, but it was $either")
}, None, pos)
}
}
}

View File

@ -4,16 +4,17 @@ import org.enso.polyglot.debugger.protocol.{
ExceptionRepresentation,
ObjectRepresentation
}
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
class SerializationTest extends AnyWordSpec with Matchers {
class SerializationTest extends AnyWordSpec with Matchers with EitherValue {
"EvaluationRequest" should {
"preserve all information when being serialized and deserialized" in {
val expression = "2 + 2"
val bytes = Debugger.createEvaluationRequest(expression)
val request = Debugger.deserializeRequest(bytes).get
val request = Debugger.deserializeRequest(bytes).rightValue
request shouldEqual EvaluationRequest(expression)
}
@ -22,7 +23,7 @@ class SerializationTest extends AnyWordSpec with Matchers {
"ListBindingsRequest" should {
"preserve all information when being serialized and deserialized" in {
val bytes = Debugger.createListBindingsRequest()
val request = Debugger.deserializeRequest(bytes).get
val request = Debugger.deserializeRequest(bytes).rightValue
request shouldEqual ListBindingsRequest
}
@ -31,7 +32,7 @@ class SerializationTest extends AnyWordSpec with Matchers {
"SessionExitRequest" should {
"preserve all information when being serialized and deserialized" in {
val bytes = Debugger.createSessionExitRequest()
val request = Debugger.deserializeRequest(bytes).get
val request = Debugger.deserializeRequest(bytes).rightValue
request shouldEqual SessionExitRequest
}
@ -45,11 +46,12 @@ class SerializationTest extends AnyWordSpec with Matchers {
"EvaluationSuccess" should {
"preserve all information when being serialized and deserialized" in {
val result = ("String", 42)
val bytes = Debugger.createEvaluationSuccess(result)
val request = Debugger.deserializeResponse(bytes).get
val result = ("String", 42)
val bytes = Debugger.createEvaluationSuccess(result)
val response = Debugger.deserializeResponse(bytes).rightValue
val EvaluationSuccess(repr) = request
response should matchPattern { case EvaluationSuccess(_) => }
val EvaluationSuccess(repr) = response
assert(objectRepresentationIsConsistent(result, repr))
}
}
@ -92,9 +94,10 @@ class SerializationTest extends AnyWordSpec with Matchers {
"preserve all information when being serialized and deserialized" in {
val exception = new RuntimeException("Test")
val bytes = Debugger.createEvaluationFailure(exception)
val request = Debugger.deserializeResponse(bytes).get
val response = Debugger.deserializeResponse(bytes).rightValue
val EvaluationFailure(repr) = request
response should matchPattern { case EvaluationFailure(_) => }
val EvaluationFailure(repr) = response
assert(
exceptionRepresentationIsConsistent(exception, repr),
"exception representation should be consistent"
@ -108,10 +111,11 @@ class SerializationTest extends AnyWordSpec with Matchers {
"a" -> "Test",
"b" -> int2Integer(42)
)
val bytes = Debugger.createListBindingsResult(bindings)
val request = Debugger.deserializeResponse(bytes).get
val bytes = Debugger.createListBindingsResult(bindings)
val response = Debugger.deserializeResponse(bytes).rightValue
val ListBindingsResult(bindingsRepr) = request
response should matchPattern { case ListBindingsResult(_) => }
val ListBindingsResult(bindingsRepr) = response
bindingsRepr.size shouldEqual bindings.size
assert(
@ -124,21 +128,12 @@ class SerializationTest extends AnyWordSpec with Matchers {
}
}
"SessionExitSuccess" should {
"preserve all information when being serialized and deserialized" in {
val bytes = Debugger.createSessionExitSuccess()
val request = Debugger.deserializeResponse(bytes).get
request shouldEqual SessionExitSuccess
}
}
"SessionExitNotification" should {
"preserve all information when being serialized and deserialized" in {
val bytes = Debugger.createSessionStartNotification()
val request = Debugger.deserializeResponse(bytes).get
val bytes = Debugger.createSessionStartNotification()
val response = Debugger.deserializeResponse(bytes).rightValue
request shouldEqual SessionStartNotification
response shouldEqual SessionStartNotification
}
}
}

View File

@ -4,6 +4,7 @@ import java.io.InputStream
import java.io.OutputStream
import org.enso.interpreter.instrument.ReplDebuggerInstrument
import org.enso.polyglot.debugger.DebugServerInfo
import org.enso.polyglot.{LanguageInfo, PolyglotContext, RuntimeOptions}
import org.graalvm.polyglot.Context
@ -35,10 +36,10 @@ class ContextFactory {
.option(RuntimeOptions.PACKAGES_PATH, packagesPath)
.option(RuntimeOptions.STRICT_ERRORS, strictErrors.toString)
.out(out)
.in(in)
.in(in) // TODO [RW] will put serverTransport here for #791
.build
val instrument = context.getEngine.getInstruments
.get(ReplDebuggerInstrument.INSTRUMENT_ID)
.get(DebugServerInfo.INSTRUMENT_NAME)
.lookup(classOf[ReplDebuggerInstrument])
instrument.setSessionManager(repl)
new PolyglotContext(context)

View File

@ -132,7 +132,13 @@ case class Repl(replIO: ReplIO) extends ReplDebuggerInstrument.SessionManager {
executionNode.exit()
return
} else {
replIO.println(s">>> ${executionNode.evaluate(line)}")
val result = executionNode.evaluate(line)
result match {
case Left(error) =>
replIO.println(s"Evaluation failed with error: $error")
case Right(value) =>
replIO.println(s">>> $value")
}
}
}
}

View File

@ -9,6 +9,9 @@ import com.oracle.truffle.api.instrumentation.Instrumenter;
import com.oracle.truffle.api.instrumentation.SourceSectionFilter;
import com.oracle.truffle.api.instrumentation.TruffleInstrument;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.enso.interpreter.Language;
@ -19,14 +22,21 @@ 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 org.enso.polyglot.debugger.DebugServerInfo;
import org.graalvm.options.OptionDescriptor;
import org.graalvm.options.OptionDescriptors;
import org.graalvm.options.OptionKey;
import org.graalvm.polyglot.io.MessageEndpoint;
import org.graalvm.polyglot.io.MessageTransport;
import scala.util.Either;
import scala.util.Left;
import scala.util.Right;
/** The Instrument implementation for the interactive debugger REPL. */
@TruffleInstrument.Registration(
id = ReplDebuggerInstrument.INSTRUMENT_ID,
id = DebugServerInfo.INSTRUMENT_NAME,
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
@ -36,7 +46,7 @@ public class ReplDebuggerInstrument extends TruffleInstrument {
private SessionManager sessionManager;
/**
* Create a new instanc of this class
* Create a new instance of this class
*
* @param sessionManager the session manager to initially store
*/
@ -63,7 +73,10 @@ public class ReplDebuggerInstrument extends TruffleInstrument {
}
}
/** An object controlling the execution of REPL. */
/** An object controlling the execution of REPL.
* Deprecated, will be removed in the next version.
* Please use org.enso.polyglot.debugger.SessionManager.
*/
public interface SessionManager {
/**
* Starts a new session with the provided execution node.
@ -84,11 +97,43 @@ public class ReplDebuggerInstrument extends TruffleInstrument {
@Override
protected void onCreate(Env env) {
SourceSectionFilter filter =
SourceSectionFilter.newBuilder().tagIs(DebuggerTags.AlwaysHalt.class).build();
SourceSectionFilter.newBuilder().tagIs(DebuggerTags.AlwaysHalt.class)
.build();
env.registerService(this); // TODO [RW] this seems unnecessary after #791
DebuggerMessageHandler handler = new DebuggerMessageHandler();
try {
MessageEndpoint client =
env.startServer(URI.create(DebugServerInfo.URI), handler);
if (client != null) {
handler.setClient(client);
} else {
env.getLogger(ReplDebuggerInstrument.class)
.warning("ReplDebuggerInstrument was initialized, " +
"but no client connected");
}
} catch (MessageTransport.VetoException e) {
env.getLogger(ReplDebuggerInstrument.class)
.warning("ReplDebuggerInstrument was initialized, " +
"but client connection has been vetoed");
} catch (IOException e) {
throw new RuntimeException(e);
}
// TODO [RW] in #791 move this inside try to not initialize the factory if
// there are no clients
Instrumenter instrumenter = env.getInstrumenter();
env.registerService(this);
instrumenter.attachExecutionEventFactory(
filter, ctx -> new ReplExecutionEventNode(ctx, sessionManagerReference));
instrumenter.attachExecutionEventFactory(filter, ctx ->
new ReplExecutionEventNode(ctx, sessionManagerReference, handler));
}
@Override
protected OptionDescriptors getOptionDescriptors() {
return OptionDescriptors.create(
Collections.singletonList(
OptionDescriptor.newBuilder(new OptionKey<>(""), DebugServerInfo.ENABLE_OPTION)
.build()));
}
/**
@ -110,11 +155,13 @@ public class ReplDebuggerInstrument extends TruffleInstrument {
private EventContext eventContext;
private SessionManagerReference sessionManagerReference;
private DebuggerMessageHandler handler;
private ReplExecutionEventNode(
EventContext eventContext, SessionManagerReference sessionManagerReference) {
EventContext eventContext, SessionManagerReference sessionManagerReference, DebuggerMessageHandler handler) {
this.eventContext = eventContext;
this.sessionManagerReference = sessionManagerReference;
this.handler = handler;
}
private Object getValue(MaterializedFrame frame, FramePointer ptr) {
@ -147,9 +194,10 @@ public class ReplDebuggerInstrument extends TruffleInstrument {
* Evaluates an arbitrary expression in the current execution context.
*
* @param expression the expression to evaluate
* @return the result of evaluating the expression
* @return the result of evaluating the expression or an exception that
* caused failure
*/
public Object evaluate(String expression) {
public Either<Exception, Object> evaluate(String expression) {
try {
Stateful result = evalNode.execute(lastScope, lastState, expression);
lastState = result.getState();
@ -157,9 +205,9 @@ public class ReplDebuggerInstrument extends TruffleInstrument {
(CaptureResultScopeNode.WithCallerInfo) result.getValue();
lastScope = payload.getCallerInfo();
lastReturn = payload.getResult();
return lastReturn;
return new Right<>(lastReturn);
} catch (Exception e) {
return e;
return new Left<>(e);
}
}
@ -187,7 +235,7 @@ public class ReplDebuggerInstrument extends TruffleInstrument {
lastReturn = lookupContextReference(Language.class).get().getUnit().newInstance();
// Note [Safe Access to State in the Debugger Instrument]
lastState = Function.ArgumentsHelper.getState(frame.getArguments());
sessionManagerReference.get().startSession(this);
startSession();
}
/* Note [Safe Access to State in the Debugger Instrument]
@ -210,5 +258,13 @@ public class ReplDebuggerInstrument extends TruffleInstrument {
protected Object onUnwind(VirtualFrame frame, Object info) {
return new Stateful(lastState, lastReturn);
}
private void startSession() {
if (handler.hasClient()) {
handler.startSession(this);
} else if (sessionManagerReference.get() != null) {
sessionManagerReference.get().startSession(this);
}
}
}
}

View File

@ -0,0 +1,107 @@
package org.enso.interpreter.instrument
import java.nio.ByteBuffer
import org.enso.interpreter.instrument.ReplDebuggerInstrument.ReplExecutionEventNode
import org.enso.polyglot.debugger.{
Debugger,
EvaluationRequest,
ListBindingsRequest,
Request,
SessionExitRequest
}
import org.graalvm.polyglot.io.MessageEndpoint
/**
* Helper class that handles communication with Debugger client and delegates
* request to the execution event node of the ReplDebuggerInstrument.
*/
class DebuggerMessageHandler extends MessageEndpoint {
private var client: MessageEndpoint = _
/**
* Sets the client end of the connection, after it has been established.
*
* @param ep the client endpoint.
*/
def setClient(ep: MessageEndpoint): Unit = client = ep
/**
* Checks if a client has been registered.
*
* @return a boolean value indicating whether a client is registered
*/
def hasClient: Boolean = client != null
override def sendText(text: String): Unit = {}
override def sendBinary(data: ByteBuffer): Unit = {
Debugger.deserializeRequest(data) match {
case Right(request) => onMessage(request)
case Left(error) => throw error
}
}
override def sendPing(data: ByteBuffer): Unit = client.sendPong(data)
override def sendPong(data: ByteBuffer): Unit = {}
override def sendClose(): Unit = {}
private def sendToClient(data: ByteBuffer): Unit = {
client.sendBinary(data)
}
private val executionNodeStack
: collection.mutable.Stack[ReplExecutionEventNode] =
collection.mutable.Stack.empty
private def currentExecutionNode: Option[ReplExecutionEventNode] =
executionNodeStack.headOption
/**
* Starts a REPL session by sending a message to the client.
*
* @param executionNode execution node used for instrumenting the session
*/
def startSession(executionNode: ReplExecutionEventNode): Unit = {
executionNodeStack.push(executionNode)
sendToClient(Debugger.createSessionStartNotification())
}
/**
* A helper function that cleans up the current session and terminates it.
*
* @return never returns as control is passed to the interpreter
*/
private def endSession(): Nothing = {
val node = executionNodeStack.pop()
node.exit()
throw new IllegalStateException(
"exit() on execution node returned unexpectedly"
)
}
private def onMessage(request: Request): Unit =
currentExecutionNode match {
case Some(node) =>
request match {
case EvaluationRequest(expression) =>
val result = node.evaluate(expression)
result match {
case Left(error) =>
sendToClient(Debugger.createEvaluationFailure(error))
case Right(value) =>
sendToClient(Debugger.createEvaluationSuccess(value))
}
case ListBindingsRequest =>
val bindings = node.listBindings()
sendToClient(Debugger.createListBindingsResult(bindings))
case SessionExitRequest =>
endSession()
}
case None =>
throw new IllegalStateException(
"Got a request but no session is running"
)
}
}

View File

@ -16,6 +16,7 @@ import org.enso.interpreter.instrument.{
}
import org.enso.interpreter.test.CodeIdsTestInstrument.IdEventListener
import org.enso.interpreter.test.CodeLocationsTestInstrument.LocationsEventListener
import org.enso.polyglot.debugger.DebugServerInfo
import org.enso.polyglot.{Function, LanguageInfo, PolyglotContext}
import org.graalvm.polyglot.{Context, Value}
import org.scalatest.Assertions
@ -155,9 +156,10 @@ trait InterpreterRunner {
inOutPrinter.println(string)
}
// TODO [RW] remove this for #791
def getReplInstrument: ReplDebuggerInstrument = {
ctx.getEngine.getInstruments
.get(ReplDebuggerInstrument.INSTRUMENT_ID)
.get(DebugServerInfo.INSTRUMENT_NAME)
.lookup(classOf[ReplDebuggerInstrument])
}

View File

@ -0,0 +1,125 @@
package org.enso.interpreter.test.instrument
import org.enso.interpreter.test.InterpreterTest
import scala.jdk.CollectionConverters._
@deprecated(
"These will be removed once the Repl in runner is migrated " +
"to server based solution"
)
class OldReplTest extends InterpreterTest {
"Repl" should "be able to list local variables in its scope" in {
val code =
"""
|main =
| x = 10
| y = 20
| z = x + y
|
| Debug.breakpoint
|""".stripMargin
var scopeResult: Map[String, AnyRef] = Map()
getReplInstrument.setSessionManager { executor =>
scopeResult = executor.listBindings.asScala.toMap
executor.exit()
}
eval(code)
scopeResult.view.mapValues(_.toString).toMap shouldEqual Map(
"this" -> "Test",
"x" -> "10",
"y" -> "20",
"z" -> "30"
)
}
"Repl" should "be able to list bindings it has created" in {
val code =
"""
|main =
| x = 10
| y = 20
| z = x + y
|
| Debug.breakpoint
|""".stripMargin
var scopeResult: Map[String, AnyRef] = Map()
getReplInstrument.setSessionManager { executor =>
executor.evaluate("x = y + z")
scopeResult = executor.listBindings.asScala.toMap
executor.exit()
}
eval(code)
scopeResult.view.mapValues(_.toString).toMap shouldEqual Map(
"this" -> "Test",
"x" -> "50",
"y" -> "20",
"z" -> "30"
)
}
"Repl" should "be able to execute arbitrary code in the caller scope" in {
val code =
"""
|main =
| x = 1
| y = 2
| Debug.breakpoint
|""".stripMargin
var evalResult: AnyRef = null
getReplInstrument.setSessionManager { executor =>
evalResult = executor.evaluate("x + y").fold(throw _, identity)
executor.exit()
}
eval(code)
evalResult shouldEqual 3
}
"Repl" should "return the last evaluated value back to normal execution flow" in {
val code =
"""
|main =
| a = 5
| b = 6
| c = Debug.breakpoint
| 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 =
"""
|main =
| x = 10
| Debug.breakpoint
|""".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 =
"""
|main =
| State.put 10
| Debug.breakpoint
| State.get
|""".stripMargin
getReplInstrument.setSessionManager { executor =>
executor.evaluate("x = State.get")
executor.evaluate("State.put (x + 1)")
executor.exit()
}
eval(code) shouldEqual 11
}
}

View File

@ -1,121 +1,255 @@
package org.enso.interpreter.test.instrument
import org.enso.interpreter.test.InterpreterTest
import org.enso.interpreter.test.{InterpreterRunner, ValueEquality}
import org.enso.polyglot.debugger.protocol.{
ExceptionRepresentation,
ObjectRepresentation
}
import org.enso.polyglot.debugger.{
DebugServerInfo,
DebuggerSessionManagerEndpoint,
ReplExecutor,
SessionManager
}
import org.graalvm.polyglot.Context
import org.graalvm.polyglot.io.MessageEndpoint
import org.enso.polyglot.{debugger, LanguageInfo, PolyglotContext}
import org.scalatest.BeforeAndAfter
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import scala.jdk.CollectionConverters._
trait ReplRunner extends InterpreterRunner {
var endPoint: MessageEndpoint = _
var messageQueue
: List[debugger.Response] = List() // TODO probably need a better message handler
class ReplTest extends InterpreterTest {
"Repl" should "be able to list local variables in its scope" in {
val code =
"""
|main =
| x = 10
| y = 20
| z = x + y
|
| Debug.breakpoint
|""".stripMargin
var scopeResult: Map[String, AnyRef] = Map()
getReplInstrument.setSessionManager { executor =>
scopeResult = executor.listBindings.asScala.toMap
executor.exit()
}
eval(code)
scopeResult.view.mapValues(_.toString).toMap shouldEqual Map(
"this" -> "Test",
"x" -> "10",
"y" -> "20",
"z" -> "30"
)
class ReplaceableSessionManager extends SessionManager {
var currentSessionManager: SessionManager = _
def setSessionManager(manager: SessionManager): Unit =
currentSessionManager = manager
override def startSession(executor: ReplExecutor): Nothing =
currentSessionManager.startSession(executor)
}
"Repl" should "be able to list bindings it has created" in {
val code =
"""
|main =
| x = 10
| y = 20
| z = x + y
|
| Debug.breakpoint
|""".stripMargin
var scopeResult: Map[String, AnyRef] = Map()
getReplInstrument.setSessionManager { executor =>
executor.evaluate("x = y + z")
scopeResult = executor.listBindings.asScala.toMap
executor.exit()
private val sessionManager = new ReplaceableSessionManager
override val ctx = Context
.newBuilder(LanguageInfo.ID)
.allowExperimentalOptions(true)
.allowAllAccess(true)
.option(DebugServerInfo.ENABLE_OPTION, "true")
.out(output)
.err(err)
.in(in)
.serverTransport { (uri, peer) =>
if (uri.toString == DebugServerInfo.URI) {
new DebuggerSessionManagerEndpoint(sessionManager, peer)
} else null
}
eval(code)
scopeResult.view.mapValues(_.toString).toMap shouldEqual Map(
"this" -> "Test",
"x" -> "50",
"y" -> "20",
"z" -> "30"
)
.build()
override lazy val executionContext = new PolyglotContext(ctx)
def setSessionManager(manager: SessionManager): Unit =
sessionManager.setSessionManager(manager)
}
class ReplTest
extends AnyWordSpec
with Matchers
with BeforeAndAfter
with ValueEquality
with ReplRunner {
after {
messageQueue = List()
}
"Repl" should "be able to execute arbitrary code in the caller scope" in {
val code =
"""
|main =
| x = 1
| y = 2
| Debug.breakpoint
|""".stripMargin
var evalResult: AnyRef = null
getReplInstrument.setSessionManager { executor =>
evalResult = executor.evaluate("x + y")
executor.exit()
"Repl" should {
"initialize properly" in {
val code =
"""
|main = Debug.breakpoint
|""".stripMargin
setSessionManager(executor => executor.exit())
eval(code)
}
eval(code)
evalResult shouldEqual 3
}
"Repl" should "return the last evaluated value back to normal execution flow" in {
val code =
"""
|main =
| a = 5
| b = 6
| c = Debug.breakpoint
| c * a
|""".stripMargin
getReplInstrument.setSessionManager { executor =>
executor.evaluate("a + b")
executor.exit()
"be able to execute arbitrary code in the caller scope" in {
val code =
"""
|main =
| x = 1
| y = 2
| Debug.breakpoint
|""".stripMargin
var evalResult: Either[ExceptionRepresentation, ObjectRepresentation] =
null
setSessionManager { executor =>
evalResult = executor.evaluate("x + y")
executor.exit()
}
eval(code) shouldEqual 3
evalResult.fold(_.toString, _.representation()) shouldEqual "3"
}
eval(code) shouldEqual 55
}
"Repl" should "be able to define its local variables" in {
val code =
"""
|main =
| x = 10
| Debug.breakpoint
|""".stripMargin
getReplInstrument.setSessionManager { executor =>
executor.evaluate("y = x + 1")
executor.evaluate("z = y * x")
executor.evaluate("z")
executor.exit()
"return the last evaluated value back to normal execution flow" in {
val code =
"""
|main =
| a = 5
| b = 6
| c = Debug.breakpoint
| c * a
|""".stripMargin
setSessionManager { executor =>
executor.evaluate("a + b")
executor.exit()
}
eval(code) shouldEqual 55
}
eval(code) shouldEqual 110
}
"Repl" should "access and modify monadic state" in {
val code =
"""
|main =
| State.put 10
| Debug.breakpoint
| State.get
|""".stripMargin
getReplInstrument.setSessionManager { executor =>
executor.evaluate("x = State.get")
executor.evaluate("State.put (x + 1)")
executor.exit()
"be able to define its local variables" in {
val code =
"""
|main =
| x = 10
| Debug.breakpoint
|""".stripMargin
setSessionManager { executor =>
executor.evaluate("y = x + 1")
executor.evaluate("z = y * x")
executor.evaluate("z")
executor.exit()
}
eval(code) shouldEqual 110
}
"not overwrite bindings" in {
val code =
"""
|main =
| x = 10
| Debug.breakpoint
| x
|""".stripMargin
setSessionManager { executor =>
executor.evaluate("x = 20")
executor.exit()
}
eval(code) shouldEqual 10
}
"access and modify monadic state" in {
val code =
"""
|main =
| State.put 10
| Debug.breakpoint
| State.get
|""".stripMargin
setSessionManager { executor =>
executor.evaluate("x = State.get")
executor.evaluate("State.put (x + 1)")
executor.exit()
}
eval(code) shouldEqual 11
}
"be able to list local variables in its scope" in {
val code =
"""
|main =
| x = 10
| y = 20
| z = x + y
|
| Debug.breakpoint
|""".stripMargin
var scopeResult: Map[String, ObjectRepresentation] = Map()
setSessionManager { executor =>
scopeResult = executor.listBindings()
executor.exit()
}
eval(code)
scopeResult.view.mapValues(_.representation()).toMap shouldEqual Map(
"this" -> "Test",
"x" -> "10",
"y" -> "20",
"z" -> "30"
)
}
"be able to list bindings it has created" in {
val code =
"""
|main =
| x = 10
| y = 20
| z = x + y
|
| Debug.breakpoint
|""".stripMargin
var scopeResult: Map[String, ObjectRepresentation] = Map()
setSessionManager { executor =>
executor.evaluate("x = y + z")
scopeResult = executor.listBindings()
executor.exit()
}
eval(code)
scopeResult.view.mapValues(_.representation()).toMap shouldEqual Map(
"this" -> "Test",
"x" -> "50",
"y" -> "20",
"z" -> "30"
)
}
"allow to be nested" in {
val code =
"""
|main =
| 10 * Debug.breakpoint + 1
|""".stripMargin
setSessionManager { topExecutor =>
setSessionManager { nestedExecutor =>
setSessionManager { doubleNestedExecutor =>
doubleNestedExecutor.evaluate("4")
doubleNestedExecutor.exit()
}
nestedExecutor.evaluate("10 * Debug.breakpoint + 3")
nestedExecutor.exit()
}
topExecutor.evaluate("10 * Debug.breakpoint + 2")
topExecutor.exit()
}
eval(code) shouldEqual 4321
}
"behave well when nested" in {
val code =
"""
|main =
| x = 1
| 10 * Debug.breakpoint + x
|""".stripMargin
setSessionManager { topExecutor =>
topExecutor.evaluate("x = 2")
setSessionManager { nestedExecutor =>
nestedExecutor.evaluate("x = 3")
setSessionManager { doubleNestedExecutor =>
doubleNestedExecutor.evaluate("x = 4")
doubleNestedExecutor.evaluate("x")
doubleNestedExecutor.exit()
}
nestedExecutor.evaluate("10 * Debug.breakpoint + x")
nestedExecutor.exit()
}
topExecutor.evaluate("10 * Debug.breakpoint + x")
topExecutor.exit()
}
eval(code) shouldEqual 4321
}
eval(code) shouldEqual 11
}
}