Integrate the LS with context management (#657)

This commit is contained in:
Dmitry Bushev 2020-04-17 19:31:12 +03:00 committed by GitHub
parent 86fdc07ce0
commit 75f25b66db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 507 additions and 274 deletions

View File

@ -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.

View File

@ -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(

View File

@ -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 {

View File

@ -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,

View File

@ -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
)

View File

@ -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
}

View File

@ -85,4 +85,6 @@ object ExecutionApi {
case object EmptyStackError extends Error(2003, "Stack is empty")
case object InvalidStackItemError extends Error(2004, "Invalid stack item")
}

View File

@ -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)
}
}

View File

@ -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))

View File

@ -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(

View File

@ -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

View File

@ -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<String> 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));
}
}

View File

@ -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<Package> packages;
/**
* Creates a new Enso context.
@ -41,12 +43,17 @@ public class Context {
this.out = new PrintStream(environment.out());
List<File> packagePaths = OptionsHelper.getPackagesPaths(environment);
Map<String, Module> knownFiles =
packages =
packagePaths.stream()
.map(Package::fromDirectory)
.map(ScalaConversions::asJava)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
Map<String, Module> 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<QualifiedName> 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<Module> getModuleForFile(File path) {
return getModuleNameForFile(path)
.flatMap(n -> compiler().topScope().getModule(n.toString()));
}
private void initializeScope(ModuleScope scope) {
scope.addImport(getBuiltins().getScope());
}

View File

@ -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<String> 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.
*

View File

@ -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<IdExecutionInstrument.ExpressionValue> valueCallback,
Consumer<IdExecutionInstrument.ExpressionCall> funCallCallback)
throws UnsupportedMessageException, ArityException, UnsupportedTypeException {
Optional<FunctionCallInstrumentationNode.FunctionCall> callMay =
prepareFunctionCall(moduleName, consName, methodName);
Optional<FunctionCallInstrumentationNode.FunctionCall> callMay = context
.getModuleNameForFile(modulePath)
.flatMap(moduleName -> prepareFunctionCall(moduleName.toString(), consName, methodName));
if (!callMay.isPresent()) {
return;
}
execute(callMay.get(), valueCallback, funCallCallback);
}
}

View File

@ -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.
*

View File

@ -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(
Api.ExpressionValuesComputed(
contextId,
Vector(
Api.ExpressionValueUpdate(
res.getExpressionId,
res.getValue.toString
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,14 +136,48 @@ 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()
@ -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))
)
}
}
}

View File

@ -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))
)
}
}

View File

@ -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,20 +73,17 @@ class RuntimeServerTest
}
}
override protected def beforeEach(): Unit = {
context = new TestContext("Test")
val Some(Api.Response(_, Api.InitializedNotification())) = context.receive
}
object Program {
"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 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)
context.writeMain(
metadata.appendToCode(
val text =
"""
|main =
| x = 1 + 5
@ -105,23 +96,184 @@ class RuntimeServerTest
| 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
}
"RuntimeServer" should "push and pop functions on the stack" in {
val mainFile = context.writeMain(Program.code)
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
// 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)
)
)
Set.fill(2)(context.receive) shouldEqual Set(
Some(Api.Response(requestId, Api.InvalidStackItemError(contextId))),
None
)
val updates = Set(context.receive, context.receive)
updates shouldEqual Set(
Some(Api.Response(Api.ExpressionValueUpdateNotification(idFooY, "9"))),
Some(Api.Response(Api.ExpressionValueUpdateNotification(idFooZ, "45")))
// 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
)
}
}