diff --git a/engine/runtime-instrument-common/src/main/java/org/enso/interpreter/service/ExecutionService.java b/engine/runtime-instrument-common/src/main/java/org/enso/interpreter/service/ExecutionService.java index 0bd15e87cbbe..a48e231c2bec 100644 --- a/engine/runtime-instrument-common/src/main/java/org/enso/interpreter/service/ExecutionService.java +++ b/engine/runtime-instrument-common/src/main/java/org/enso/interpreter/service/ExecutionService.java @@ -406,7 +406,7 @@ public void modifyModuleSources( rope -> { logger.log( Level.FINE, - "Applied edits. Source has {} lines, last line has {} characters", + "Applied edits. Source has {0} lines, last line has {1} characters.", new Object[] { rope.lines().length(), rope.lines().drop(rope.lines().length() - 1).characters().length() diff --git a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/job/EnsureCompiledJob.scala b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/job/EnsureCompiledJob.scala index 8e8f69ad296f..c9338ed61c41 100644 --- a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/job/EnsureCompiledJob.scala +++ b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/job/EnsureCompiledJob.scala @@ -5,11 +5,18 @@ import com.oracle.truffle.api.TruffleLogger import org.enso.compiler.CompilerResult import org.enso.compiler.context._ import org.enso.compiler.core.Implicits.AsMetadata -import org.enso.compiler.core.IR -import org.enso.compiler.core.ir.{Diagnostic, IdentifiedLocation, Warning} +import org.enso.compiler.core.{ExternalID, IR} +import org.enso.compiler.core.ir.{ + expression, + Diagnostic, + IdentifiedLocation, + Warning +} import org.enso.compiler.core.ir.expression.Error +import org.enso.compiler.data.BindingsMap import org.enso.compiler.pass.analyse.{ CachePreferenceAnalysis, + DataflowAnalysis, GatherDiagnostics } import org.enso.interpreter.instrument.execution.{ @@ -30,7 +37,9 @@ import org.enso.polyglot.runtime.Runtime.Api.StackItem import org.enso.text.buffer.Rope import java.io.File +import java.util.UUID import java.util.logging.Level + import scala.jdk.OptionConverters._ /** A job that ensures that specified files are compiled. @@ -307,14 +316,15 @@ final class EnsureCompiledJob( ctx.locking.releasePendingEditsLock() logger.log( Level.FINEST, - s"Kept pending edits lock [EnsureCompiledJob] for ${System.currentTimeMillis() - pendingEditsLockTimestamp} milliseconds" + "Kept pending edits lock [EnsureCompiledJob] for {} milliseconds", + System.currentTimeMillis() - pendingEditsLockTimestamp ) ctx.locking.releaseFileLock(file) logger.log( Level.FINEST, - s"Kept file lock [EnsureCompiledJob] for ${System.currentTimeMillis() - fileLockTimestamp} milliseconds" + "Kept file lock [EnsureCompiledJob] for {} milliseconds", + System.currentTimeMillis() - fileLockTimestamp ) - } } @@ -329,9 +339,10 @@ final class EnsureCompiledJob( changeset: Changeset[_], ir: IR ): Seq[CacheInvalidation] = { + val resolutionErrors = findNodesWithResolutionErrors(ir) val invalidateExpressionsCommand = CacheInvalidation.Command.InvalidateKeys( - changeset.invalidated + changeset.invalidated ++ resolutionErrors ) val moduleIds = ir.preorder().flatMap(_.location()).flatMap(_.id()).toSet val invalidateStaleCommand = @@ -350,6 +361,40 @@ final class EnsureCompiledJob( ) } + /** Looks for the nodes with the resolution error and their dependents. + * + * @param ir the module IR + * @return the set of node ids affected by a resolution error in the module + */ + private def findNodesWithResolutionErrors(ir: IR): Set[UUID @ExternalID] = { + val metadata = ir + .unsafeGetMetadata( + DataflowAnalysis, + "Empty dataflow analysis metadata during the interactive compilation." + ) + + val resolutionNotFoundKeys = + ir.preorder() + .collect { + case err @ expression.errors.Resolution( + _, + expression.errors.Resolution + .ResolverError(BindingsMap.ResolutionNotFound), + _, + _ + ) => + DataflowAnalysis.DependencyInfo.Type.Static( + err.getId, + err.getExternalId + ) + } + .toSet + + resolutionNotFoundKeys.flatMap( + metadata.dependents.getExternal(_).getOrElse(Set()) + ) + } + /** Run the invalidation commands. * * @param module the compiled module diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/ApplicationNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/ApplicationNode.java index e19f08ba4d12..622f0d65526c 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/ApplicationNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/ApplicationNode.java @@ -75,7 +75,7 @@ public void setTailStatus(TailStatus isTail) { * @return the results of evaluating the function arguments */ @ExplodeLoop - public Object[] evaluateArguments(VirtualFrame frame) { + private Object[] evaluateArguments(VirtualFrame frame) { Object[] computedArguments = new Object[this.argExpressions.length]; for (int i = 0; i < this.argExpressions.length; ++i) { @@ -93,8 +93,9 @@ public Object[] evaluateArguments(VirtualFrame frame) { @Override public Object executeGeneric(VirtualFrame frame) { State state = Function.ArgumentsHelper.getState(frame.getArguments()); + Object[] evaluatedArguments = evaluateArguments(frame); return this.invokeCallableNode.execute( - this.callable.executeGeneric(frame), frame, state, evaluateArguments(frame)); + this.callable.executeGeneric(frame), frame, state, evaluatedArguments); } /** diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/Type.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/Type.java index c8a35de2130f..de86a2c3e0e3 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/Type.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/Type.java @@ -345,6 +345,7 @@ public boolean isEigenType() { public void registerConstructor(AtomConstructor constructor) { constructors.put(constructor.getName(), constructor); + gettersGenerated = false; } public Map getConstructors() { diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/ModuleScope.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/ModuleScope.java index 09c047355532..b076a5986588 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/ModuleScope.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/ModuleScope.java @@ -71,8 +71,9 @@ public ModuleScope( this.exports = exports; } - public void registerType(Type type) { - types.put(type.getName(), type); + public Type registerType(Type type) { + Type current = types.putIfAbsent(type.getName(), type); + return current == null ? type : current; } /** @@ -109,7 +110,7 @@ private Map> ensureMethodMapFor(Type type) { private Map> getMethodMapFor(Type type) { Type tpeKey = type == null ? noTypeKey : type; - Map> result = methods.get(type); + Map> result = methods.get(tpeKey); if (result == null) { return new HashMap<>(); } @@ -396,7 +397,6 @@ public void reset() { imports = new HashSet<>(); exports = new HashSet<>(); methods = new HashMap<>(); - types = new HashMap<>(); conversions = new HashMap<>(); polyglotSymbols = new HashMap<>(); } diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/runtime/RuntimeStubsGenerator.scala b/engine/runtime/src/main/scala/org/enso/interpreter/runtime/RuntimeStubsGenerator.scala index fef4b37aaf47..4a0d552f8220 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/runtime/RuntimeStubsGenerator.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/runtime/RuntimeStubsGenerator.scala @@ -48,12 +48,13 @@ class RuntimeStubsGenerator(builtins: Builtins) { scope.registerType(builtinType.getType) builtinType.getType.setShadowDefinitions(scope, true) } else { - val rtp = if (tp.members.nonEmpty || tp.builtinType) { - Type.create(tp.name, scope, builtins.any(), builtins.any(), false) - } else { - Type.createSingleton(tp.name, scope, builtins.any(), false) - } - scope.registerType(rtp) + val createdType = + if (tp.members.nonEmpty || tp.builtinType) { + Type.create(tp.name, scope, builtins.any(), builtins.any(), false) + } else { + Type.createSingleton(tp.name, scope, builtins.any(), false) + } + val rtp = scope.registerType(createdType) tp.members.foreach { cons => val constructor = new AtomConstructor(cons.name, scope, rtp) rtp.registerConstructor(constructor) diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeTypesTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeTypesTest.scala new file mode 100644 index 000000000000..488fcca3f58e --- /dev/null +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeTypesTest.scala @@ -0,0 +1,533 @@ +package org.enso.interpreter.test.instrument + +import org.apache.commons.io.output.TeeOutputStream +import org.enso.interpreter.runtime.EnsoContext +import org.enso.interpreter.runtime.`type`.ConstantsGen +import org.enso.interpreter.test.Metadata +import org.enso.polyglot._ +import org.enso.polyglot.runtime.Runtime.Api +import org.enso.text.editing.model +import org.enso.text.editing.model.TextEdit +import org.graalvm.polyglot.Context +import org.scalatest.BeforeAndAfterEach +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import java.io.{ByteArrayOutputStream, File} +import java.nio.file.{Files, Paths} +import java.util.UUID + +@scala.annotation.nowarn("msg=multiarg infix syntax") +class RuntimeTypesTest + extends AnyFlatSpec + with Matchers + with BeforeAndAfterEach { + + // === Test Utilities ======================================================= + + var context: TestContext = _ + + class TestContext(packageName: String) + extends InstrumentTestContext(packageName) { + + val out: ByteArrayOutputStream = new ByteArrayOutputStream() + val logOut: ByteArrayOutputStream = new ByteArrayOutputStream() + val context = + Context + .newBuilder(LanguageInfo.ID) + .allowExperimentalOptions(true) + .allowAllAccess(true) + .option(RuntimeOptions.PROJECT_ROOT, pkg.root.getAbsolutePath) + .option( + RuntimeOptions.LOG_LEVEL, + java.util.logging.Level.WARNING.getName + ) + .option(RuntimeOptions.INTERPRETER_SEQUENTIAL_COMMAND_EXECUTION, "true") + .option(RuntimeOptions.ENABLE_PROJECT_SUGGESTIONS, "false") + .option(RuntimeOptions.ENABLE_GLOBAL_SUGGESTIONS, "false") + .option(RuntimeOptions.ENABLE_EXECUTION_TIMER, "false") + .option( + RuntimeOptions.DISABLE_IR_CACHES, + InstrumentTestContext.DISABLE_IR_CACHE + ) + .option(RuntimeServerInfo.ENABLE_OPTION, "true") + .option(RuntimeOptions.INTERACTIVE_MODE, "true") + .option( + RuntimeOptions.LANGUAGE_HOME_OVERRIDE, + Paths + .get("../../test/micro-distribution/component") + .toFile + .getAbsolutePath + ) + .option(RuntimeOptions.EDITION_OVERRIDE, "0.0.0-dev") + .logHandler(new TeeOutputStream(logOut, System.err)) + .out(new TeeOutputStream(out, System.err)) + .serverTransport(runtimeServerEmulator.makeServerTransport) + .build() + + lazy val languageContext = executionContext.context + .getBindings(LanguageInfo.ID) + .invokeMember(MethodNames.TopScope.LEAK_CONTEXT) + .asHostObject[EnsoContext] + + def writeMain(contents: String): File = + Files.write(pkg.mainFile.toPath, contents.getBytes).toFile + + 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 + } + + def send(msg: Api.Request): Unit = runtimeServerEmulator.sendToRuntime(msg) + + def consumeOut: List[String] = { + val result = out.toString + out.reset() + result.linesIterator.toList + } + + def executionComplete(contextId: UUID): Api.Response = + Api.Response(Api.ExecutionComplete(contextId)) + } + + override protected def beforeEach(): Unit = { + context = new TestContext("Test") + context.init() + val Some(Api.Response(_, Api.InitializedNotification())) = context.receive + } + + override protected def afterEach(): Unit = { + if (context != null) { + context.close() + context.out.reset() + context = null + } + } + + it should "edit and resolve a type getter from cached" in { + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val moduleName = "Enso_Test.Test.Main" + + val metadata = new Metadata + val id_x = metadata.addItem(49, 22, "aa") + + val code = + """type Student + | Value id region + | + |main = + | x = Student.Value 1 'EAST' + | x + |""".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(requestId, Api.OpenFileRequest(mainFile, contents)) + ) + context.receive shouldEqual Some( + Api.Response(Some(requestId), Api.OpenFileResponse) + ) + + // push main + context.send( + Api.Request( + requestId, + Api.PushContextRequest( + contextId, + Api.StackItem.ExplicitCall( + Api.MethodPointer(moduleName, moduleName, "main"), + None, + Vector() + ) + ) + ) + ) + context.receiveNIgnoreStdLib(3) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + TestMessages.update( + contextId, + id_x, + s"$moduleName.Student", + Api.MethodCall( + Api.MethodPointer(moduleName, s"$moduleName.Student", "Value") + ) + ), + context.executionComplete(contextId) + ) + + // add getter: `x` -> `x.id` + context.send( + Api.Request( + Api.EditFileNotification( + mainFile, + Seq( + TextEdit( + model.Range(model.Position(5, 5), model.Position(5, 5)), + ".id" + ) + ), + execute = true + ) + ) + ) + context.receiveNIgnoreStdLib(1) should contain theSameElementsAs Seq( + context.executionComplete(contextId) + ) + } + + it should "edit and resolve a type getter from thunk" in { + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val moduleName = "Enso_Test.Test.Main" + + val metadata = new Metadata + val id_x = metadata.addItem(42, 15, "aa") + + val code = + """type Student + | Value id + | + |main = + | x = Student.Value 1 + | x + |""".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(requestId, Api.OpenFileRequest(mainFile, contents)) + ) + context.receive shouldEqual Some( + Api.Response(Some(requestId), Api.OpenFileResponse) + ) + + // push main + context.send( + Api.Request( + requestId, + Api.PushContextRequest( + contextId, + Api.StackItem.ExplicitCall( + Api.MethodPointer(moduleName, moduleName, "main"), + None, + Vector() + ) + ) + ) + ) + context.receiveNIgnoreStdLib(3) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + TestMessages.update( + contextId, + id_x, + s"$moduleName.Student", + Api.MethodCall( + Api.MethodPointer(moduleName, s"$moduleName.Student", "Value") + ) + ), + context.executionComplete(contextId) + ) + + // add getter: `Student.Value 1` -> `Student.Value 1 . id` + context.send( + Api.Request( + Api.EditFileNotification( + mainFile, + Seq( + TextEdit( + model.Range(model.Position(4, 23), model.Position(4, 23)), + " . id" + ) + ), + execute = true + ) + ) + ) + context.receiveNIgnoreExpressionUpdates( + 1 + ) should contain theSameElementsAs Seq(context.executionComplete(contextId)) + } + + it should "fail to resolve symbol after editing the type" in { + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val moduleName = "Enso_Test.Test.Main" + + val metadata = new Metadata + val id_x = metadata.addItem(31, 5, "aa") + val id_y = metadata.addItem(45, 3, "ab") + + val code = + """type T + | C a + | + |main = + | x = T.C 1 + | y = x.a + | y + |""".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(requestId, Api.OpenFileRequest(mainFile, contents)) + ) + context.receive shouldEqual Some( + Api.Response(Some(requestId), Api.OpenFileResponse) + ) + + // push main + context.send( + Api.Request( + requestId, + Api.PushContextRequest( + contextId, + Api.StackItem.ExplicitCall( + Api.MethodPointer(moduleName, moduleName, "main"), + None, + Vector() + ) + ) + ) + ) + context.receiveNIgnoreStdLib(4) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + TestMessages.update( + contextId, + id_x, + s"$moduleName.T", + Api.MethodCall( + Api.MethodPointer(moduleName, s"$moduleName.T", "C") + ) + ), + TestMessages.update(contextId, id_y, ConstantsGen.INTEGER_BUILTIN), + context.executionComplete(contextId) + ) + + // rename type: `type T` -> `type S` + context.send( + Api.Request( + Api.EditFileNotification( + mainFile, + Seq( + TextEdit( + model.Range(model.Position(0, 5), model.Position(0, 6)), + "S" + ) + ), + execute = true + ) + ) + ) + context.receiveNIgnorePendingExpressionUpdates( + 4 + ) should contain theSameElementsAs Seq( + Api.Response( + Api.ExecutionUpdate( + contextId, + Seq( + Api.ExecutionResult.Diagnostic.error( + "The name `T` could not be found.", + Some(mainFile), + Some(model.Range(model.Position(4, 8), model.Position(4, 9))) + ) + ) + ) + ), + TestMessages.panic( + contextId, + id_x, + Api.MethodCall(Api.MethodPointer(moduleName, s"$moduleName.T", "C")), + Api.ExpressionUpdate.Payload.Panic("Compile_Error", List(id_x)), + builtin = true + ), + TestMessages.panic( + contextId, + id_y, + Api.ExpressionUpdate.Payload.Panic("Compile_Error", List(id_x)), + builtin = true + ), + context.executionComplete(contextId) + ) + + // rename type: `type S` -> `type T` + context.send( + Api.Request( + Api.EditFileNotification( + mainFile, + Seq( + TextEdit( + model.Range(model.Position(0, 5), model.Position(0, 6)), + "T" + ) + ), + execute = true + ) + ) + ) + context.receiveNIgnorePendingExpressionUpdates( + 3 + ) should contain theSameElementsAs Seq( + TestMessages.update( + contextId, + id_x, + s"$moduleName.T", + Api.MethodCall( + Api.MethodPointer(moduleName, s"$moduleName.T", "C") + ) + ), + TestMessages.update(contextId, id_y, ConstantsGen.INTEGER_BUILTIN), + context.executionComplete(contextId) + ) + } + + it should "fail to resolve symbol with cached thunk after editing the type" in { + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val moduleName = "Enso_Test.Test.Main" + + val metadata = new Metadata + val id_x = metadata.addItem(31, 9, "aa") + + val code = + """type T + | C a + | + |main = + | x = T.C 1 . a + | x + |""".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(requestId, Api.OpenFileRequest(mainFile, contents)) + ) + context.receive shouldEqual Some( + Api.Response(Some(requestId), Api.OpenFileResponse) + ) + + // push main + context.send( + Api.Request( + requestId, + Api.PushContextRequest( + contextId, + Api.StackItem.ExplicitCall( + Api.MethodPointer(moduleName, moduleName, "main"), + None, + Vector() + ) + ) + ) + ) + context.receiveNIgnoreStdLib(3) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + TestMessages.update( + contextId, + id_x, + ConstantsGen.INTEGER_BUILTIN + ), + context.executionComplete(contextId) + ) + + // rename type: `type T` -> `type S` + context.send( + Api.Request( + Api.EditFileNotification( + mainFile, + Seq( + TextEdit( + model.Range(model.Position(0, 5), model.Position(0, 6)), + "S" + ) + ), + execute = true + ) + ) + ) + context.receiveNIgnorePendingExpressionUpdates( + 3 + ) should contain theSameElementsAs Seq( + Api.Response( + Api.ExecutionUpdate( + contextId, + Seq( + Api.ExecutionResult.Diagnostic.error( + "The name `T` could not be found.", + Some(mainFile), + Some(model.Range(model.Position(4, 8), model.Position(4, 9))) + ) + ) + ) + ), + TestMessages.panic( + contextId, + id_x, + Api.ExpressionUpdate.Payload.Panic("Compile_Error", List(id_x)), + builtin = true + ), + context.executionComplete(contextId) + ) + + // rename type in expression: `T.C 1 . a` -> `S.C 1 . a` + context.send( + Api.Request( + Api.EditFileNotification( + mainFile, + Seq( + TextEdit( + model.Range(model.Position(4, 8), model.Position(4, 9)), + "S" + ) + ), + execute = true + ) + ) + ) + context.receiveNIgnorePendingExpressionUpdates( + 2 + ) should contain theSameElementsAs Seq( + TestMessages.update( + contextId, + id_x, + ConstantsGen.INTEGER_BUILTIN + ), + context.executionComplete(contextId) + ) + } + +}