Handle TailCallException in Runtime Instrument (#1068)

PR implements TailCallException handler
in the IdExecutionInstrument sending
correct value updates.

- implement onReturnExceptional of the 
  runtime instrument
- add onExceptionalCallback to the 
  runtime instrument
This commit is contained in:
Dmitry Bushev 2020-08-07 20:00:32 +03:00 committed by GitHub
parent b2fbf1a848
commit 65dec91bc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 785 additions and 105 deletions

View File

@ -6,12 +6,15 @@ import com.oracle.truffle.api.frame.FrameInstance;
import com.oracle.truffle.api.frame.FrameInstanceVisitor;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.instrumentation.*;
import com.oracle.truffle.api.interop.InteropException;
import com.oracle.truffle.api.interop.InteropLibrary;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.nodes.RootNode;
import org.enso.interpreter.node.EnsoRootNode;
import org.enso.interpreter.node.ExpressionNode;
import org.enso.interpreter.node.MethodRootNode;
import org.enso.interpreter.node.callable.FunctionCallInstrumentationNode;
import org.enso.interpreter.runtime.control.TailCallException;
import org.enso.interpreter.runtime.tag.IdentifiedTag;
import org.enso.interpreter.runtime.type.Types;
import org.enso.pkg.QualifiedName;
@ -104,14 +107,22 @@ public class IdExecutionInstrument extends TruffleInstrument {
@Override
public String toString() {
return "ExpressionValue{" +
"expressionId=" + expressionId +
", value=" + value +
", type='" + type + '\'' +
", cachedType='" + cachedType + '\'' +
", callInfo=" + callInfo +
", cachedCallInfo=" + cachedCallInfo +
'}';
return "ExpressionValue{"
+ "expressionId="
+ expressionId
+ ", value="
+ value
+ ", type='"
+ type
+ '\''
+ ", cachedType='"
+ cachedType
+ '\''
+ ", callInfo="
+ callInfo
+ ", cachedCallInfo="
+ cachedCallInfo
+ '}';
}
/** @return the id of the expression computed. */
@ -184,9 +195,9 @@ public class IdExecutionInstrument extends TruffleInstrument {
return false;
}
FunctionCallInfo that = (FunctionCallInfo) o;
return Objects.equals(moduleName, that.moduleName) &&
Objects.equals(typeName, that.typeName) &&
functionName.equals(that.functionName);
return Objects.equals(moduleName, that.moduleName)
&& Objects.equals(typeName, that.typeName)
&& functionName.equals(that.functionName);
}
@Override
@ -221,6 +232,7 @@ public class IdExecutionInstrument extends TruffleInstrument {
private final Consumer<ExpressionCall> functionCallCallback;
private final Consumer<ExpressionValue> onComputedCallback;
private final Consumer<ExpressionValue> onCachedCallback;
private final Consumer<Throwable> onExceptionalCallback;
private final RuntimeCache cache;
private final MethodCallsCache callsCache;
private final UUID nextExecutionItem;
@ -236,6 +248,7 @@ public class IdExecutionInstrument extends TruffleInstrument {
* @param functionCallCallback the consumer of function call events.
* @param onComputedCallback the consumer of the computed value events.
* @param onCachedCallback the consumer of the cached value events.
* @param onExceptionalCallback the consumer of the exceptional events.
*/
public IdExecutionEventListener(
CallTarget entryCallTarget,
@ -244,7 +257,8 @@ public class IdExecutionInstrument extends TruffleInstrument {
UUID nextExecutionItem,
Consumer<ExpressionCall> functionCallCallback,
Consumer<ExpressionValue> onComputedCallback,
Consumer<ExpressionValue> onCachedCallback) {
Consumer<ExpressionValue> onCachedCallback,
Consumer<Throwable> onExceptionalCallback) {
this.entryCallTarget = entryCallTarget;
this.cache = cache;
this.callsCache = methodCallsCache;
@ -252,6 +266,7 @@ public class IdExecutionInstrument extends TruffleInstrument {
this.functionCallCallback = functionCallCallback;
this.onComputedCallback = onComputedCallback;
this.onCachedCallback = onCachedCallback;
this.onExceptionalCallback = onExceptionalCallback;
}
@Override
@ -332,8 +347,22 @@ public class IdExecutionInstrument extends TruffleInstrument {
}
@Override
public void onReturnExceptional(
EventContext context, VirtualFrame frame, Throwable exception) {}
public void onReturnExceptional(EventContext context, VirtualFrame frame, Throwable exception) {
if (exception instanceof TailCallException) {
try {
TailCallException tailCallException = (TailCallException) exception;
FunctionCallInstrumentationNode.FunctionCall functionCall =
new FunctionCallInstrumentationNode.FunctionCall(
tailCallException.getFunction(),
tailCallException.getState(),
tailCallException.getArguments());
Object result = InteropLibrary.getFactory().getUncached().execute(functionCall);
onReturnValue(context, frame, result);
} catch (InteropException e) {
onExceptionalCallback.accept(e);
}
}
}
/**
* Checks if we're not inside a recursive call, i.e. the {@link #entryCallTarget} only appears
@ -375,9 +404,10 @@ public class IdExecutionInstrument extends TruffleInstrument {
* @param cache the precomputed expression values.
* @param methodCallsCache the storage tracking the executed method calls.
* @param nextExecutionItem the next item scheduled for execution.
* @param functionCallCallback the consumer of function call events.
* @param onComputedCallback the consumer of the computed value events.
* @param onCachedCallback the consumer of the cached value events.
* @param functionCallCallback the consumer of function call events.
* @param onExceptionalCallback the consumer of the exceptional events.
* @return a reference to the attached event listener.
*/
public EventBinding<ExecutionEventListener> bind(
@ -387,9 +417,10 @@ public class IdExecutionInstrument extends TruffleInstrument {
RuntimeCache cache,
MethodCallsCache methodCallsCache,
UUID nextExecutionItem,
Consumer<ExpressionValue> onComputedCallback,
Consumer<IdExecutionInstrument.ExpressionCall> functionCallCallback,
Consumer<IdExecutionInstrument.ExpressionValue> onComputedCallback,
Consumer<IdExecutionInstrument.ExpressionValue> onCachedCallback,
Consumer<ExpressionCall> functionCallCallback) {
Consumer<Throwable> onExceptionalCallback) {
SourceSectionFilter filter =
SourceSectionFilter.newBuilder()
.tagIs(StandardTags.ExpressionTag.class, StandardTags.CallTag.class)
@ -397,18 +428,17 @@ public class IdExecutionInstrument extends TruffleInstrument {
.indexIn(funSourceStart, funSourceLength)
.build();
EventBinding<ExecutionEventListener> binding =
env.getInstrumenter()
.attachExecutionEventListener(
filter,
new IdExecutionEventListener(
entryCallTarget,
cache,
methodCallsCache,
nextExecutionItem,
functionCallCallback,
onComputedCallback,
onCachedCallback));
return binding;
return env.getInstrumenter()
.attachExecutionEventListener(
filter,
new IdExecutionEventListener(
entryCallTarget,
cache,
methodCallsCache,
nextExecutionItem,
functionCallCallback,
onComputedCallback,
onCachedCallback,
onExceptionalCallback));
}
}

View File

@ -15,6 +15,7 @@ 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.interpreter.runtime.state.data.EmptyMap;
import org.enso.polyglot.LanguageInfo;
import org.enso.polyglot.MethodNames;
import org.enso.text.buffer.Rope;
@ -75,7 +76,7 @@ public class ExecutionService {
}
FunctionCallInstrumentationNode.FunctionCall call =
new FunctionCallInstrumentationNode.FunctionCall(
function, context.getBuiltins().unit(), new Object[] {atomConstructor.newInstance()});
function, EmptyMap.create(), new Object[] {atomConstructor.newInstance()});
return Optional.of(call);
}
@ -86,18 +87,20 @@ public class ExecutionService {
* @param cache the precomputed expression values.
* @param methodCallsCache the storage tracking the executed method calls.
* @param nextExecutionItem the next item scheduled for execution.
* @param funCallCallback the consumer for function call events.
* @param onComputedCallback the consumer of the computed value events.
* @param onCachedCallback the consumer of the cached value events.
* @param funCallCallback the consumer for function call events.
* @param onExceptionalCallback the consumer of the exceptional events.
*/
public void execute(
FunctionCallInstrumentationNode.FunctionCall call,
RuntimeCache cache,
MethodCallsCache methodCallsCache,
UUID nextExecutionItem,
Consumer<IdExecutionInstrument.ExpressionCall> funCallCallback,
Consumer<IdExecutionInstrument.ExpressionValue> onComputedCallback,
Consumer<IdExecutionInstrument.ExpressionValue> onCachedCallback,
Consumer<IdExecutionInstrument.ExpressionCall> funCallCallback)
Consumer<Throwable> onExceptionalCallback)
throws UnsupportedMessageException, ArityException, UnsupportedTypeException {
SourceSection src = call.getFunction().getSourceSection();
@ -112,9 +115,10 @@ public class ExecutionService {
cache,
methodCallsCache,
nextExecutionItem,
funCallCallback,
onComputedCallback,
onCachedCallback,
funCallCallback);
onExceptionalCallback);
interopLibrary.execute(call);
listener.dispose();
}
@ -129,9 +133,10 @@ public class ExecutionService {
* @param cache the precomputed expression values.
* @param methodCallsCache the storage tracking the executed method calls.
* @param nextExecutionItem the next item scheduled for execution.
* @param funCallCallback the consumer for function call events.
* @param onComputedCallback the consumer of the computed value events.
* @param onCachedCallback the consumer of the cached value events.
* @param funCallCallback the consumer for function call events.
* @param onExceptionalCallback the consumer of the exceptional events.
*/
public void execute(
String moduleName,
@ -140,9 +145,10 @@ public class ExecutionService {
RuntimeCache cache,
MethodCallsCache methodCallsCache,
UUID nextExecutionItem,
Consumer<IdExecutionInstrument.ExpressionCall> funCallCallback,
Consumer<IdExecutionInstrument.ExpressionValue> onComputedCallback,
Consumer<IdExecutionInstrument.ExpressionValue> onCachedCallback,
Consumer<IdExecutionInstrument.ExpressionCall> funCallCallback)
Consumer<Throwable> onExceptionalCallback)
throws UnsupportedMessageException, ArityException, UnsupportedTypeException {
Optional<FunctionCallInstrumentationNode.FunctionCall> callMay =
context
@ -156,9 +162,10 @@ public class ExecutionService {
cache,
methodCallsCache,
nextExecutionItem,
funCallCallback,
onComputedCallback,
onCachedCallback,
funCallCallback);
onExceptionalCallback);
}
/**

View File

@ -5,6 +5,8 @@ import java.util.function.Consumer
import java.util.logging.Level
import cats.implicits._
import com.oracle.truffle.api.TruffleException
import com.oracle.truffle.api.interop.InteropException
import org.enso.interpreter.instrument.IdExecutionInstrument.{
ExpressionCall,
ExpressionValue
@ -39,6 +41,7 @@ trait ProgramExecutionSupport {
* @param cachedMethodCallsCallback a listener for cached method calls
* @param onComputedCallback a listener of computed values
* @param onCachedCallback a listener of cached values
* @param onExceptionalCallback the consumer of the exceptional events.
*/
@scala.annotation.tailrec
final private def executeProgram(
@ -46,7 +49,8 @@ trait ProgramExecutionSupport {
callStack: List[LocalCallFrame],
cachedMethodCallsCallback: Consumer[ExpressionValue],
onComputedCallback: Consumer[ExpressionValue],
onCachedCallback: Consumer[ExpressionValue]
onCachedCallback: Consumer[ExpressionValue],
onExceptionalCallback: Consumer[Throwable]
)(implicit ctx: RuntimeContext): Unit = {
val methodCallsCache = new MethodCallsCache
var enterables = Map[UUID, FunctionCall]()
@ -68,9 +72,10 @@ trait ProgramExecutionSupport {
cache,
methodCallsCache,
callStack.headOption.map(_.expressionId).orNull,
callablesCallback,
computedCallback,
onCachedCallback,
callablesCallback
onExceptionalCallback
)
case ExecutionFrame(ExecutionItem.CallData(callData), cache) =>
ctx.executionService.execute(
@ -78,9 +83,10 @@ trait ProgramExecutionSupport {
cache,
methodCallsCache,
callStack.headOption.map(_.expressionId).orNull,
callablesCallback,
computedCallback,
onCachedCallback,
callablesCallback
onExceptionalCallback
)
}
@ -108,7 +114,8 @@ trait ProgramExecutionSupport {
tail,
cachedMethodCallsCallback,
onComputedCallback,
onCachedCallback
onCachedCallback,
onExceptionalCallback
)
case None =>
()
@ -171,9 +178,14 @@ trait ProgramExecutionSupport {
fireVisualisationUpdates(contextId, value)
}
val onExceptionalCallback: Consumer[Throwable] = { value =>
ctx.executionService.getLogger.finer(s"ON_ERROR $value")
sendErrorUpdate(contextId, value)
}
val (explicitCallOpt, localCalls) = unwind(stack, Nil, Nil)
for {
stackItem <- Either.fromOption(explicitCallOpt, "stack is empty")
stackItem <- Either.fromOption(explicitCallOpt, "Stack is empty.")
_ <-
Either
.catchNonFatal(
@ -182,20 +194,38 @@ trait ProgramExecutionSupport {
localCalls,
cachedMethodCallsCallback,
onComputedValueCallback,
onCachedValueCallback
onCachedValueCallback,
onExceptionalCallback
)
)
.leftMap { ex =>
ctx.executionService.getLogger.log(
Level.FINE,
s"Error executing a function '${getName(stackItem.item)}'",
s"Error executing a function '${getName(stackItem.item)}.'",
ex
)
s"error in function: ${getName(stackItem.item)}"
getErrorMessage(ex)
.getOrElse(s"Error in function ${getName(stackItem.item)}.")
}
} yield ()
}
/** Get error message from throwable. */
private def getErrorMessage(t: Throwable): Option[String] =
t match {
case ex: TruffleException => Some(ex.getMessage)
case ex: InteropException => Some(ex.getMessage)
case _ => None
}
private def sendErrorUpdate(contextId: ContextId, error: Throwable)(implicit
ctx: RuntimeContext
): Unit = {
ctx.endpoint.sendToClient(
Api.Response(Api.ExecutionFailed(contextId, error.getMessage))
)
}
private def sendMethodPointerUpdate(
contextId: ContextId,
value: ExpressionValue

View File

@ -251,19 +251,6 @@ class RuntimeServerTest
}
}
object MainWithError {
val metadata = new Metadata
val idMain = metadata.addItem(8, 6)
val code = metadata.appendToCode(
"""
|main = 1 + 2L
|""".stripMargin
)
}
object Visualisation {
val code =
@ -378,19 +365,20 @@ class RuntimeServerTest
)
}
it should "send updates when the type is changed" in {
it should "send updates from last line" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val moduleName = "Test.Main"
val metadata = new Metadata
val idResult = metadata.addItem(20, 4)
val idPrintln = metadata.addItem(29, 17)
val idMain = metadata.addItem(6, 40)
val idMain = metadata.addItem(23, 17)
val idMainFoo = metadata.addItem(28, 12)
val code =
"""main =
| result = 1337
| IO.println result
"""foo a b = a + b
|
|main =
| this.foo 1 2
|""".stripMargin.linesIterator.mkString("\n")
val contents = metadata.appendToCode(code)
val mainFile = context.writeMain(contents)
@ -421,18 +409,218 @@ class RuntimeServerTest
)
)
)
context.receive(6) should contain theSameElementsAs Seq(
context.receive(4) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.PushContextResponse(contextId)),
Api.Response(
Api.ExpressionValuesComputed(
contextId,
Vector(Api.ExpressionValueUpdate(idResult, Some("Number"), None))
Vector(
Api.ExpressionValueUpdate(
idMainFoo,
Some("Number"),
Some(Api.MethodPointer(moduleName, "Main", "foo"))
)
)
)
),
Api.Response(
Api.ExpressionValuesComputed(
contextId,
Vector(Api.ExpressionValueUpdate(idPrintln, Some("Unit"), None))
Vector(Api.ExpressionValueUpdate(idMain, Some("Number"), None))
)
),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
moduleName,
Seq(
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
moduleName,
"foo",
Seq(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("a", "Any", false, false, None),
Suggestion.Argument("b", "Any", false, false, None)
),
"here",
"Any",
None
)
),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
Some(idMain),
moduleName,
"main",
List(Suggestion.Argument("this", "Any", false, false, None)),
"here",
"Any",
None
)
)
)
)
)
)
}
it should "compute side effects correctly from last line" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val moduleName = "Test.Main"
val metadata = new Metadata
val idMain = metadata.addItem(23, 30)
val idMainFoo = metadata.addItem(40, 12)
val code =
"""foo a b = a + b
|
|main =
| IO.println (this.foo 1 2)
|""".stripMargin.linesIterator.mkString("\n")
val contents = metadata.appendToCode(code)
val mainFile = context.writeMain(contents)
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
context.receive shouldEqual Some(
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
// open file
context.send(
Api.Request(Api.OpenFileNotification(mainFile, contents, false))
)
context.receive shouldEqual None
// push main
context.send(
Api.Request(
requestId,
Api.PushContextRequest(
contextId,
Api.StackItem.ExplicitCall(
Api.MethodPointer(moduleName, "Main", "main"),
None,
Vector()
)
)
)
)
context.receive(5) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.PushContextResponse(contextId)),
Api.Response(
Api.ExpressionValuesComputed(
contextId,
Vector(
Api.ExpressionValueUpdate(
idMainFoo,
Some("Number"),
Some(Api.MethodPointer(moduleName, "Main", "foo"))
)
)
)
),
Api.Response(
Api.ExpressionValuesComputed(
contextId,
Vector(Api.ExpressionValueUpdate(idMain, Some("Unit"), None))
)
),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
moduleName,
Seq(
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
moduleName,
"foo",
Seq(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("a", "Any", false, false, None),
Suggestion.Argument("b", "Any", false, false, None)
),
"here",
"Any",
None
)
),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
Some(idMain),
moduleName,
"main",
List(Suggestion.Argument("this", "Any", false, false, None)),
"here",
"Any",
None
)
)
)
)
)
)
context.consumeOut shouldEqual List("3")
}
it should "Run State getting the initial state" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val moduleName = "Test.Main"
val metadata = new Metadata
val idMain = metadata.addItem(7, 41)
val idMainBar = metadata.addItem(39, 8)
val code =
"""main = IO.println (State.run Number 42 this.bar)
|
|bar = State.get Number
|""".stripMargin
val contents = metadata.appendToCode(code)
val mainFile = context.writeMain(contents)
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
context.receive shouldEqual Some(
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
// open file
context.send(
Api.Request(Api.OpenFileNotification(mainFile, contents, false))
)
context.receive shouldEqual None
// push main
context.send(
Api.Request(
requestId,
Api.PushContextRequest(
contextId,
Api.StackItem.ExplicitCall(
Api.MethodPointer(moduleName, "Main", "main"),
None,
Vector()
)
)
)
)
context.receive(4) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.PushContextResponse(contextId)),
Api.Response(
Api.ExpressionValuesComputed(
contextId,
Vector(
Api.ExpressionValueUpdate(
idMainBar,
Some("Number"),
Some(Api.MethodPointer(moduleName, "Main", "bar"))
)
)
)
),
Api.Response(
@ -457,46 +645,220 @@ class RuntimeServerTest
)
),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Local(
Some(idResult),
Suggestion.Method(
None,
moduleName,
"result",
"bar",
Seq(Suggestion.Argument("this", "Any", false, false, None)),
"here",
"Any",
Suggestion.Scope(
Suggestion.Position(0, 6),
Suggestion.Position(2, 21)
)
None
)
)
)
)
)
)
context.consumeOut shouldEqual List("1337")
context.consumeOut shouldEqual List("42")
}
// Modify the file
it should "Run State setting the state" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val moduleName = "Test.Main"
val metadata = new Metadata
val idMain = metadata.addItem(7, 40)
val idMainBar = metadata.addItem(38, 8)
val code =
"""main = IO.println (State.run Number 0 this.bar)
|
|bar =
| State.put Number 10
| State.get Number
|""".stripMargin
val contents = metadata.appendToCode(code)
val mainFile = context.writeMain(contents)
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
context.receive shouldEqual Some(
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
// open file
context.send(
Api.Request(Api.OpenFileNotification(mainFile, contents, false))
)
context.receive shouldEqual None
// push main
context.send(
Api.Request(
Api.EditFileNotification(
mainFile,
requestId,
Api.PushContextRequest(
contextId,
Api.StackItem.ExplicitCall(
Api.MethodPointer(moduleName, "Main", "main"),
None,
Vector()
)
)
)
)
context.receive(4) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.PushContextResponse(contextId)),
Api.Response(
Api.ExpressionValuesComputed(
contextId,
Vector(
Api.ExpressionValueUpdate(
idMainBar,
Some("Number"),
Some(Api.MethodPointer(moduleName, "Main", "bar"))
)
)
)
),
Api.Response(
Api.ExpressionValuesComputed(
contextId,
Vector(Api.ExpressionValueUpdate(idMain, Some("Unit"), None))
)
),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
moduleName,
Seq(
TextEdit(
model.Range(model.Position(1, 13), model.Position(1, 17)),
"\"Hi\""
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
Some(idMain),
moduleName,
"main",
Seq(Suggestion.Argument("this", "Any", false, false, None)),
"here",
"Any",
None
)
),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
moduleName,
"bar",
Seq(Suggestion.Argument("this", "Any", false, false, None)),
"here",
"Any",
None
)
)
)
)
)
)
context.receive(2) should contain theSameElementsAs Seq(
Api.Response(
Api.ExpressionValuesComputed(
context.consumeOut shouldEqual List("10")
}
it should "send updates of a function call" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val moduleName = "Test.Main"
val metadata = new Metadata
val idMain = metadata.addItem(23, 23)
val idMainFoo = metadata.addItem(28, 12)
val code =
"""foo a b = a + b
|
|main =
| this.foo 1 2
| 1
|""".stripMargin.linesIterator.mkString("\n")
val contents = metadata.appendToCode(code)
val mainFile = context.writeMain(contents)
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
context.receive shouldEqual Some(
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
// open file
context.send(
Api.Request(Api.OpenFileNotification(mainFile, contents, false))
)
context.receive shouldEqual None
// push main
context.send(
Api.Request(
requestId,
Api.PushContextRequest(
contextId,
Vector(Api.ExpressionValueUpdate(idResult, Some("Text"), None))
Api.StackItem.ExplicitCall(
Api.MethodPointer(moduleName, "Main", "main"),
None,
Vector()
)
)
)
)
context.receive(4) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.PushContextResponse(contextId)),
Api.Response(
Api.ExpressionValuesComputed(
contextId,
Vector(
Api.ExpressionValueUpdate(
idMainFoo,
Some("Number"),
Some(Api.MethodPointer(moduleName, "Main", "foo"))
)
)
)
),
Api.Response(
Api.ExpressionValuesComputed(
contextId,
Vector(Api.ExpressionValueUpdate(idMain, Some("Number"), None))
)
),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
moduleName,
Seq(
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
moduleName,
"foo",
Seq(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("a", "Any", false, false, None),
Suggestion.Argument("b", "Any", false, false, None)
),
"here",
"Any",
None
)
),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
Some(idMain),
moduleName,
"main",
List(Suggestion.Argument("this", "Any", false, false, None)),
"here",
"Any",
None
)
)
)
)
)
)
context.consumeOut shouldEqual List("Hi")
}
it should "not send updates when the type is not changed" in {
@ -668,6 +1030,127 @@ class RuntimeServerTest
)
}
it should "send updates when the type is changed" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val moduleName = "Test.Main"
val metadata = new Metadata
val idResult = metadata.addItem(20, 4)
val idPrintln = metadata.addItem(29, 17)
val idMain = metadata.addItem(6, 40)
val code =
"""main =
| result = 1337
| IO.println result
|""".stripMargin.linesIterator.mkString("\n")
val contents = metadata.appendToCode(code)
val mainFile = context.writeMain(contents)
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
context.receive shouldEqual Some(
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
// open file
context.send(
Api.Request(Api.OpenFileNotification(mainFile, contents, false))
)
context.receive shouldEqual None
// push main
context.send(
Api.Request(
requestId,
Api.PushContextRequest(
contextId,
Api.StackItem.ExplicitCall(
Api.MethodPointer(moduleName, "Main", "main"),
None,
Vector()
)
)
)
)
context.receive(6) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.PushContextResponse(contextId)),
Api.Response(
Api.ExpressionValuesComputed(
contextId,
Vector(Api.ExpressionValueUpdate(idResult, Some("Number"), None))
)
),
Api.Response(
Api.ExpressionValuesComputed(
contextId,
Vector(Api.ExpressionValueUpdate(idPrintln, Some("Unit"), None))
)
),
Api.Response(
Api.ExpressionValuesComputed(
contextId,
Vector(Api.ExpressionValueUpdate(idMain, Some("Unit"), None))
)
),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
moduleName,
Seq(
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
Some(idMain),
moduleName,
"main",
Seq(Suggestion.Argument("this", "Any", false, false, None)),
"here",
"Any",
None
)
),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Local(
Some(idResult),
moduleName,
"result",
"Any",
Suggestion.Scope(
Suggestion.Position(0, 6),
Suggestion.Position(2, 21)
)
)
)
)
)
)
)
context.consumeOut shouldEqual List("1337")
// Modify the file
context.send(
Api.Request(
Api.EditFileNotification(
mainFile,
Seq(
TextEdit(
model.Range(model.Position(1, 13), model.Position(1, 17)),
"\"Hi\""
)
)
)
)
)
context.receive(2) should contain theSameElementsAs Seq(
Api.Response(
Api.ExpressionValuesComputed(
contextId,
Vector(Api.ExpressionValueUpdate(idResult, Some("Text"), None))
)
)
)
context.consumeOut shouldEqual List("Hi")
}
it should "send updates when the method pointer is changed" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
@ -1562,10 +2045,18 @@ class RuntimeServerTest
)
}
it should "return error when computing erroneous code" in {
context.writeMain(context.MainWithError.code)
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
it should "return error not invocable" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val moduleName = "Test.Main"
val metadata = new Metadata
val code =
"""main = this.bar 40 2 9
|
|bar x y = x + y
|""".stripMargin.linesIterator.mkString("\n")
val contents = metadata.appendToCode(code)
context.writeMain(contents)
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
@ -1574,26 +2065,148 @@ class RuntimeServerTest
)
// push main
val item1 = Api.StackItem.ExplicitCall(
Api.MethodPointer("Test.Main", "Main", "main"),
None,
Vector()
)
context.send(
Api.Request(requestId, Api.PushContextRequest(contextId, item1))
Api.Request(
requestId,
Api.PushContextRequest(
contextId,
Api.StackItem.ExplicitCall(
Api.MethodPointer(moduleName, "Main", "main"),
None,
Vector()
)
)
)
)
context.receive(2) should contain theSameElementsAs Seq(
context.receive(3) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.PushContextResponse(contextId)),
Api.Response(Api.ExecutionFailed(contextId, "error in function: main"))
Api.Response(
Api.ExecutionFailed(contextId, "Object 42 is not invokable.")
)
)
}
it should "return error in function main" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val moduleName = "Test.Main"
val metadata = new Metadata
val code =
"""main = this.bar x y
|
|bar x y = x + y
|""".stripMargin.linesIterator.mkString("\n")
val contents = metadata.appendToCode(code)
context.writeMain(contents)
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
context.receive shouldEqual Some(
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
// recompute
// push main
context.send(
Api.Request(requestId, Api.RecomputeContextRequest(contextId, None))
Api.Request(
requestId,
Api.PushContextRequest(
contextId,
Api.StackItem.ExplicitCall(
Api.MethodPointer(moduleName, "Main", "main"),
None,
Vector()
)
)
)
)
context.receive(2) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.RecomputeContextResponse(contextId)),
Api.Response(Api.ExecutionFailed(contextId, "error in function: main"))
context.receive(3) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.PushContextResponse(contextId)),
Api.Response(Api.ExecutionFailed(contextId, "Error in function main."))
)
}
it should "return error unexpected type" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val moduleName = "Test.Main"
val metadata = new Metadata
val code =
"""main = this.bar "one" 2
|
|bar x y = x + y
|""".stripMargin.linesIterator.mkString("\n")
val contents = metadata.appendToCode(code)
context.writeMain(contents)
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
context.receive shouldEqual Some(
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
// push main
context.send(
Api.Request(
requestId,
Api.PushContextRequest(
contextId,
Api.StackItem.ExplicitCall(
Api.MethodPointer(moduleName, "Main", "main"),
None,
Vector()
)
)
)
)
context.receive(3) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.PushContextResponse(contextId)),
Api.Response(
Api.ExecutionFailed(
contextId,
"Unexpected type provided for argument `that` in Text.+"
)
)
)
}
it should "return error method does not exist" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val moduleName = "Test.Main"
val metadata = new Metadata
val code = "main = Number.pi"
val contents = metadata.appendToCode(code)
context.writeMain(contents)
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
context.receive shouldEqual Some(
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
// push main
context.send(
Api.Request(
requestId,
Api.PushContextRequest(
contextId,
Api.StackItem.ExplicitCall(
Api.MethodPointer(moduleName, "Main", "main"),
None,
Vector()
)
)
)
)
context.receive(3) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.PushContextResponse(contextId)),
Api.Response(
Api.ExecutionFailed(
contextId,
"Object Number does not define method pi."
)
)
)
}
@ -2107,7 +2720,7 @@ class RuntimeServerTest
)
context.receive(3) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.VisualisationAttached()),
Api.Response(Api.ExecutionFailed(contextId, "stack is empty"))
Api.Response(Api.ExecutionFailed(contextId, "Stack is empty."))
)
// push main
val item1 = Api.StackItem.ExplicitCall(