diff --git a/engine/runtime/src/main/java/org/enso/interpreter/instrument/IdExecutionInstrument.java b/engine/runtime/src/main/java/org/enso/interpreter/instrument/IdExecutionInstrument.java index 211c2fe303..02de3db27e 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/instrument/IdExecutionInstrument.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/instrument/IdExecutionInstrument.java @@ -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 functionCallCallback; private final Consumer onComputedCallback; private final Consumer onCachedCallback; + private final Consumer 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 functionCallCallback, Consumer onComputedCallback, - Consumer onCachedCallback) { + Consumer onCachedCallback, + Consumer 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 bind( @@ -387,9 +417,10 @@ public class IdExecutionInstrument extends TruffleInstrument { RuntimeCache cache, MethodCallsCache methodCallsCache, UUID nextExecutionItem, - Consumer onComputedCallback, + Consumer functionCallCallback, + Consumer onComputedCallback, Consumer onCachedCallback, - Consumer functionCallCallback) { + Consumer 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 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)); } } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/service/ExecutionService.java b/engine/runtime/src/main/java/org/enso/interpreter/service/ExecutionService.java index 590efac546..4ee78e7b13 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/service/ExecutionService.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/service/ExecutionService.java @@ -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 funCallCallback, Consumer onComputedCallback, Consumer onCachedCallback, - Consumer funCallCallback) + Consumer 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 funCallCallback, Consumer onComputedCallback, Consumer onCachedCallback, - Consumer funCallCallback) + Consumer onExceptionalCallback) throws UnsupportedMessageException, ArityException, UnsupportedTypeException { Optional callMay = context @@ -156,9 +162,10 @@ public class ExecutionService { cache, methodCallsCache, nextExecutionItem, + funCallCallback, onComputedCallback, onCachedCallback, - funCallCallback); + onExceptionalCallback); } /** diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala index 82f370300a..10f0dd5f51 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala @@ -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 diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala index e696d94fbd..8bb2141e23 100644 --- a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala @@ -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(