From 75f25b66dba752ddc39387ea5f5546d6aa4f852f Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Fri, 17 Apr 2020 19:31:12 +0300 Subject: [PATCH] Integrate the LS with context management (#657) --- .../specification/enso-protocol.md | 18 +- .../enso/languageserver/boot/MainModule.scala | 3 +- .../monitoring/PingHandler.scala | 15 +- .../runtime/ContextEventsListener.scala | 4 +- .../runtime/ContextRegistry.scala | 2 +- .../runtime/ContextRegistryProtocol.scala | 7 + .../languageserver/runtime/ExecutionApi.scala | 2 + .../runtime/RuntimeFailureMapper.scala | 4 + .../filemanager/WatcherAdapterSpec.scala | 14 +- .../websocket/ContextRegistryTest.scala | 54 +++- .../org/enso/polyglot/runtime/Runtime.scala | 52 +--- .../instrument/IdExecutionInstrument.java | 25 +- .../org/enso/interpreter/runtime/Context.java | 36 ++- .../enso/interpreter/runtime/type/Types.java | 30 +++ .../interpreter/service/ExecutionService.java | 12 +- .../instrument/ExecutionContext.scala | 9 + .../enso/interpeter/instrument/Handler.scala | 124 +++++++--- .../instrument/ContextManagementTest.scala | 138 ----------- .../test/instrument/RuntimeServerTest.scala | 232 +++++++++++++++--- 19 files changed, 507 insertions(+), 274 deletions(-) delete mode 100644 engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/ContextManagementTest.scala diff --git a/doc/language-server/specification/enso-protocol.md b/doc/language-server/specification/enso-protocol.md index e1ff0b6b762..9f03424cba0 100644 --- a/doc/language-server/specification/enso-protocol.md +++ b/doc/language-server/specification/enso-protocol.md @@ -1272,7 +1272,7 @@ be correlated between the textual and data connections. ``` ##### Errors -- [`SessionAlreadyInitialisedError`](#sessionalreadyinitialisederror) to signal +- [`SessionAlreadyInitialisedError`](#sessionalreadyinitialisederror) to signal that session is already initialised. #### `session/initDataConnection` @@ -2404,10 +2404,12 @@ null ``` ##### Errors -- [`StackItemNotFoundError`](#stackitemnotfounderror) when the request stack - item could not be found. - [`AccessDeniedError`](#accessdeniederror) when the user does not hold the `executionContext/canModify` capability for this context. +- [`StackItemNotFoundError`](#stackitemnotfounderror) when the request stack + item could not be found. +- [`InvalidStackItemError`](#invalidstackitemerror) when pushing `LocalCall` on + top of the empty stack, or pushing `ExplicitCall` on top of non-empty stack. #### `executionContext/pop` @@ -2708,6 +2710,16 @@ It signals that stack is empty. } ``` +##### `InvalidStackItemError` +It signals that stack is invalid in this context. + +```typescript +"error" : { + "code" : 2004, + "message" : "Invalid stack item" +} +``` + ##### `FileNotOpenedError` Signals that a file wasn't opened. diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala index 8b078f863db..d70c914fbee 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala @@ -25,7 +25,7 @@ import org.enso.languageserver.protocol.{JsonRpc, ServerClientControllerFactory} import org.enso.languageserver.runtime.{ContextRegistry, RuntimeConnector} import org.enso.languageserver.text.BufferRegistry import org.enso.languageserver.LanguageServer -import org.enso.polyglot.{LanguageInfo, RuntimeServerInfo} +import org.enso.polyglot.{LanguageInfo, RuntimeOptions, RuntimeServerInfo} import org.graalvm.polyglot.Context import org.graalvm.polyglot.io.MessageEndpoint @@ -103,6 +103,7 @@ class MainModule(serverConfig: LanguageServerConfig) { .allowAllAccess(true) .allowExperimentalOptions(true) .option(RuntimeServerInfo.ENABLE_OPTION, "true") + .option(RuntimeOptions.getPackagesPathOption, serverConfig.contentRootPath) .serverTransport((uri: URI, peerEndpoint: MessageEndpoint) => { if (uri.toString == RuntimeServerInfo.URI) { val connection = new RuntimeConnector.Endpoint( diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/monitoring/PingHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/monitoring/PingHandler.scala index a6a4687ff3c..351d7ffeb03 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/monitoring/PingHandler.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/monitoring/PingHandler.scala @@ -22,20 +22,22 @@ class PingHandler( import context.dispatcher + private var cancellable: Option[Cancellable] = None + override def receive: Receive = scatter private def scatter: Receive = { case Request(MonitoringApi.Ping, id, Unused) => subsystems.foreach(_ ! Ping) - val cancellable = + cancellable = Some( context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout) - context.become(gather(id, sender(), cancellable)) + ) + context.become(gather(id, sender())) } private def gather( id: Id, replyTo: ActorRef, - cancellable: Cancellable, count: Int = 0 ): Receive = { case RequestTimeout => @@ -47,13 +49,16 @@ class PingHandler( case Pong => if (count + 1 == subsystems.size) { replyTo ! ResponseResult(MonitoringApi.Ping, id, Unused) - cancellable.cancel() context.stop(self) } else { - context.become(gather(id, replyTo, cancellable, count + 1)) + context.become(gather(id, replyTo, count + 1)) } } + override def postStop(): Unit = { + cancellable.foreach(_.cancel()) + } + } object PingHandler { diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala index 52174e1de8b..d9ffe994790 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala @@ -43,6 +43,8 @@ final class ContextEventsListener( contextId, updates ) + case _: Api.ExpressionValuesComputed => + // ignore updates from other contexts } private def toRuntimeUpdate( @@ -73,7 +75,7 @@ final class ContextEventsListener( private def toRuntimePointer( pointer: Api.MethodPointer ): Option[MethodPointer] = - config.findRelativePath(pointer.file.toFile).map { relativePath => + config.findRelativePath(pointer.file).map { relativePath => MethodPointer( file = relativePath, definedOnType = pointer.definedOnType, diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala index 726e13d54a1..3d3d130c93a 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala @@ -125,7 +125,7 @@ final class ContextRegistry(config: Config, runtime: ActorRef) ): Either[FileSystemFailure, Api.MethodPointer] = config.findContentRoot(pointer.file.rootId).map { rootPath => Api.MethodPointer( - file = pointer.file.toFile(rootPath).toPath, + file = pointer.file.toFile(rootPath), definedOnType = pointer.definedOnType, name = pointer.name ) diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala index ecf01c295d0..5de039b7865 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala @@ -112,4 +112,11 @@ object ContextRegistryProtocol { * @param contextId execution context identifier */ case class EmptyStackError(contextId: ContextId) extends Failure + + /** + * Signals that stack item is invalid in this context. + * + * @param contextId execution context identifier + */ + case class InvalidStackItemError(contextId: ContextId) extends Failure } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ExecutionApi.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ExecutionApi.scala index 2ee7a47ddc5..a2c6da152f5 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ExecutionApi.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ExecutionApi.scala @@ -85,4 +85,6 @@ object ExecutionApi { case object EmptyStackError extends Error(2003, "Stack is empty") + case object InvalidStackItemError extends Error(2004, "Invalid stack item") + } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/RuntimeFailureMapper.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/RuntimeFailureMapper.scala index eefb6a3b830..2ec90366012 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/RuntimeFailureMapper.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/RuntimeFailureMapper.scala @@ -24,6 +24,8 @@ object RuntimeFailureMapper { FileSystemFailureMapper.mapFailure(error) case ContextRegistryProtocol.EmptyStackError(_) => EmptyStackError + case ContextRegistryProtocol.InvalidStackItemError(_) => + InvalidStackItemError } /** @@ -38,6 +40,8 @@ object RuntimeFailureMapper { ContextRegistryProtocol.ContextNotFound(contextId) case Api.EmptyStackError(contextId) => ContextRegistryProtocol.EmptyStackError(contextId) + case Api.InvalidStackItemError(contextId) => + ContextRegistryProtocol.InvalidStackItemError(contextId) } } diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/filemanager/WatcherAdapterSpec.scala b/engine/language-server/src/test/scala/org/enso/languageserver/filemanager/WatcherAdapterSpec.scala index d09cc2c748b..99a4a0f4f03 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/filemanager/WatcherAdapterSpec.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/filemanager/WatcherAdapterSpec.scala @@ -1,7 +1,7 @@ package org.enso.languageserver.filemanager import java.nio.file.{Files, Path, Paths} -import java.util.concurrent.{Executors, LinkedBlockingQueue} +import java.util.concurrent.{Executors, LinkedBlockingQueue, Semaphore} import org.apache.commons.io.FileUtils import org.enso.languageserver.effect.Effects @@ -65,17 +65,23 @@ class WatcherAdapterSpec extends AnyFlatSpec with Matchers with Effects { def withWatcher( test: (Path, LinkedBlockingQueue[WatcherEvent]) => Any ): Any = { + val lock = new Semaphore(0) val executor = Executors.newSingleThreadExecutor() val tmp = Files.createTempDirectory(null).toRealPath() val queue = new LinkedBlockingQueue[WatcherEvent]() val watcher = WatcherAdapter.build(tmp, queue.put(_), println(_)) executor.submit(new Runnable { - def run() = watcher.start().unsafeRunSync(): Unit + def run(): Unit = { + lock.release() + watcher.start().unsafeRunSync(): Unit + } }) - try test(tmp, queue) - finally { + try { + lock.tryAcquire(Timeout.length, Timeout.unit) + test(tmp, queue) + } finally { watcher.stop().unsafeRunSync() executor.shutdown() Try(executor.awaitTermination(Timeout.length, Timeout.unit)) diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/ContextRegistryTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/ContextRegistryTest.scala index 4c8fe4a1796..3df5e3a8cba 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/ContextRegistryTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/ContextRegistryTest.scala @@ -1,6 +1,6 @@ package org.enso.languageserver.websocket -import java.nio.file.Paths +import java.io.File import java.util.UUID import io.circe.literal._ @@ -252,6 +252,54 @@ class ContextRegistryTest extends BaseServerTest { """) } + "return InvalidStackItemError when pushing invalid item to stack" in { + val client = getInitialisedWsClient() + // create context + client.send(json.executionContextCreateRequest(1)) + val (requestId1, contextId) = + runtimeConnectorProbe.receiveN(1).head match { + case Api.Request(requestId, Api.CreateContextRequest(contextId)) => + (requestId, contextId) + case msg => + fail(s"Unexpected message: $msg") + } + runtimeConnectorProbe.lastSender ! Api.Response( + requestId1, + Api.CreateContextResponse(contextId) + ) + client.expectJson(json.executionContextCreateResponse(1, contextId)) + + // push invalid item + val expressionId = UUID.randomUUID() + client.send(json.executionContextPushRequest(2, contextId, expressionId)) + val requestId2 = + runtimeConnectorProbe.receiveN(1).head match { + case Api.Request( + requestId, + Api.PushContextRequest( + `contextId`, + Api.StackItem.LocalCall(`expressionId`) + ) + ) => + requestId + case msg => + fail(s"Unexpected message: $msg") + } + runtimeConnectorProbe.lastSender ! Api.Response( + requestId2, + Api.InvalidStackItemError(contextId) + ) + client.expectJson(json""" + { "jsonrpc": "2.0", + "id" : 2, + "error" : { + "code" : 2004, + "message" : "Invalid stack item" + } + } + """) + } + "send notifications" in { val client = getInitialisedWsClient() @@ -277,7 +325,7 @@ class ContextRegistryTest extends BaseServerTest { shortValue = Some("ShortValue"), methodCall = Some( Api.MethodPointer( - file = testContentRoot, + file = testContentRoot.toFile, definedOnType = "DefinedOnType", name = "Name" ) @@ -288,7 +336,7 @@ class ContextRegistryTest extends BaseServerTest { expressionType = None, shortValue = None, methodCall = Some( - Api.MethodPointer(Paths.get("/invalid"), "Invalid", "Invalid") + Api.MethodPointer(new File("/invalid"), "Invalid", "Invalid") ) ) system.eventStream.publish( diff --git a/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala b/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala index 15fa7ac1fca..00d20efc898 100644 --- a/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala +++ b/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala @@ -1,7 +1,7 @@ package org.enso.polyglot.runtime +import java.io.File import java.nio.ByteBuffer -import java.nio.file.Path import java.util.UUID import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo} @@ -66,14 +66,13 @@ object Runtime { value = classOf[Api.EmptyStackError], name = "emptyStackError" ), - new JsonSubTypes.Type(value = classOf[Api.Execute], name = "execute"), + new JsonSubTypes.Type( + value = classOf[Api.InvalidStackItemError], + name = "invalidStackItemError" + ), new JsonSubTypes.Type( value = classOf[Api.InitializedNotification], name = "initializedNotification" - ), - new JsonSubTypes.Type( - value = classOf[Api.ExpressionValueUpdateNotification], - name = "expressionValueUpdateNotification" ) ) ) @@ -96,7 +95,7 @@ object Runtime { /** * A representation of a pointer to a method definition. */ - case class MethodPointer(file: Path, definedOnType: String, name: String) + case class MethodPointer(file: File, definedOnType: String, name: String) /** * A representation of an executable position in code. @@ -274,6 +273,13 @@ object Runtime { */ case class EmptyStackError(contextId: ContextId) extends Error + /** + * An error response signifying that stack item is invalid. + * + * @param contextId the context's id + */ + case class InvalidStackItemError(contextId: ContextId) extends Error + /** * Notification sent from the server to the client upon successful * initialization. Any messages sent to the server before receiving this @@ -281,38 +287,6 @@ object Runtime { */ case class InitializedNotification() extends ApiResponse - /** - * An execution request for a given method. - * Note that this is a temporary message, only used to test functionality. - * To be replaced with actual execution stack API. - * - * @param modName the module to look for the method. - * @param consName the constructor the method is defined on. - * @param funName the method name. - * @param enterExprs the expressions that should be "entered" after - * executing the base method. - */ - case class Execute( - modName: String, - consName: String, - funName: String, - enterExprs: List[ExpressionId] - ) extends ApiRequest - - /** - * A notification sent from the server whenever an expression value - * becomes available. - * Note this is a temporary message, only used to test functionality. - * To be replaced with actual value computed notifications. - * - * @param expressionId the id of computed expression. - * @param shortValue the string representation of the expression's value. - */ - case class ExpressionValueUpdateNotification( - expressionId: ExpressionId, - shortValue: String - ) extends ApiResponse - private lazy val mapper = { val factory = new CBORFactory() val mapper = new ObjectMapper(factory) with ScalaObjectMapper diff --git a/engine/runtime/src/main/java/org/enso/interpreter/instrument/IdExecutionInstrument.java b/engine/runtime/src/main/java/org/enso/interpreter/instrument/IdExecutionInstrument.java index f935075982c..1d6fb0bfe78 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/instrument/IdExecutionInstrument.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/instrument/IdExecutionInstrument.java @@ -1,6 +1,7 @@ package org.enso.interpreter.instrument; import com.oracle.truffle.api.CallTarget; +import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.Truffle; import com.oracle.truffle.api.frame.FrameInstance; import com.oracle.truffle.api.frame.FrameInstanceVisitor; @@ -10,7 +11,9 @@ import com.oracle.truffle.api.nodes.Node; import org.enso.interpreter.node.ExpressionNode; import org.enso.interpreter.node.callable.FunctionCallInstrumentationNode; import org.enso.interpreter.runtime.tag.IdentifiedTag; +import org.enso.interpreter.runtime.type.Types; +import java.util.Optional; import java.util.UUID; import java.util.function.Consumer; @@ -34,7 +37,7 @@ public class IdExecutionInstrument extends TruffleInstrument { this.env = env; } - /** A value class for notifications about functions being called in the course of execution. */ + /** A class for notifications about functions being called in the course of execution. */ public static class ExpressionCall { private UUID expressionId; private FunctionCallInstrumentationNode.FunctionCall call; @@ -61,19 +64,22 @@ public class IdExecutionInstrument extends TruffleInstrument { } } - /** A value class for notifications about identified expressions' values being computed. */ + /** A class for notifications about identified expressions' values being computed. */ public static class ExpressionValue { - private UUID expressionId; - private Object value; + private final UUID expressionId; + private final String type; + private final Object value; /** * Creates a new instance of this class. * * @param expressionId the id of the expression being computed. + * @param type of the computed expression. * @param value the value returned by computing the expression. */ - public ExpressionValue(UUID expressionId, Object value) { + public ExpressionValue(UUID expressionId, String type, Object value) { this.expressionId = expressionId; + this.type = type; this.value = value; } @@ -82,6 +88,12 @@ public class IdExecutionInstrument extends TruffleInstrument { return expressionId; } + /** @return the computed type of the expression. */ + @CompilerDirectives.TruffleBoundary + public Optional getType() { + return Optional.ofNullable(type); + } + /** @return the computed value of the expression. */ public Object getValue() { return value; @@ -166,7 +178,8 @@ public class IdExecutionInstrument extends TruffleInstrument { (FunctionCallInstrumentationNode.FunctionCall) result)); } else if (node instanceof ExpressionNode) { valueCallback.accept( - new ExpressionValue(((ExpressionNode) context.getInstrumentedNode()).getId(), result)); + new ExpressionValue( + ((ExpressionNode) node).getId(), Types.getName(result).orElse(null), result)); } } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/Context.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/Context.java index 84ea74980e0..0765be073cd 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/Context.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/Context.java @@ -17,6 +17,7 @@ import org.enso.interpreter.runtime.scope.ModuleScope; import org.enso.interpreter.runtime.scope.TopLevelScope; import org.enso.interpreter.util.ScalaConversions; import org.enso.pkg.Package; +import org.enso.pkg.QualifiedName; /** * The language context is the internal state of the language that is associated with each thread in @@ -28,6 +29,7 @@ public class Context { private final Env environment; private final Compiler compiler; private final PrintStream out; + private List packages; /** * Creates a new Enso context. @@ -41,12 +43,17 @@ public class Context { this.out = new PrintStream(environment.out()); List packagePaths = OptionsHelper.getPackagesPaths(environment); - Map knownFiles = + + packages = packagePaths.stream() .map(Package::fromDirectory) .map(ScalaConversions::asJava) .filter(Optional::isPresent) .map(Optional::get) + .collect(Collectors.toList()); + + Map knownFiles = + packages.stream() .flatMap(p -> ScalaConversions.asJava(p.listSources()).stream()) .collect( Collectors.toMap( @@ -124,6 +131,33 @@ public class Context { initializeScope(scope); } + /** + * Guess module name from the file path by comparing it with the source pathes + * of imported packages. + * + * @param path file path. + * @return qualified module name if the function can find imported package + * with matching path. + */ + public Optional getModuleNameForFile(File path) { + return packages.stream() + .filter(pkg -> path.getAbsolutePath().startsWith(pkg.sourceDir().getAbsolutePath())) + .map(pkg -> pkg.moduleNameForFile(path)) + .findFirst(); + } + + /** + * Get module from the file path. Function tries to recover module name from + * the provided file path. + * + * @param path file path. + * @return module if module name can be guessed from the provided file path. + */ + public Optional getModuleForFile(File path) { + return getModuleNameForFile(path) + .flatMap(n -> compiler().topScope().getModule(n.toString())); + } + private void initializeScope(ModuleScope scope) { scope.addImport(getBuiltins().getScope()); } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/type/Types.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/type/Types.java index ab4b5e4aa98..cf714fa33f8 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/type/Types.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/type/Types.java @@ -11,6 +11,8 @@ import org.enso.interpreter.runtime.callable.atom.AtomConstructor; import org.enso.interpreter.runtime.callable.function.Function; import org.enso.interpreter.runtime.error.RuntimeError; +import java.util.Optional; + /** * This class defines the interpreter-level type system for Enso. * @@ -23,6 +25,7 @@ import org.enso.interpreter.runtime.error.RuntimeError; */ @TypeSystem({ long.class, + String.class, Function.class, Atom.class, AtomConstructor.class, @@ -89,6 +92,33 @@ public class Types { } } + /** + * Return a type of the given object as a string. + * + * @param value an object of interest. + * @return the string representation of object's type. + */ + public static Optional getName(Object value) { + if (TypesGen.isLong(value)) { + return Optional.of("Number"); + } else if (TypesGen.isString(value)) { + return Optional.of("Text"); + } else if (TypesGen.isFunction(value)) { + return Optional.of("Function"); + } else if (TypesGen.isAtom(value)) { + return Optional.of(TypesGen.asAtom(value).getConstructor().getName()); + } else if (TypesGen.isAtomConstructor(value)) { + return Optional.of(TypesGen.asAtomConstructor(value).getName()); + } else if (TypesGen.isThunk(value)) { + return Optional.of("Thunk"); + } else if (TypesGen.isRuntimeError(value)) { + return Optional + .of("Error " + TypesGen.asRuntimeError(value).getPayload().toString()); + } else { + return Optional.empty(); + } + } + /** * Asserts that the arguments array has exactly one element of a given type and extracts it. * diff --git a/engine/runtime/src/main/java/org/enso/interpreter/service/ExecutionService.java b/engine/runtime/src/main/java/org/enso/interpreter/service/ExecutionService.java index fbf199e83b2..c68d2c1555d 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/service/ExecutionService.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/service/ExecutionService.java @@ -14,7 +14,9 @@ import org.enso.interpreter.runtime.Module; import org.enso.interpreter.runtime.callable.atom.AtomConstructor; import org.enso.interpreter.runtime.callable.function.Function; import org.enso.interpreter.runtime.scope.ModuleScope; +import org.enso.pkg.QualifiedName; +import java.io.File; import java.util.Optional; import java.util.function.Consumer; @@ -93,24 +95,26 @@ public class ExecutionService { * Executes a method described by its name, constructor it's defined on and the module it's * defined in. * - * @param moduleName the qualified name of the module the method is defined in. + * @param modulePath the path to the module where the method is defined. * @param consName the name of the constructor the method is defined on. * @param methodName the method name. * @param valueCallback the consumer for expression value events. * @param funCallCallback the consumer for function call events. */ public void execute( - String moduleName, + File modulePath, String consName, String methodName, Consumer valueCallback, Consumer funCallCallback) throws UnsupportedMessageException, ArityException, UnsupportedTypeException { - Optional callMay = - prepareFunctionCall(moduleName, consName, methodName); + Optional callMay = context + .getModuleNameForFile(modulePath) + .flatMap(moduleName -> prepareFunctionCall(moduleName.toString(), consName, methodName)); if (!callMay.isPresent()) { return; } execute(callMay.get(), valueCallback, funCallCallback); } + } diff --git a/engine/runtime/src/main/scala/org/enso/interpeter/instrument/ExecutionContext.scala b/engine/runtime/src/main/scala/org/enso/interpeter/instrument/ExecutionContext.scala index 577ffa4fbba..78dad311105 100644 --- a/engine/runtime/src/main/scala/org/enso/interpeter/instrument/ExecutionContext.scala +++ b/engine/runtime/src/main/scala/org/enso/interpeter/instrument/ExecutionContext.scala @@ -43,6 +43,15 @@ class ExecutionContextManager { _ <- contexts.get(ExecutionContext(id)) } yield ExecutionContext(id) + /** + * Gets a stack for a given context id. + * + * @param id the context id. + * @return the stack. + */ + def getStack(id: ContextId): Stack[StackItem] = + contexts.getOrElse(ExecutionContext(id), Stack()) + /** * If the context exists, push the item on the stack. * diff --git a/engine/runtime/src/main/scala/org/enso/interpeter/instrument/Handler.scala b/engine/runtime/src/main/scala/org/enso/interpeter/instrument/Handler.scala index 51dff413149..90cc2cabbc5 100644 --- a/engine/runtime/src/main/scala/org/enso/interpeter/instrument/Handler.scala +++ b/engine/runtime/src/main/scala/org/enso/interpeter/instrument/Handler.scala @@ -1,5 +1,6 @@ package org.enso.interpeter.instrument +import java.io.File import java.nio.ByteBuffer import java.util.UUID import java.util.function.Consumer @@ -14,6 +15,8 @@ import org.enso.interpreter.service.ExecutionService import org.enso.polyglot.runtime.Runtime.Api import org.graalvm.polyglot.io.MessageEndpoint +import scala.jdk.javaapi.OptionConverters + /** * A message endpoint implementation used by the * [[org.enso.interpreter.instrument.RuntimeServerInstrument]]. @@ -53,7 +56,7 @@ class Endpoint(handler: Handler) extends MessageEndpoint { * A message handler, dispatching behaviors based on messages received * from an instance of [[Endpoint]]. */ -class Handler { +final class Handler { val endpoint = new Endpoint(this) val contextManager = new ExecutionContextManager @@ -80,37 +83,50 @@ class Handler { private object ExecutionItem { case class Method( - module: String, + file: File, constructor: String, function: String ) extends ExecutionItem case class CallData(callData: FunctionCall) extends ExecutionItem } - private def sendVal(res: ExpressionValue): Unit = { + + private def sendUpdate( + contextId: Api.ContextId, + res: ExpressionValue + ): Unit = { endpoint.sendToClient( Api.Response( - Api.ExpressionValueUpdateNotification( - res.getExpressionId, - res.getValue.toString + Api.ExpressionValuesComputed( + contextId, + Vector( + Api.ExpressionValueUpdate( + res.getExpressionId, + OptionConverters.toScala(res.getType), + Some(res.getValue.toString), + None + ) + ) ) ) ) } + @scala.annotation.tailrec private def execute( executionItem: ExecutionItem, - furtherStack: List[UUID] + callStack: List[UUID], + valueCallback: Consumer[ExpressionValue] ): Unit = { var enterables: Map[UUID, FunctionCall] = Map() val valsCallback: Consumer[ExpressionValue] = - if (furtherStack.isEmpty) sendVal else _ => () + if (callStack.isEmpty) valueCallback else _ => () val callablesCallback: Consumer[ExpressionCall] = fun => enterables += fun.getExpressionId -> fun.getCall executionItem match { - case ExecutionItem.Method(module, cons, function) => + case ExecutionItem.Method(file, cons, function) => executionService.execute( - module, + file, cons, function, valsCallback, @@ -120,15 +136,49 @@ class Handler { executionService.execute(callData, valsCallback, callablesCallback) } - furtherStack match { + callStack match { case Nil => () case item :: tail => - enterables - .get(item) - .foreach(call => execute(ExecutionItem.CallData(call), tail)) + enterables.get(item) match { + case Some(call) => + execute(ExecutionItem.CallData(call), tail, valueCallback) + case None => + () + } } } + private def execute( + contextId: Api.ContextId, + stack: List[Api.StackItem] + ): Unit = { + def unwind( + stack: List[Api.StackItem], + explicitCalls: List[Api.StackItem.ExplicitCall], + localCalls: List[UUID] + ): (List[Api.StackItem.ExplicitCall], List[UUID]) = + stack match { + case Nil => + (explicitCalls, localCalls) + case List(call: Api.StackItem.ExplicitCall) => + (List(call), localCalls) + case Api.StackItem.LocalCall(id) :: xs => + unwind(xs, explicitCalls, id :: localCalls) + } + val (explicitCalls, localCalls) = unwind(stack, Nil, Nil) + val item = toExecutionItem(explicitCalls.head) + execute(item, localCalls, sendUpdate(contextId, _)) + } + + private def toExecutionItem( + call: Api.StackItem.ExplicitCall + ): ExecutionItem = + ExecutionItem.Method( + call.methodPointer.file, + call.methodPointer.definedOnType, + call.methodPointer.name + ) + private def withContext(action: => Unit): Unit = { val token = truffleContext.enter() try { @@ -163,18 +213,19 @@ class Handler { } case Api.Request(requestId, Api.PushContextRequest(contextId, item)) => { - val payload = contextManager.push(contextId, item) match { - case Some(()) => Api.PushContextResponse(contextId) - case None => Api.ContextNotExistError(contextId) - } - endpoint.sendToClient(Api.Response(requestId, payload)) - } - - case Api.Request(requestId, Api.PopContextRequest(contextId)) => if (contextManager.get(contextId).isDefined) { - val payload = contextManager.pop(contextId) match { - case Some(_) => Api.PopContextResponse(contextId) - case None => Api.EmptyStackError(contextId) + val stack = contextManager.getStack(contextId) + val payload = item match { + case call: Api.StackItem.ExplicitCall if stack.isEmpty => + contextManager.push(contextId, item) + withContext(execute(contextId, List(call))) + Api.PushContextResponse(contextId) + case _: Api.StackItem.LocalCall if stack.nonEmpty => + contextManager.push(contextId, item) + withContext(execute(contextId, stack.toList)) + Api.PushContextResponse(contextId) + case _ => + Api.InvalidStackItemError(contextId) } endpoint.sendToClient(Api.Response(requestId, payload)) } else { @@ -182,9 +233,26 @@ class Handler { Api.Response(requestId, Api.ContextNotExistError(contextId)) ) } + } - case Api.Request(_, Api.Execute(mod, cons, fun, furtherStack)) => - withContext(execute(ExecutionItem.Method(mod, cons, fun), furtherStack)) - + case Api.Request(requestId, Api.PopContextRequest(contextId)) => + if (contextManager.get(contextId).isDefined) { + val payload = contextManager.pop(contextId) match { + case Some(_: Api.StackItem.ExplicitCall) => + Api.PopContextResponse(contextId) + case Some(_: Api.StackItem.LocalCall) => + withContext( + execute(contextId, contextManager.getStack(contextId).toList) + ) + Api.PopContextResponse(contextId) + case None => + Api.EmptyStackError(contextId) + } + endpoint.sendToClient(Api.Response(requestId, payload)) + } else { + endpoint.sendToClient( + Api.Response(requestId, Api.ContextNotExistError(contextId)) + ) + } } } diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/ContextManagementTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/ContextManagementTest.scala deleted file mode 100644 index 2ba3698fbcb..00000000000 --- a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/ContextManagementTest.scala +++ /dev/null @@ -1,138 +0,0 @@ -package org.enso.interpreter.test.instrument - -import java.nio.ByteBuffer -import java.util.UUID - -import org.enso.polyglot.runtime.Runtime.Api -import org.enso.polyglot.{LanguageInfo, RuntimeServerInfo} -import org.graalvm.polyglot.Context -import org.graalvm.polyglot.io.MessageEndpoint -import org.scalatest.BeforeAndAfterEach -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class ContextManagementTest - extends AnyFlatSpec - with Matchers - with BeforeAndAfterEach { - - var context: Context = _ - var messageQueue: List[Api.Response] = _ - var endPoint: MessageEndpoint = _ - - override protected def beforeEach(): Unit = { - messageQueue = List() - context = Context - .newBuilder(LanguageInfo.ID) - .allowExperimentalOptions(true) - .option(RuntimeServerInfo.ENABLE_OPTION, "true") - .serverTransport { (uri, peer) => - if (uri.toString == RuntimeServerInfo.URI) { - endPoint = peer - new MessageEndpoint { - override def sendText(text: String): Unit = {} - - override def sendBinary(data: ByteBuffer): Unit = - messageQueue ++= Api.deserializeResponse(data) - - override def sendPing(data: ByteBuffer): Unit = {} - - override def sendPong(data: ByteBuffer): Unit = {} - - override def sendClose(): Unit = {} - } - } else null - } - .build() - } - - def send(msg: Api.Request): Unit = endPoint.sendBinary(Api.serialize(msg)) - def receive: Option[Api.Response] = { - val msg = messageQueue.headOption - messageQueue = messageQueue.drop(1) - msg - } - - "Runtime server" should "allow context creation and deletion" in { - val requestId1 = UUID.randomUUID() - val requestId2 = UUID.randomUUID() - val contextId = UUID.randomUUID() - send(Api.Request(requestId1, Api.CreateContextRequest(contextId))) - receive shouldEqual Some( - Api.Response(requestId1, Api.CreateContextResponse(contextId)) - ) - send(Api.Request(requestId2, Api.DestroyContextRequest(contextId))) - receive shouldEqual Some( - Api.Response(requestId2, Api.DestroyContextResponse(contextId)) - ) - } - - "Runtime server" should "fail destroying a context if it does not exist" in { - val requestId1 = UUID.randomUUID() - val contextId1 = UUID.randomUUID() - val requestId2 = UUID.randomUUID() - val contextId2 = UUID.randomUUID() - send(Api.Request(requestId1, Api.CreateContextRequest(contextId1))) - receive shouldEqual Some( - Api.Response(requestId1, Api.CreateContextResponse(contextId1)) - ) - send(Api.Request(requestId2, Api.DestroyContextRequest(contextId2))) - receive shouldEqual Some( - Api.Response(requestId2, Api.ContextNotExistError(contextId2)) - ) - } - - "Runtime server" should "push and pop the context stack" in { - val contextId = UUID.randomUUID() - val expressionId = UUID.randomUUID() - val requestId1 = UUID.randomUUID() - val requestId2 = UUID.randomUUID() - val requestId3 = UUID.randomUUID() - send(Api.Request(requestId1, Api.CreateContextRequest(contextId))) - receive shouldEqual Some( - Api.Response(requestId1, Api.CreateContextResponse(contextId)) - ) - send( - Api.Request( - requestId2, - Api.PushContextRequest(contextId, Api.StackItem.LocalCall(expressionId)) - ) - ) - receive shouldEqual Some( - Api.Response(requestId2, Api.PushContextResponse(contextId)) - ) - send(Api.Request(requestId3, Api.PopContextRequest(contextId))) - receive shouldEqual Some( - Api.Response(requestId3, Api.PopContextResponse(contextId)) - ) - } - - "Runtime server" should "fail pushing context stack if it doesn't exist" in { - val contextId = UUID.randomUUID() - val expressionId = UUID.randomUUID() - val requestId = UUID.randomUUID() - send( - Api.Request( - requestId, - Api.PushContextRequest(contextId, Api.StackItem.LocalCall(expressionId)) - ) - ) - receive shouldEqual Some( - Api.Response(requestId, Api.ContextNotExistError(contextId)) - ) - } - - "Runtime server" should "fail popping empty stack" in { - val contextId = UUID.randomUUID() - val requestId1 = UUID.randomUUID() - val requestId2 = UUID.randomUUID() - send(Api.Request(requestId1, Api.CreateContextRequest(contextId))) - receive shouldEqual Some( - Api.Response(requestId1, Api.CreateContextResponse(contextId)) - ) - send(Api.Request(requestId2, Api.PopContextRequest(contextId))) - receive shouldEqual Some( - Api.Response(requestId2, Api.EmptyStackError(contextId)) - ) - } -} diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala index 1d19994a436..697f55affbc 100644 --- a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala @@ -60,14 +60,8 @@ class RuntimeServerTest ) executionContext.context.initialize(LanguageInfo.ID) - def mkFile(name: String): File = new File(tmpDir, name) - - def writeFile(name: String, contents: String): Unit = { - Files.write(mkFile(name).toPath, contents.getBytes): Unit - } - - def writeMain(contents: String): Unit = { - Files.write(pkg.mainFile.toPath, contents.getBytes): Unit + def writeMain(contents: String): File = { + Files.write(pkg.mainFile.toPath, contents.getBytes).toFile } def send(msg: Api.Request): Unit = endPoint.sendBinary(Api.serialize(msg)) @@ -79,49 +73,207 @@ class RuntimeServerTest } } + object Program { + + val metadata = new Metadata + + val idMainX = metadata.addItem(16, 5) + val idMainY = metadata.addItem(30, 7) + val idMainZ = metadata.addItem(46, 5) + val idFooY = metadata.addItem(85, 8) + val idFooZ = metadata.addItem(102, 5) + + val text = + """ + |main = + | x = 1 + 5 + | y = x.foo 5 + | z = y + 5 + | z + | + |Number.foo = x -> + | y = this + 3 + | z = y * x + | z + |""".stripMargin + + val code = metadata.appendToCode(text) + + object update { + + def idMainX(contextId: UUID) = + Api.Response( + Api.ExpressionValuesComputed( + contextId, + Vector( + Api.ExpressionValueUpdate( + Program.idMainX, + Some("Number"), + Some("6"), + None + ) + ) + ) + ) + + def idMainY(contextId: UUID) = + Api.Response( + Api.ExpressionValuesComputed( + contextId, + Vector( + Api.ExpressionValueUpdate( + Program.idMainY, + Some("Number"), + Some("45"), + None + ) + ) + ) + ) + + def idMainZ(contextId: UUID) = + Api.Response( + Api.ExpressionValuesComputed( + contextId, + Vector( + Api.ExpressionValueUpdate( + Program.idMainZ, + Some("Number"), + Some("50"), + None + ) + ) + ) + ) + + def idFooY(contextId: UUID) = + Api.Response( + Api.ExpressionValuesComputed( + contextId, + Vector( + Api.ExpressionValueUpdate( + Program.idFooY, + Some("Number"), + Some("9"), + None + ) + ) + ) + ) + + def idFooZ(contextId: UUID) = + Api.Response( + Api.ExpressionValuesComputed( + contextId, + Vector( + Api.ExpressionValueUpdate( + Program.idFooZ, + Some("Number"), + Some("45"), + None + ) + ) + ) + ) + } + } + override protected def beforeEach(): Unit = { context = new TestContext("Test") val Some(Api.Response(_, Api.InitializedNotification())) = context.receive } - "Runtime server" should "allow executing a stack of functions by entering them through call-sites" in { - val metadata = new Metadata - val _ = metadata.addItem(14, 7) - val idMainY = metadata.addItem(30, 7) - val idFooY = metadata.addItem(85, 8) - val idFooZ = metadata.addItem(102, 5) + "RuntimeServer" should "push and pop functions on the stack" in { + val mainFile = context.writeMain(Program.code) + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() - context.writeMain( - metadata.appendToCode( - """ - |main = - | x = 1 + 5 - | y = x.foo 5 - | z = y + 5 - | z - | - |Number.foo = x -> - | y = this + 3 - | z = y * x - | z - |""".stripMargin - ) + // create context + context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) + context.receive shouldEqual Some( + Api.Response(requestId, Api.CreateContextResponse(contextId)) + ) + + // push local item on top of the empty stack + val invalidLocalItem = Api.StackItem.LocalCall(Program.idMainY) + context.send( + Api + .Request(requestId, Api.PushContextRequest(contextId, invalidLocalItem)) + ) + Set.fill(2)(context.receive) shouldEqual Set( + Some(Api.Response(requestId, Api.InvalidStackItemError(contextId))), + None + ) + + // push main + val item1 = Api.StackItem.ExplicitCall( + Api.MethodPointer(mainFile, "Main", "main"), + None, + Vector() + ) + context.send( + Api.Request(requestId, Api.PushContextRequest(contextId, item1)) + ) + Set.fill(5)(context.receive) shouldEqual Set( + Some(Api.Response(requestId, Api.PushContextResponse(contextId))), + Some(Program.update.idMainX(contextId)), + Some(Program.update.idMainY(contextId)), + Some(Program.update.idMainZ(contextId)), + None + ) + + // push foo call + val item2 = Api.StackItem.LocalCall(Program.idMainY) + context.send( + Api.Request(requestId, Api.PushContextRequest(contextId, item2)) + ) + Set.fill(4)(context.receive) shouldEqual Set( + Some(Api.Response(requestId, Api.PushContextResponse(contextId))), + Some(Program.update.idFooY(contextId)), + Some(Program.update.idFooZ(contextId)), + None + ) + + // push method pointer on top of the non-empty stack + val invalidExplicitCall = Api.StackItem.ExplicitCall( + Api.MethodPointer(mainFile, "Main", "main"), + None, + Vector() ) context.send( Api.Request( - UUID.randomUUID(), - Api.Execute( - "Test.Main", - "Main", - "main", - List(idMainY) - ) + requestId, + Api.PushContextRequest(contextId, invalidExplicitCall) ) ) - val updates = Set(context.receive, context.receive) - updates shouldEqual Set( - Some(Api.Response(Api.ExpressionValueUpdateNotification(idFooY, "9"))), - Some(Api.Response(Api.ExpressionValueUpdateNotification(idFooZ, "45"))) + Set.fill(2)(context.receive) shouldEqual Set( + Some(Api.Response(requestId, Api.InvalidStackItemError(contextId))), + None + ) + + // pop foo call + context.send(Api.Request(requestId, Api.PopContextRequest(contextId))) + Set.fill(5)(context.receive) shouldEqual Set( + Some(Api.Response(requestId, Api.PopContextResponse(contextId))), + Some(Program.update.idMainX(contextId)), + Some(Program.update.idMainY(contextId)), + Some(Program.update.idMainZ(contextId)), + None + ) + + // pop main + context.send(Api.Request(requestId, Api.PopContextRequest(contextId))) + Set.fill(2)(context.receive) shouldEqual Set( + Some(Api.Response(requestId, Api.PopContextResponse(contextId))), + None + ) + + // pop empty stack + context.send(Api.Request(requestId, Api.PopContextRequest(contextId))) + Set.fill(2)(context.receive) shouldEqual Set( + Some(Api.Response(requestId, Api.EmptyStackError(contextId))), + None ) } + }