From af8b5f88cf7261b3110064a58246b7cae656f569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Wa=C5=9Bko?= Date: Tue, 9 Jun 2020 16:23:52 +0200 Subject: [PATCH] Implement debbuger server in the instrument (#822) --- docs/debugger/protocol.md | 18 +- .../polyglot/debugger/DebugServerInfo.java | 10 + .../org/enso/polyglot/debugger/Debugger.scala | 64 ++-- .../DebuggerSessionManagerEndpoint.scala | 145 ++++++++ .../DeserializationFailedException.scala | 4 + .../org/enso/polyglot/debugger/Response.scala | 5 - .../polyglot/debugger/SessionManager.scala | 66 ++++ .../protocol/factory/ResponseFactory.scala | 14 - .../src/main/schema/debugger/message.fbs | 1 - .../src/main/schema/debugger/session.fbs | 3 - .../enso/polyglot/debugger/EitherValue.scala | 24 ++ .../polyglot/debugger/SerializationTest.scala | 45 ++- .../org/enso/runner/ContextFactory.scala | 5 +- .../src/main/scala/org/enso/runner/Repl.scala | 8 +- .../instrument/ReplDebuggerInstrument.java | 86 ++++- .../instrument/DebuggerMessageHandler.scala | 107 ++++++ .../interpreter/test/InterpreterTest.scala | 4 +- .../test/instrument/OldReplTest.scala | 125 +++++++ .../test/instrument/ReplTest.scala | 340 ++++++++++++------ 19 files changed, 861 insertions(+), 213 deletions(-) create mode 100644 engine/polyglot-api/src/main/java/org/enso/polyglot/debugger/DebugServerInfo.java create mode 100644 engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/DebuggerSessionManagerEndpoint.scala create mode 100644 engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/DeserializationFailedException.scala create mode 100644 engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/SessionManager.scala create mode 100644 engine/polyglot-api/src/test/scala/org/enso/polyglot/debugger/EitherValue.scala create mode 100644 engine/runtime/src/main/scala/org/enso/interpreter/instrument/DebuggerMessageHandler.scala create mode 100644 engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/OldReplTest.scala diff --git a/docs/debugger/protocol.md b/docs/debugger/protocol.md index 3af9601743a..d2f99c372f3 100644 --- a/docs/debugger/protocol.md +++ b/docs/debugger/protocol.md @@ -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 {} -``` \ No newline at end of file diff --git a/engine/polyglot-api/src/main/java/org/enso/polyglot/debugger/DebugServerInfo.java b/engine/polyglot-api/src/main/java/org/enso/polyglot/debugger/DebugServerInfo.java new file mode 100644 index 00000000000..e590d11da8a --- /dev/null +++ b/engine/polyglot-api/src/main/java/org/enso/polyglot/debugger/DebugServerInfo.java @@ -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"; +} diff --git a/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/Debugger.scala b/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/Debugger.scala index 5f79c56a1b8..dc334f8a2a7 100644 --- a/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/Debugger.scala +++ b/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/Debugger.scala @@ -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. diff --git a/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/DebuggerSessionManagerEndpoint.scala b/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/DebuggerSessionManagerEndpoint.scala new file mode 100644 index 00000000000..8f9fd858c7d --- /dev/null +++ b/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/DebuggerSessionManagerEndpoint.scala @@ -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" + ) + } + } + } +} diff --git a/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/DeserializationFailedException.scala b/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/DeserializationFailedException.scala new file mode 100644 index 00000000000..b6ff0685244 --- /dev/null +++ b/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/DeserializationFailedException.scala @@ -0,0 +1,4 @@ +package org.enso.polyglot.debugger + +class DeserializationFailedException(message: String, cause: Throwable = null) + extends RuntimeException(message, cause) diff --git a/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/Response.scala b/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/Response.scala index 2248caac397..4d6c9012421 100644 --- a/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/Response.scala +++ b/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/Response.scala @@ -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 diff --git a/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/SessionManager.scala b/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/SessionManager.scala new file mode 100644 index 00000000000..90563068761 --- /dev/null +++ b/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/SessionManager.scala @@ -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 +} diff --git a/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/protocol/factory/ResponseFactory.scala b/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/protocol/factory/ResponseFactory.scala index ef1ea25cf3e..c5604e8ea9c 100644 --- a/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/protocol/factory/ResponseFactory.scala +++ b/engine/polyglot-api/src/main/scala/org/enso/polyglot/debugger/protocol/factory/ResponseFactory.scala @@ -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 = { diff --git a/engine/polyglot-api/src/main/schema/debugger/message.fbs b/engine/polyglot-api/src/main/schema/debugger/message.fbs index 030c4552cd3..e46ea44c57e 100644 --- a/engine/polyglot-api/src/main/schema/debugger/message.fbs +++ b/engine/polyglot-api/src/main/schema/debugger/message.fbs @@ -22,7 +22,6 @@ union ResponsePayload { EVALUATION_SUCCESS: EvaluationSuccess, EVALUATION_FAILURE: EvaluationFailure, LIST_BINDINGS: ListBindingsResult, - SESSION_EXIT: SessionExitSuccess, SESSION_START: SessionStartNotification } diff --git a/engine/polyglot-api/src/main/schema/debugger/session.fbs b/engine/polyglot-api/src/main/schema/debugger/session.fbs index 52c2708c100..43060a52941 100644 --- a/engine/polyglot-api/src/main/schema/debugger/session.fbs +++ b/engine/polyglot-api/src/main/schema/debugger/session.fbs @@ -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 {} diff --git a/engine/polyglot-api/src/test/scala/org/enso/polyglot/debugger/EitherValue.scala b/engine/polyglot-api/src/test/scala/org/enso/polyglot/debugger/EitherValue.scala new file mode 100644 index 00000000000..1bdd84b81c1 --- /dev/null +++ b/engine/polyglot-api/src/test/scala/org/enso/polyglot/debugger/EitherValue.scala @@ -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) + } + } +} diff --git a/engine/polyglot-api/src/test/scala/org/enso/polyglot/debugger/SerializationTest.scala b/engine/polyglot-api/src/test/scala/org/enso/polyglot/debugger/SerializationTest.scala index c18b05c0941..20d4458b36f 100644 --- a/engine/polyglot-api/src/test/scala/org/enso/polyglot/debugger/SerializationTest.scala +++ b/engine/polyglot-api/src/test/scala/org/enso/polyglot/debugger/SerializationTest.scala @@ -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 } } } diff --git a/engine/runner/src/main/scala/org/enso/runner/ContextFactory.scala b/engine/runner/src/main/scala/org/enso/runner/ContextFactory.scala index 6295f824507..43b60b89c1c 100644 --- a/engine/runner/src/main/scala/org/enso/runner/ContextFactory.scala +++ b/engine/runner/src/main/scala/org/enso/runner/ContextFactory.scala @@ -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) diff --git a/engine/runner/src/main/scala/org/enso/runner/Repl.scala b/engine/runner/src/main/scala/org/enso/runner/Repl.scala index 54fd3f37c38..95f7c05914f 100644 --- a/engine/runner/src/main/scala/org/enso/runner/Repl.scala +++ b/engine/runner/src/main/scala/org/enso/runner/Repl.scala @@ -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") + } } } } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/instrument/ReplDebuggerInstrument.java b/engine/runtime/src/main/java/org/enso/interpreter/instrument/ReplDebuggerInstrument.java index dfbdc5a809c..1b3f57140fd 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/instrument/ReplDebuggerInstrument.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/instrument/ReplDebuggerInstrument.java @@ -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 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); + } + } } } diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/DebuggerMessageHandler.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/DebuggerMessageHandler.scala new file mode 100644 index 00000000000..eaa7b274eb4 --- /dev/null +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/DebuggerMessageHandler.scala @@ -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" + ) + } +} diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/InterpreterTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/InterpreterTest.scala index 2b223e3d10a..209e8b71edb 100644 --- a/engine/runtime/src/test/scala/org/enso/interpreter/test/InterpreterTest.scala +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/InterpreterTest.scala @@ -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]) } diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/OldReplTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/OldReplTest.scala new file mode 100644 index 00000000000..a41f94ea01a --- /dev/null +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/OldReplTest.scala @@ -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 + } +} diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/ReplTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/ReplTest.scala index 83d0f515e1d..292fa850f19 100644 --- a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/ReplTest.scala +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/ReplTest.scala @@ -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 } }