mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 07:12:20 +03:00
Implement debbuger server in the instrument (#822)
This commit is contained in:
parent
a5f6d789b1
commit
af8b5f88cf
@ -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 {}
|
||||
```
|
@ -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";
|
||||
}
|
@ -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.
|
||||
|
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
package org.enso.polyglot.debugger
|
||||
|
||||
class DeserializationFailedException(message: String, cause: Throwable = null)
|
||||
extends RuntimeException(message, cause)
|
@ -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
|
||||
|
@ -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
|
||||
}
|
@ -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 = {
|
||||
|
@ -22,7 +22,6 @@ union ResponsePayload {
|
||||
EVALUATION_SUCCESS: EvaluationSuccess,
|
||||
EVALUATION_FAILURE: EvaluationFailure,
|
||||
LIST_BINDINGS: ListBindingsResult,
|
||||
SESSION_EXIT: SessionExitSuccess,
|
||||
SESSION_START: SessionStartNotification
|
||||
}
|
||||
|
||||
|
@ -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 {}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
@ -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])
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user