Truffle Integration for the Binary Protocol (#711)

This commit is contained in:
Łukasz Olczak 2020-05-06 14:23:00 +02:00 committed by GitHub
parent 7c7bd3e6ae
commit 29190f8339
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1162 additions and 229 deletions

View File

@ -2552,7 +2552,16 @@ null
```
##### Errors
TBC
- [`AccessDeniedError`](#accessdeniederror) when the user does not hold the
`executionContext/canModify` capability for this context.
- [`ContextNotFoundError`](#contextnotfounderror) when context can not be found
by provided id.
- [`ModuleNotFoundError`](#modulenotfounderror) to signal that the module with
the visualisation cannot be found.
- [`VisualisationExpressionError`](#visualisationexpressionerror) to signal that
the expression specified in the `VisualisationConfiguration` cannot be
evaluated.
#### `executionContext/detachVisualisation`
This message allows a client to detach a visualisation from the executing code.
@ -2568,6 +2577,7 @@ This message allows a client to detach a visualisation from the executing code.
interface DetachVisualisationRequest {
executionContextId: UUID;
visualisationId: UUID;
expressionId: UUID;
}
```
@ -2578,7 +2588,12 @@ null
```
##### Errors
TBC
- [`AccessDeniedError`](#accessdeniederror) when the user does not hold the
`executionContext/canModify` capability for this context.
- [`ContextNotFoundError`](#contextnotfounderror) when context can not be found
by provided id.
- [`VisualisationNotFoundError`](#visualisationnotfounderror) when a
visualisation can not be found.
#### `executionContext/modifyVisualisation`
This message allows a client to modify the configuration for an existing
@ -2605,7 +2620,17 @@ null
```
##### Errors
TBC
- [`AccessDeniedError`](#accessdeniederror) when the user does not hold the
`executionContext/canModify` capability for this context.
- [`ContextNotFoundError`](#contextnotfounderror) when context can not be found
by provided id.
- [`ModuleNotFoundError`](#modulenotfounderror) to signal that the module with
the visualisation cannot be found.
- [`VisualisationExpressionError`](#visualisationexpressionerror) to signal that
the expression specified in the `VisualisationConfiguration` cannot be
evaluated.
- [`VisualisationNotFoundError`](#visualisationnotfounderror) when a
visualisation can not be found.
#### `executionContext/visualisationUpdate`
This message is responsible for providing a visualisation data update to the
@ -2771,6 +2796,48 @@ It signals that stack is invalid in this context.
}
```
##### `ModuleNotFoundError`
It signals that the given module cannot be found.
```typescript
"error" : {
"code" : 2005,
"message" : "Module not found [Foo.Bar.Baz]"
}
```
##### `VisualisationNotFoundError`
It signals that the visualisation cannot be found.
```typescript
"error" : {
"code" : 2006,
"message" : "Visualisation not found"
}
```
##### `VisualisationExpressionError`
It signals that the expression specified in the `VisualisationConfiguration`
cannot be evaluated.
```typescript
"error" : {
"code" : 2007,
"message" : "Evaluation of the visualisation expression failed [i is not defined]"
}
```
##### `VisualisationEvaluationError`
It is a push message. It signals that an evaluation of a code responsible for
generating visualisation data failed.
```typescript
"error" : {
"code" : 2008,
"message" : "Evaluation of the visualisation failed [cannot execute foo]"
}
```
##### `FileNotOpenedError`
Signals that a file wasn't opened.

View File

@ -28,7 +28,7 @@ import org.enso.languageserver.protocol.data.factory.{
}
import org.enso.languageserver.protocol.data.session.SessionInit
import org.enso.languageserver.protocol.data.util.EnsoUUID
import org.enso.languageserver.runtime.VisualisationProtocol.VisualisationUpdate
import org.enso.languageserver.runtime.ContextRegistryProtocol.VisualisationUpdate
import org.enso.languageserver.session.DataSession
import org.enso.languageserver.util.UnhandledLogging
import org.enso.languageserver.util.binary.DecodingFailure

View File

@ -2,7 +2,7 @@ package org.enso.languageserver.protocol.data.factory
import com.google.flatbuffers.FlatBufferBuilder
import org.enso.languageserver.protocol.data.executioncontext
import org.enso.languageserver.runtime.VisualisationProtocol.{
import org.enso.languageserver.runtime.ContextRegistryProtocol.{
VisualisationContext,
VisualisationUpdate
}

View File

@ -8,8 +8,7 @@ import org.enso.languageserver.requesthandler.RequestTimeout
import org.enso.languageserver.runtime.VisualisationApi.AttachVisualisation
import org.enso.languageserver.runtime.{
ContextRegistryProtocol,
RuntimeFailureMapper,
VisualisationProtocol
RuntimeFailureMapper
}
import org.enso.languageserver.util.UnhandledLogging
@ -36,7 +35,7 @@ class AttachVisualisationHandler(
private def requestStage: Receive = {
case Request(AttachVisualisation, id, params: AttachVisualisation.Params) =>
contextRegistry ! VisualisationProtocol.AttachVisualisation(
contextRegistry ! ContextRegistryProtocol.AttachVisualisation(
clientId,
params.visualisationId,
params.expressionId,
@ -57,7 +56,7 @@ class AttachVisualisationHandler(
replyTo ! ResponseError(Some(id), ServiceError)
context.stop(self)
case VisualisationProtocol.VisualisationAttached =>
case ContextRegistryProtocol.VisualisationAttached =>
replyTo ! ResponseResult(AttachVisualisation, id, Unused)
cancellable.cancel()
context.stop(self)

View File

@ -8,8 +8,7 @@ import org.enso.languageserver.requesthandler.RequestTimeout
import org.enso.languageserver.runtime.VisualisationApi.DetachVisualisation
import org.enso.languageserver.runtime.{
ContextRegistryProtocol,
RuntimeFailureMapper,
VisualisationProtocol
RuntimeFailureMapper
}
import org.enso.languageserver.util.UnhandledLogging
@ -36,7 +35,7 @@ class DetachVisualisationHandler(
private def requestStage: Receive = {
case Request(DetachVisualisation, id, params: DetachVisualisation.Params) =>
contextRegistry ! VisualisationProtocol.DetachVisualisation(
contextRegistry ! ContextRegistryProtocol.DetachVisualisation(
clientId,
params.contextId,
params.visualisationId,
@ -57,7 +56,7 @@ class DetachVisualisationHandler(
replyTo ! ResponseError(Some(id), ServiceError)
context.stop(self)
case VisualisationProtocol.VisualisationDetached =>
case ContextRegistryProtocol.VisualisationDetached =>
replyTo ! ResponseResult(DetachVisualisation, id, Unused)
cancellable.cancel()
context.stop(self)

View File

@ -8,8 +8,7 @@ import org.enso.languageserver.requesthandler.RequestTimeout
import org.enso.languageserver.runtime.VisualisationApi.ModifyVisualisation
import org.enso.languageserver.runtime.{
ContextRegistryProtocol,
RuntimeFailureMapper,
VisualisationProtocol
RuntimeFailureMapper
}
import org.enso.languageserver.util.UnhandledLogging
@ -36,7 +35,7 @@ class ModifyVisualisationHandler(
private def requestStage: Receive = {
case Request(ModifyVisualisation, id, params: ModifyVisualisation.Params) =>
contextRegistry ! VisualisationProtocol.ModifyVisualisation(
contextRegistry ! ContextRegistryProtocol.ModifyVisualisation(
clientId,
params.visualisationId,
params.visualisationConfig
@ -56,7 +55,7 @@ class ModifyVisualisationHandler(
replyTo ! ResponseError(Some(id), ServiceError)
context.stop(self)
case VisualisationProtocol.VisualisationModified =>
case ContextRegistryProtocol.VisualisationModified =>
replyTo ! ResponseResult(ModifyVisualisation, id, Unused)
cancellable.cancel()
context.stop(self)

View File

@ -2,11 +2,11 @@ package org.enso.languageserver.runtime
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import org.enso.languageserver.data.Config
import org.enso.languageserver.runtime.ExecutionApi.ContextId
import org.enso.languageserver.runtime.VisualisationProtocol.{
import org.enso.languageserver.runtime.ContextRegistryProtocol.{
VisualisationContext,
VisualisationUpdate
}
import org.enso.languageserver.runtime.ExecutionApi.ContextId
import org.enso.languageserver.session.RpcSession
import org.enso.languageserver.session.SessionRouter.DeliverToDataController
import org.enso.languageserver.util.UnhandledLogging

View File

@ -6,11 +6,6 @@ import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import org.enso.languageserver.data.{ClientId, Config}
import org.enso.languageserver.filemanager.FileSystemFailure
import org.enso.languageserver.monitoring.MonitoringProtocol.{Ping, Pong}
import org.enso.languageserver.runtime.VisualisationProtocol.{
AttachVisualisation,
DetachVisualisation,
ModifyVisualisation
}
import org.enso.languageserver.runtime.handler._
import org.enso.languageserver.util.UnhandledLogging
import org.enso.polyglot.runtime.Runtime.Api

View File

@ -1,5 +1,8 @@
package org.enso.languageserver.runtime
import java.util.UUID
import org.enso.languageserver.data.ClientId
import org.enso.languageserver.filemanager.FileSystemFailure
import org.enso.languageserver.runtime.ExecutionApi.ContextId
import org.enso.languageserver.session.RpcSession
@ -139,4 +142,119 @@ object ContextRegistryProtocol {
* @param contextId execution context identifier
*/
case class InvalidStackItemError(contextId: ContextId) extends Failure
/**
* Requests the language server to attach a visualisation to the expression
* specified by `expressionId`.
*
* @param clientId the requester id
* @param visualisationId an identifier of a visualisation
* @param expressionId an identifier of an expression which is visualised
* @param visualisationConfig a configuration object for properties of the
* visualisation
*/
case class AttachVisualisation(
clientId: ClientId,
visualisationId: UUID,
expressionId: UUID,
visualisationConfig: VisualisationConfiguration
)
/**
* Signals that attaching a visualisation has succeeded.
*/
case object VisualisationAttached
/**
* Requests the language server to detach a visualisation from the expression
* specified by `expressionId`.
*
* @param clientId the requester id
* @param contextId an execution context identifier
* @param visualisationId an identifier of a visualisation
* @param expressionId an identifier of an expression which is visualised
*/
case class DetachVisualisation(
clientId: ClientId,
contextId: UUID,
visualisationId: UUID,
expressionId: UUID
)
/**
* Signals that detaching a visualisation has succeeded.
*/
case object VisualisationDetached
/**
* Requests the language server to modify a visualisation.
*
* @param clientId the requester id
* @param visualisationId an identifier of a visualisation
* @param visualisationConfig a configuration object for properties of the
* visualisation
*/
case class ModifyVisualisation(
clientId: ClientId,
visualisationId: UUID,
visualisationConfig: VisualisationConfiguration
)
/**
* Signals that a visualisation modification has succeeded.
*/
case object VisualisationModified
/**
* Represents a visualisation context.
*
* @param visualisationId a visualisation identifier
* @param contextId a context identifier
* @param expressionId an expression identifier
*/
case class VisualisationContext(
visualisationId: UUID,
contextId: UUID,
expressionId: UUID
)
/**
* An event signaling a visualisation update.
*
* @param visualisationContext a visualisation context
* @param data a visualisation data
*/
case class VisualisationUpdate(
visualisationContext: VisualisationContext,
data: Array[Byte]
)
/**
* Signals that a module cannot be found.
*
* @param moduleName the module name
*/
case class ModuleNotFound(moduleName: String) extends Failure
/**
* Signals that visualisation cannot be found.
*/
case object VisualisationNotFound extends Failure
/**
* Signals that an expression specified in a [[AttachVisualisation]] or
* a [[ModifyVisualisation]] cannot be evaluated.
*
* @param message the reason of the failure
*/
case class VisualisationExpressionFailed(message: String) extends Failure
/**
* Signals that an evaluation of a code responsible for generating
* visualisation data failed.
*
* @param message the reason of the failure
*/
case class VisualisationEvaluationFailed(message: String) extends Failure
}

View File

@ -104,4 +104,18 @@ object ExecutionApi {
case object InvalidStackItemError extends Error(2004, "Invalid stack item")
case class ModuleNotFoundError(moduleName: String)
extends Error(2005, s"Module not found [$moduleName]")
case object VisualisationNotFoundError
extends Error(2006, s"Visualisation not found")
case class VisualisationExpressionError(msg: String)
extends Error(
2007,
s"Evaluation of the visualisation expression failed [$msg]"
)
case class VisualisationEvaluationError(msg: String)
extends Error(2008, s"Evaluation of the visualisation failed [$msg]")
}

View File

@ -1,10 +1,10 @@
package org.enso.languageserver.runtime
import org.enso.jsonrpc._
import org.enso.languageserver.filemanager.FileSystemFailureMapper
import org.enso.languageserver.protocol.rpc.ErrorApi._
import org.enso.languageserver.runtime.ExecutionApi._
import org.enso.polyglot.runtime.Runtime.Api
import org.enso.jsonrpc.Error
object RuntimeFailureMapper {
@ -26,6 +26,14 @@ object RuntimeFailureMapper {
EmptyStackError
case ContextRegistryProtocol.InvalidStackItemError(_) =>
InvalidStackItemError
case ContextRegistryProtocol.VisualisationNotFound =>
VisualisationNotFoundError
case ContextRegistryProtocol.ModuleNotFound(name) =>
ModuleNotFoundError(name)
case ContextRegistryProtocol.VisualisationExpressionFailed(msg) =>
VisualisationExpressionError(msg)
case ContextRegistryProtocol.VisualisationEvaluationFailed(msg) =>
VisualisationEvaluationError(msg)
}
/**
@ -42,6 +50,14 @@ object RuntimeFailureMapper {
ContextRegistryProtocol.EmptyStackError(contextId)
case Api.InvalidStackItemError(contextId) =>
ContextRegistryProtocol.InvalidStackItemError(contextId)
case Api.ModuleNotFound(moduleName: String) =>
ContextRegistryProtocol.ModuleNotFound(moduleName)
case Api.VisualisationExpressionFailed(message: String) =>
ContextRegistryProtocol.VisualisationExpressionFailed(message)
case Api.VisualisationEvaluationFailed(message: String) =>
ContextRegistryProtocol.VisualisationEvaluationFailed(message)
case Api.VisualisationNotFound() =>
ContextRegistryProtocol.VisualisationNotFound
}
}

View File

@ -1,95 +0,0 @@
package org.enso.languageserver.runtime
import java.util.UUID
import org.enso.languageserver.data.ClientId
object VisualisationProtocol {
/**
* Requests the language server to attach a visualisation to the expression
* specified by `expressionId`.
*
* @param clientId the requester id
* @param visualisationId an identifier of a visualisation
* @param expressionId an identifier of an expression which is visualised
* @param visualisationConfig a configuration object for properties of the
* visualisation
*/
case class AttachVisualisation(
clientId: ClientId,
visualisationId: UUID,
expressionId: UUID,
visualisationConfig: VisualisationConfiguration
)
/**
* Signals that attaching a visualisation has succeeded.
*/
case object VisualisationAttached
/**
* Requests the language server to detach a visualisation from the expression
* specified by `expressionId`.
*
* @param clientId the requester id
* @param contextId an execution context identifier
* @param visualisationId an identifier of a visualisation
* @param expressionId an identifier of an expression which is visualised
*/
case class DetachVisualisation(
clientId: ClientId,
contextId: UUID,
visualisationId: UUID,
expressionId: UUID
)
/**
* Signals that detaching a visualisation has succeeded.
*/
case object VisualisationDetached
/**
* Requests the language server to modify a visualisation.
*
* @param clientId the requester id
* @param visualisationId an identifier of a visualisation
* @param visualisationConfig a configuration object for properties of the
* visualisation
*/
case class ModifyVisualisation(
clientId: ClientId,
visualisationId: UUID,
visualisationConfig: VisualisationConfiguration
)
/**
* Signals that a visualisation modification has succeeded.
*/
case object VisualisationModified
/**
* Represents a visualisation context.
*
* @param visualisationId a visualisation identifier
* @param contextId a context identifier
* @param expressionId an expression identifier
*/
case class VisualisationContext(
visualisationId: UUID,
contextId: UUID,
expressionId: UUID
)
/**
* An event signaling a visualisation update.
*
* @param visualisationContext a visualisation context
* @param data a visualisation data
*/
case class VisualisationUpdate(
visualisationContext: VisualisationContext,
data: Array[Byte]
)
}

View File

@ -5,8 +5,8 @@ import java.util.UUID
import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props}
import org.enso.languageserver.requesthandler.RequestTimeout
import org.enso.languageserver.runtime.{
RuntimeFailureMapper,
VisualisationProtocol
ContextRegistryProtocol,
RuntimeFailureMapper
}
import org.enso.languageserver.util.UnhandledLogging
import org.enso.polyglot.runtime.Runtime.Api
@ -47,7 +47,7 @@ class AttachVisualisationHandler(
context.stop(self)
case Api.Response(_, Api.VisualisationAttached()) =>
replyTo ! VisualisationProtocol.VisualisationAttached
replyTo ! ContextRegistryProtocol.VisualisationAttached
cancellable.cancel()
context.stop(self)

View File

@ -5,8 +5,8 @@ import java.util.UUID
import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props}
import org.enso.languageserver.requesthandler.RequestTimeout
import org.enso.languageserver.runtime.{
RuntimeFailureMapper,
VisualisationProtocol
ContextRegistryProtocol,
RuntimeFailureMapper
}
import org.enso.languageserver.util.UnhandledLogging
import org.enso.polyglot.runtime.Runtime.Api
@ -47,7 +47,7 @@ class DetachVisualisationHandler(
context.stop(self)
case Api.Response(_, Api.VisualisationDetached()) =>
replyTo ! VisualisationProtocol.VisualisationDetached
replyTo ! ContextRegistryProtocol.VisualisationDetached
cancellable.cancel()
context.stop(self)

View File

@ -5,8 +5,8 @@ import java.util.UUID
import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props}
import org.enso.languageserver.requesthandler.RequestTimeout
import org.enso.languageserver.runtime.{
RuntimeFailureMapper,
VisualisationProtocol
ContextRegistryProtocol,
RuntimeFailureMapper
}
import org.enso.languageserver.util.UnhandledLogging
import org.enso.polyglot.runtime.Runtime.Api
@ -47,7 +47,7 @@ class ModifyVisualisationHandler(
context.stop(self)
case Api.Response(_, Api.VisualisationModified()) =>
replyTo ! VisualisationProtocol.VisualisationModified
replyTo ! ContextRegistryProtocol.VisualisationModified
cancellable.cancel()
context.stop(self)

View File

@ -1,6 +1,6 @@
include "util.fbs";
include "session.fbs";
include "execution-context.fbs";
include "execution_context.fbs";
namespace org.enso.languageserver.protocol.data.envelope;

View File

@ -10,7 +10,7 @@ import org.enso.languageserver.protocol.data.envelope.{
OutboundPayload
}
import org.enso.languageserver.protocol.data.executioncontext
import org.enso.languageserver.runtime.VisualisationProtocol.{
import org.enso.languageserver.runtime.ContextRegistryProtocol.{
VisualisationContext,
VisualisationUpdate
}

View File

@ -75,6 +75,10 @@ object Runtime {
value = classOf[Api.CloseFileNotification],
name = "closeFileNotification"
),
new JsonSubTypes.Type(
value = classOf[Api.VisualisationUpdate],
name = "visualisationUpdate"
),
new JsonSubTypes.Type(
value = classOf[Api.AttachVisualisation],
name = "attachVisualisation"
@ -111,6 +115,22 @@ object Runtime {
value = classOf[Api.EmptyStackError],
name = "emptyStackError"
),
new JsonSubTypes.Type(
value = classOf[Api.ModuleNotFound],
name = "moduleNotFound"
),
new JsonSubTypes.Type(
value = classOf[Api.VisualisationExpressionFailed],
name = "visualisationExpressionFailed"
),
new JsonSubTypes.Type(
value = classOf[Api.VisualisationEvaluationFailed],
name = "visualisationEvaluationFailed"
),
new JsonSubTypes.Type(
value = classOf[Api.VisualisationNotFound],
name = "visualisationNotFound"
),
new JsonSubTypes.Type(
value = classOf[Api.InvalidStackItemError],
name = "invalidStackItemError"
@ -428,6 +448,34 @@ object Runtime {
*/
case class ContextNotExistError(contextId: ContextId) extends Error
/**
* Signals that a module cannot be found.
*
* @param moduleName the module name
*/
case class ModuleNotFound(moduleName: String) extends Error
/**
* Signals that an expression specified in a [[AttachVisualisation]] or
* a [[ModifyVisualisation]] cannot be evaluated.
*
* @param message the reason of the failure
*/
case class VisualisationExpressionFailed(message: String) extends Error
/**
* Signals that an evaluation of a code responsible for generating
* visualisation data failed.
*
* @param message the reason of the failure
*/
case class VisualisationEvaluationFailed(message: String) extends Error
/**
* Signals that visualisation cannot be found.
*/
case class VisualisationNotFound() extends Error
/**
* An error response signifying that stack is empty.
*

View File

@ -174,6 +174,16 @@ public class Context {
.flatMap(n -> getCompiler().topScope().getModule(n.toString()));
}
/**
* Fetches a module with a given name.
*
* @param moduleName the qualified name of the module to lookup.
* @return the relevant module, if exists.
*/
public Optional<Module> findModule(String moduleName) {
return getCompiler().topScope().getModule(moduleName);
}
/**
* Registers a new module corresponding to a given file.
*

View File

@ -2,10 +2,7 @@ package org.enso.interpreter.service;
import com.oracle.truffle.api.instrumentation.EventBinding;
import com.oracle.truffle.api.instrumentation.ExecutionEventListener;
import com.oracle.truffle.api.interop.ArityException;
import com.oracle.truffle.api.interop.InteropLibrary;
import com.oracle.truffle.api.interop.UnsupportedMessageException;
import com.oracle.truffle.api.interop.UnsupportedTypeException;
import com.oracle.truffle.api.interop.*;
import com.oracle.truffle.api.source.SourceSection;
import org.enso.interpreter.instrument.Cache;
import org.enso.interpreter.instrument.IdExecutionInstrument;
@ -17,6 +14,8 @@ import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.scope.ModuleScope;
import java.io.File;
import org.enso.polyglot.MethodNames;
import org.enso.text.buffer.Rope;
import org.enso.text.editing.JavaEditorAdapter;
import org.enso.text.editing.model;
@ -129,6 +128,36 @@ public class ExecutionService {
execute(callMay.get(), cache, valueCallback, funCallCallback);
}
/**
* Evaluates an expression in the scope of the provided module.
*
* @param module the module providing a scope for the expression
* @param expression the expression to evluated
* @return a result of evaluation
*/
public Object evaluateExpression(Module module, String expression)
throws UnsupportedMessageException, ArityException,
UnknownIdentifierException, UnsupportedTypeException {
return interopLibrary.invokeMember(
module,
MethodNames.Module.EVAL_EXPRESSION,
expression
);
}
/**
* Calls a function with the given argument.
*
* @param fn the function object
* @param argument the argument applied to the function
* @return the result of calling the function
*/
public Object callFunction(Object fn, Object argument)
throws UnsupportedTypeException, ArityException,
UnsupportedMessageException {
return interopLibrary.execute(fn, argument);
}
/**
* Sets a module at a given path to use a literal source.
*
@ -155,6 +184,16 @@ public class ExecutionService {
module.ifPresent(Module::unsetLiteralSource);
}
/**
* Finds a module by qualified name.
*
* @param moduleName the qualified name of the module
* @return the relevant module, if exists
*/
public Optional<Module> findModule(String moduleName) {
return context.findModule(moduleName);
}
/**
* Applies modifications to literal module sources.
*

View File

@ -1,79 +0,0 @@
package org.enso.interpeter.instrument
import org.enso.polyglot.runtime.Runtime.Api.{ContextId, StackItem}
import scala.collection.mutable.Stack
/**
* Storage for active execution contexts.
*/
class ExecutionContextManager {
private var contexts: Map[ContextId, Stack[StackItem]] = Map()
/**
* Creates a new context with a given id.
*
* @param id the context id.
*/
def create(id: ContextId): Unit =
contexts += id -> Stack.empty
/**
* Destroys a context with a given id.
* @param id the context id.
*/
def destroy(id: ContextId): Unit =
contexts -= id
/**
* Gets a context with a given id.
*
* @param id the context id.
* @return the context with the given id, if exists.
*/
def get(id: ContextId): Option[ContextId] =
for {
_ <- contexts.get(id)
} yield 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(id, Stack())
/**
* Gets all execution contexts.
*
* @return all currently available execution contexsts.
*/
def getAll: collection.MapView[ContextId, Stack[StackItem]] =
contexts.view
/**
* If the context exists, push the item on the stack.
*
* @param id the context id.
* @param item stack item.
* @return Unit representing success or None if the context does not exist.
*/
def push(id: ContextId, item: StackItem): Option[Unit] =
for {
stack <- contexts.get(id)
} yield stack.push(item)
/**
* If the context exists and stack not empty, pop the item from the stack.
*
* @param id the context id.
* @return stack item or None if the stack is empty or not exists.
*/
def pop(id: ContextId): Option[StackItem] =
for {
stack <- contexts.get(id)
if stack.nonEmpty
} yield stack.pop()
}

View File

@ -0,0 +1,158 @@
package org.enso.interpeter.instrument
import org.enso.polyglot.runtime.Runtime.Api.{
ContextId,
ExpressionId,
StackItem,
VisualisationId
}
import scala.collection.mutable.Stack
/**
* Storage for active execution contexts.
*/
class ExecutionContextManager {
private var contexts: Map[ContextId, ExecutionContextState] =
Map().withDefaultValue(ExecutionContextState.empty)
/**
* Creates a new context with a given id.
*
* @param id the context id.
*/
def create(id: ContextId): Unit =
contexts += id -> ExecutionContextState.empty
/**
* Destroys a context with a given id.
* @param id the context id.
*/
def destroy(id: ContextId): Unit =
contexts -= id
/**
* Gets a context with a given id.
*
* @param id the context id.
* @return the context with the given id, if exists.
*/
def get(id: ContextId): Option[ContextId] =
for {
_ <- contexts.get(id)
} yield id
/**
* Gets a stack for a given context id.
*
* @param id the context id.
* @return the stack.
*/
def getStack(id: ContextId): Stack[StackItem] =
contexts(id).stack
/**
* Gets all execution contexts.
*
* @return all currently available execution contexsts.
*/
def getAll: collection.MapView[ContextId, Stack[StackItem]] =
contexts.view.mapValues(_.stack)
/**
* If the context exists, push the item on the stack.
*
* @param id the context id.
* @param item stack item.
* @return Unit representing success or None if the context does not exist.
*/
def push(id: ContextId, item: StackItem): Option[Unit] =
for {
state <- contexts.get(id)
} yield state.stack.push(item)
/**
* If the context exists and stack not empty, pop the item from the stack.
*
* @param id the context id.
* @return stack item or None if the stack is empty or not exists.
*/
def pop(id: ContextId): Option[StackItem] =
for {
state <- contexts.get(id)
if state.stack.nonEmpty
} yield state.stack.pop()
/**
* Tests if a context specified by its id is stored by the manager.
*
* @param contextId the identifier of the execution context
* @return true if the context is stored or false otherwise
*/
def contains(contextId: ContextId): Boolean = contexts.contains(contextId)
/**
* Upserts a visualisation for the specified context.
*
* @param contextId the identifier of the execution context
* @param visualisation the visualisation to upsert
*/
def upsertVisualisation(
contextId: ContextId,
visualisation: Visualisation
): Unit = {
val state = contexts(contextId)
state.visualisations.upsert(visualisation)
}
/**
* Returns a visualisation with the provided id.
*
* @param contextId the identifier of the execution context
* @param visualisationId the identifier of visualisation
* @return an option with visualisation
*/
def getVisualisationById(
contextId: ContextId,
visualisationId: VisualisationId
): Option[Visualisation] =
for {
state <- contexts.get(contextId)
visualisation <- state.visualisations.getById(visualisationId)
} yield visualisation
/**
* Finds all visualisations attached to an expression.
*
* @param contextId the identifier of the execution context
* @param expressionId the unique identifier of the expression
* @return a list of matching visualisation
*/
def findVisualisationForExpression(
contextId: ContextId,
expressionId: ExpressionId
): List[Visualisation] =
for {
state <- contexts.get(contextId).toList
visualisation <- state.visualisations.find(expressionId)
} yield visualisation
/**
* Removes a visualisation from the holder.
*
* @param contextId the identifier of the execution context
* @param visualisationId the visualisation identifier
* @param expressionId the id of expression that the visualisation is
* attached to
*/
def removeVisualisation(
contextId: ContextId,
expressionId: ExpressionId,
visualisationId: VisualisationId
): Unit = {
val state = contexts(contextId)
state.visualisations.remove(visualisationId, expressionId)
}
}

View File

@ -0,0 +1,26 @@
package org.enso.interpeter.instrument
import org.enso.polyglot.runtime.Runtime.Api.StackItem
import scala.collection.mutable.Stack
/**
* Represents a state of an execution context.
*
* @param stack the current call stack for the execution context
* @param visualisations the holder of all visualisations attached to the
* execution context
*/
case class ExecutionContextState(
stack: Stack[StackItem],
visualisations: VisualisationHolder
)
object ExecutionContextState {
/**
* Returns empty state.
*/
def empty = ExecutionContextState(Stack.empty, VisualisationHolder.empty)
}

View File

@ -5,8 +5,14 @@ import java.nio.ByteBuffer
import java.util.UUID
import java.util.function.Consumer
import cats.implicits._
import com.oracle.truffle.api.TruffleContext
import org.enso.interpreter.instrument.Cache
import org.enso.interpeter.instrument.Handler.{
EvalFailure,
EvaluationFailed,
ModuleNotFound
}
import org.enso.interpreter.instrument.IdExecutionInstrument.{
ExpressionCall,
ExpressionValue
@ -14,11 +20,19 @@ import org.enso.interpreter.instrument.IdExecutionInstrument.{
import org.enso.interpreter.node.callable.FunctionCallInstrumentationNode.FunctionCall
import org.enso.interpreter.service.ExecutionService
import org.enso.pkg.QualifiedName
import org.enso.polyglot.runtime.Runtime.Api
import org.enso.polyglot.runtime.Runtime.Api.{
ContextId,
ExpressionId,
RequestId,
VisualisationId
}
import org.enso.polyglot.runtime.Runtime.{Api, ApiResponse}
import org.graalvm.polyglot.io.MessageEndpoint
import scala.jdk.javaapi.OptionConverters
import scala.jdk.CollectionConverters._
import scala.jdk.javaapi.OptionConverters
import scala.util.control.NonFatal
import cats.implicits._
/**
* A message endpoint implementation used by the
@ -95,9 +109,17 @@ final class Handler {
case class CallData(callData: FunctionCall) extends ExecutionItem
}
private def sendUpdate(
private def onExpressionValueComputed(
contextId: Api.ContextId,
res: ExpressionValue
value: ExpressionValue
): Unit = {
sendValueUpdate(contextId, value)
fireVisualisationUpdates(contextId, value)
}
private def sendValueUpdate(
contextId: ContextId,
value: ExpressionValue
): Unit = {
endpoint.sendToClient(
Api.Response(
@ -105,10 +127,10 @@ final class Handler {
contextId,
Vector(
Api.ExpressionValueUpdate(
res.getExpressionId,
OptionConverters.toScala(res.getType),
Some(res.getValue.toString),
toMethodPointer(res)
value.getExpressionId,
OptionConverters.toScala(value.getType),
Some(value.getValue.toString),
toMethodPointer(value)
)
)
)
@ -116,6 +138,63 @@ final class Handler {
)
}
private def fireVisualisationUpdates(
contextId: ContextId,
value: ExpressionValue
): Unit = {
val visualisations =
contextManager.findVisualisationForExpression(
contextId,
value.getExpressionId
)
visualisations foreach { visualisation =>
emitVisualisationUpdate(contextId, value, visualisation)
}
}
private def emitVisualisationUpdate(
contextId: ContextId,
value: ExpressionValue,
visualisation: Visualisation
): Unit = {
val errorMsgOrVisualisationData =
Either
.catchNonFatal {
executionService.callFunction(
visualisation.callback,
value.getValue
)
}
.leftMap(_.getMessage)
.flatMap {
case text: String => Right(text.getBytes("UTF-8"))
case bytes: Array[Byte] => Right(bytes)
case other =>
Left(s"Cannot encode ${other.getClass} to byte array")
}
errorMsgOrVisualisationData match {
case Left(msg) =>
endpoint.sendToClient(
Api.Response(Api.VisualisationEvaluationFailed(msg))
)
case Right(data) =>
endpoint.sendToClient(
Api.Response(
Api.VisualisationUpdate(
Api.VisualisationContext(
visualisation.id,
contextId,
value.getExpressionId
),
data
)
)
)
}
}
private def toMethodPointer(
value: ExpressionValue
): Option[Api.MethodPointer] =
@ -199,7 +278,11 @@ final class Handler {
}
val (explicitCalls, localCalls) = unwind(stack, Nil, Nil)
explicitCalls.headOption.foreach { item =>
execute(toExecutionItem(item), localCalls, sendUpdate(contextId, _))
execute(
toExecutionItem(item),
localCalls,
onExpressionValueComputed(contextId, _)
)
}
}
@ -218,7 +301,7 @@ final class Handler {
call.methodPointer.name
)
private def withContext(action: => Unit): Unit = {
private def withContext[A](action: => A): A = {
val token = truffleContext.enter()
try {
action
@ -322,7 +405,161 @@ final class Handler {
executionService.modifyModuleSources(path, edits.asJava)
withContext(executeAll())
case _ => throw new RuntimeException("Unhandled cases.")
case Api.AttachVisualisation(visualisationId, expressionId, config) =>
if (contextManager.contains(config.executionContextId)) {
upsertVisualisation(
requestId,
visualisationId,
expressionId,
config,
Api.VisualisationAttached()
)
} else {
endpoint.sendToClient(
Api.Response(
requestId,
Api.ContextNotExistError(config.executionContextId)
)
)
}
case Api.DetachVisualisation(ctxId, visualisationId, exprId) =>
if (contextManager.contains(ctxId)) {
contextManager.removeVisualisation(ctxId, exprId, visualisationId)
endpoint.sendToClient(
Api.Response(
requestId,
Api.VisualisationDetached()
)
)
} else {
endpoint.sendToClient(
Api.Response(
requestId,
Api.ContextNotExistError(ctxId)
)
)
}
case Api.ModifyVisualisation(visualisationId, config) =>
if (contextManager.contains(config.executionContextId)) {
val maybeVisualisation = contextManager.getVisualisationById(
config.executionContextId,
visualisationId
)
maybeVisualisation match {
case None =>
endpoint.sendToClient(
Api.Response(requestId, Api.VisualisationNotFound())
)
case Some(visualisation) =>
upsertVisualisation(
requestId,
visualisationId,
visualisation.expressionId,
config,
Api.VisualisationModified()
)
}
} else {
endpoint.sendToClient(
Api.Response(
requestId,
Api.ContextNotExistError(config.executionContextId)
)
)
}
}
}
private def upsertVisualisation(
requestId: Option[RequestId],
visualisationId: VisualisationId,
expressionId: ExpressionId,
config: Api.VisualisationConfiguration,
replyWith: ApiResponse
): Unit = {
val maybeCallable =
evaluateExpression(config.visualisationModule, config.expression)
maybeCallable match {
case Left(ModuleNotFound) =>
endpoint.sendToClient(
Api.Response(
requestId,
Api.ModuleNotFound(config.visualisationModule)
)
)
case Left(EvaluationFailed(msg)) =>
endpoint.sendToClient(
Api.Response(
requestId,
Api.VisualisationExpressionFailed(msg)
)
)
case Right(callable) =>
val visualisation = Visualisation(
visualisationId,
expressionId,
callable
)
contextManager.upsertVisualisation(
config.executionContextId,
visualisation
)
endpoint.sendToClient(
Api.Response(requestId, replyWith)
)
val stack = contextManager.getStack(config.executionContextId)
withContext(execute(config.executionContextId, stack.toList))
}
}
private def evaluateExpression(
moduleName: String,
expression: String
): Either[EvalFailure, AnyRef] = {
val maybeModule = executionService.findModule(moduleName)
val notFoundOrModule =
if (maybeModule.isPresent) Right(maybeModule.get())
else Left(ModuleNotFound)
notFoundOrModule.flatMap { module =>
try {
withContext {
executionService.evaluateExpression(module, expression).asRight
}
} catch {
case NonFatal(th) => EvaluationFailed(th.getMessage).asLeft
}
}
}
}
object Handler {
/**
* Base trait for evaluation failures.
*/
sealed trait EvalFailure
/**
* Signals that a module cannto be found.
*/
case object ModuleNotFound extends EvalFailure
/**
* Signals that an evaluation of an expression failed.
*
* @param msg the textual reason of a failure
*/
case class EvaluationFailed(msg: String) extends EvalFailure
}

View File

@ -0,0 +1,17 @@
package org.enso.interpeter.instrument
import org.enso.polyglot.runtime.Runtime.Api.{ExpressionId, VisualisationId}
/**
* An object containing visualisation data.
*
* @param id the unique identifier of visualisation
* @param expressionId the identifier of expression that the visualisation is
* attached to
* @param callback the callable expression used to generate visualisation data
*/
case class Visualisation(
id: VisualisationId,
expressionId: ExpressionId,
callback: AnyRef
)

View File

@ -0,0 +1,67 @@
package org.enso.interpeter.instrument
import org.enso.polyglot.runtime.Runtime.Api.{ExpressionId, VisualisationId}
/**
* A mutable holder of all visualisations attached to an execution context.
*/
class VisualisationHolder() {
private var visualisationMap: Map[ExpressionId, List[Visualisation]] =
Map.empty.withDefaultValue(List.empty)
/**
* Upserts a visualisation.
*
* @param visualisation the visualisation to upsert
*/
def upsert(visualisation: Visualisation): Unit = {
val visualisations = visualisationMap(visualisation.expressionId)
val removed = visualisations.filterNot(_.id == visualisation.id)
visualisationMap += (visualisation.expressionId -> (visualisation :: removed))
}
/**
* Removes a visualisation from the holder.
*
* @param visualisationId the visualisation identifier
* @param expressionId the id of expression that the visualisation is
* attached to
*/
def remove(
visualisationId: VisualisationId,
expressionId: ExpressionId
): Unit = {
val visualisations = visualisationMap(expressionId)
val removed = visualisations.filterNot(_.id == visualisationId)
visualisationMap += (expressionId -> removed)
}
/**
* Finds all visualisations attached to an expression.
*
* @param expressionId the unique identifier of the expression
* @return a list of matching visualisation
*/
def find(expressionId: ExpressionId): List[Visualisation] =
visualisationMap(expressionId)
/**
* Returns a visualisation with the provided id.
*
* @param visualisationId the identifier of visualisation
* @return an option with visualisation
*/
def getById(visualisationId: VisualisationId): Option[Visualisation] =
visualisationMap.values.flatten.find(_.id == visualisationId)
}
object VisualisationHolder {
/**
* Returns an empty holder.
*/
def empty = new VisualisationHolder()
}

View File

@ -11,6 +11,7 @@ import org.enso.interpreter.instrument.{
}
import org.enso.interpreter.test.Metadata
import org.enso.pkg.Package
import org.enso.polyglot.runtime.Runtime.Api.VisualisationUpdate
import org.enso.polyglot.runtime.Runtime.{Api, ApiRequest}
import org.enso.polyglot.{
LanguageInfo,
@ -77,7 +78,11 @@ class RuntimeServerTest
def writeMain(contents: String): File =
Files.write(pkg.mainFile.toPath, contents.getBytes).toFile
def writeFile(file: File, contents: String): File = {
def writeFile(file: File, contents: String): File =
Files.write(file.toPath, contents.getBytes).toFile
def writeInSrcDir(moduleName: String, contents: String): File = {
val file = new File(pkg.sourceDir, s"$moduleName.enso")
Files.write(file.toPath, contents.getBytes).toFile
}
@ -89,6 +94,19 @@ class RuntimeServerTest
msg
}
def drain(): Unit = {
messageQueue = List.empty
}
def drainAndCollectFirstMatching[A](
pf: PartialFunction[Api.Response, A]
): A = {
val maybeMessage = messageQueue.collectFirst(pf)
maybeMessage.isDefined shouldBe true
drain()
maybeMessage.get
}
def consumeOut: List[String] = {
val result = out.toString
out.reset()
@ -199,6 +217,18 @@ class RuntimeServerTest
}
}
object Visualisation {
val code =
"""
|encode = x -> x.to_text
|
|incAndEncode = x -> here.encode x+1
|
|""".stripMargin
}
object Main2 {
val metadata = new Metadata
@ -358,9 +388,6 @@ class RuntimeServerTest
}
it should "support file modification operations" in {
def send(msg: ApiRequest): Unit =
context.send(Api.Request(UUID.randomUUID(), msg))
val fooFile = new File(context.pkg.sourceDir, "Foo.enso")
val contextId = UUID.randomUUID()
@ -553,4 +580,275 @@ class RuntimeServerTest
)
context.consumeOut shouldEqual List()
}
it should "emit visualisation update when expression is evaluated" in {
val mainFile = context.writeMain(context.Main.code)
val visualisationFile =
context.writeInSrcDir("Visualisation", context.Visualisation.code)
send(
Api.OpenFileNotification(
visualisationFile,
context.Visualisation.code
)
)
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val visualisationId = UUID.randomUUID()
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
context.receive shouldEqual Some(
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
// push main
val item1 = Api.StackItem.ExplicitCall(
Api.MethodPointer(mainFile, "Main", "main"),
None,
Vector()
)
context.send(
Api.Request(requestId, Api.PushContextRequest(contextId, item1))
)
context.drain()
context.send(
Api.Request(
requestId,
Api.AttachVisualisation(
visualisationId,
context.Main.idMainX,
Api.VisualisationConfiguration(
contextId,
"Test.Visualisation",
"x -> here.encode x"
)
)
)
)
context.receive shouldBe Some(
Api.Response(requestId, Api.VisualisationAttached())
)
val expectedExprId = context.Main.idMainX
val data = context.drainAndCollectFirstMatching {
case Api.Response(
None,
Api.VisualisationUpdate(
Api.VisualisationContext(
`visualisationId`,
`contextId`,
`expectedExprId`
),
data
)
) =>
data
}
data.sameElements("6".getBytes) shouldBe true
// recompute
context.send(
Api.Request(requestId, Api.RecomputeContextRequest(contextId, None))
)
val data2 = context.drainAndCollectFirstMatching {
case Api.Response(
None,
Api.VisualisationUpdate(
Api.VisualisationContext(
`visualisationId`,
`contextId`,
`expectedExprId`
),
data
)
) =>
data
}
data2.sameElements("6".getBytes) shouldBe true
}
it should "be able to modify visualisations" in {
val mainFile = context.writeMain(context.Main.code)
val visualisationFile =
context.writeInSrcDir("Visualisation", context.Visualisation.code)
send(
Api.OpenFileNotification(
visualisationFile,
context.Visualisation.code
)
)
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val visualisationId = UUID.randomUUID()
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
context.receive shouldEqual Some(
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
// push main
val item1 = Api.StackItem.ExplicitCall(
Api.MethodPointer(mainFile, "Main", "main"),
None,
Vector()
)
context.send(
Api.Request(requestId, Api.PushContextRequest(contextId, item1))
)
context.drain()
context.send(
Api.Request(
requestId,
Api.AttachVisualisation(
visualisationId,
context.Main.idMainX,
Api.VisualisationConfiguration(
contextId,
"Test.Visualisation",
"x -> here.encode x"
)
)
)
)
context.receive shouldBe Some(
Api.Response(requestId, Api.VisualisationAttached())
)
val expectedExprId = context.Main.idMainX
val data = context.drainAndCollectFirstMatching {
case Api.Response(
None,
Api.VisualisationUpdate(
Api.VisualisationContext(
`visualisationId`,
`contextId`,
`expectedExprId`
),
data
)
) =>
data
}
data.sameElements("6".getBytes) shouldBe true
context.send(
Api.Request(
requestId,
Api.ModifyVisualisation(
visualisationId,
Api.VisualisationConfiguration(
contextId,
"Test.Visualisation",
"x -> here.incAndEncode x"
)
)
)
)
context.receive shouldBe Some(
Api.Response(requestId, Api.VisualisationModified())
)
val dataAfterModification = context.drainAndCollectFirstMatching {
case Api.Response(
None,
Api.VisualisationUpdate(
Api.VisualisationContext(
`visualisationId`,
`contextId`,
`expectedExprId`
),
data
)
) =>
data
}
dataAfterModification.sameElements("7".getBytes) shouldBe true
}
it should "not emit visualisation updates when visualisation is detached" in {
val mainFile = context.writeMain(context.Main.code)
val visualisationFile =
context.writeInSrcDir("Visualisation", context.Visualisation.code)
send(
Api.OpenFileNotification(
visualisationFile,
context.Visualisation.code
)
)
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val visualisationId = UUID.randomUUID()
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
context.receive shouldEqual Some(
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
context.send(
Api.Request(
requestId,
Api.AttachVisualisation(
visualisationId,
context.Main.idMainX,
Api.VisualisationConfiguration(
contextId,
"Test.Visualisation",
"x -> here.encode x"
)
)
)
)
context.receive shouldBe Some(
Api.Response(requestId, Api.VisualisationAttached())
)
// push main
val item1 = Api.StackItem.ExplicitCall(
Api.MethodPointer(mainFile, "Main", "main"),
None,
Vector()
)
context.send(
Api.Request(requestId, Api.PushContextRequest(contextId, item1))
)
context.drain()
context.send(
Api.Request(
requestId,
Api.DetachVisualisation(
contextId,
visualisationId,
context.Main.idMainX
)
)
)
context.receive shouldBe Some(
Api.Response(requestId, Api.VisualisationDetached())
)
// recompute
context.send(
Api.Request(requestId, Api.RecomputeContextRequest(contextId, None))
)
Set.fill(5)(context.receive) shouldEqual Set(
Some(Api.Response(requestId, Api.RecomputeContextResponse(contextId))),
Some(context.Main.Update.mainX(contextId)),
Some(context.Main.Update.mainY(contextId)),
Some(context.Main.Update.mainZ(contextId)),
None
)
}
private def send(msg: ApiRequest): Unit =
context.send(Api.Request(UUID.randomUUID(), msg))
}