diff --git a/engine/runtime-compiler/src/main/scala/org/enso/compiler/data/BindingsMap.scala b/engine/runtime-compiler/src/main/scala/org/enso/compiler/data/BindingsMap.scala index f865dd8ece5..dea0795eeeb 100644 --- a/engine/runtime-compiler/src/main/scala/org/enso/compiler/data/BindingsMap.scala +++ b/engine/runtime-compiler/src/main/scala/org/enso/compiler/data/BindingsMap.scala @@ -15,6 +15,7 @@ import org.enso.compiler.pass.analyse.BindingAnalysis import org.enso.compiler.pass.resolve.MethodDefinitions import org.enso.persist.Persistance.Reference import org.enso.pkg.QualifiedName +import org.enso.editions.LibraryName import java.io.ObjectOutputStream import scala.annotation.unused @@ -26,8 +27,8 @@ import scala.collection.mutable.ArrayBuffer * @param currentModule the module holding these bindings */ case class BindingsMap( - definedEntities: List[DefinedEntity], - currentModule: ModuleReference + private val _definedEntities: List[DefinedEntity], + private var _currentModule: ModuleReference ) extends IRPass.IRMetadata { import BindingsMap._ @@ -37,11 +38,38 @@ case class BindingsMap( /** Other modules, imported by [[currentModule]]. */ - var resolvedImports: List[ResolvedImport] = List() + private var _resolvedImports: List[ResolvedImport] = List() + + def definedEntities: List[DefinedEntity] = { + ensureConvertedToConcrete() + _definedEntities + } + def currentModule: ModuleReference = { + ensureConvertedToConcrete() + _currentModule + } + def resolvedImports: List[ResolvedImport] = { + ensureConvertedToConcrete() + _resolvedImports + } + def resolvedImports_=(v: List[ResolvedImport]): Unit = { + _resolvedImports = v + } + + /** Set to non-null after deserialization to signal that conversion to concrete values is needed */ + private var pendingRepository: PackageRepository = null /** Symbols exported by [[currentModule]]. */ - var exportedSymbols: Map[String, List[ResolvedName]] = Map() + private var _exportedSymbols: Map[String, List[ResolvedName]] = Map() + + def exportedSymbols: Map[String, List[ResolvedName]] = { + ensureConvertedToConcrete() + _exportedSymbols + } + def exportedSymbols_=(v: Map[String, List[ResolvedName]]): Unit = { + _exportedSymbols = v + } /** @inheritdoc */ override def prepareForSerialization( @@ -54,8 +82,8 @@ case class BindingsMap( override def restoreFromSerialization( compiler: Compiler ): Option[BindingsMap] = { - val packageRepository = compiler.getPackageRepository - this.toConcrete(packageRepository.getModuleMap) + this.pendingRepository = compiler.getPackageRepository + Some(this) } /** Convert this [[BindingsMap]] instance to use abstract module references. @@ -63,9 +91,9 @@ case class BindingsMap( * @return `this` with module references converted to abstract */ def toAbstract: BindingsMap = { - val copy = this.copy(currentModule = currentModule.toAbstract) - copy.resolvedImports = this.resolvedImports.map(_.toAbstract) - copy.exportedSymbols = this.exportedSymbols.map { case (key, value) => + val copy = this.copy(_currentModule = _currentModule.toAbstract) + copy._resolvedImports = this._resolvedImports.map(_.toAbstract) + copy._exportedSymbols = this._exportedSymbols.map { case (key, value) => key -> value.map(name => name.toAbstract) } copy @@ -77,23 +105,51 @@ case class BindingsMap( * instances * @return `this` with module references converted to concrete */ - def toConcrete(moduleMap: ModuleMap): Option[BindingsMap] = { - val newMap = this.currentModule.toConcrete(moduleMap).map { module => - this.copy(currentModule = module) + private def ensureConvertedToConcrete(): Option[BindingsMap] = { + val r = pendingRepository + if (r != null) { + toConcrete(r, r.getModuleMap).map { b => + pendingRepository = null + this._currentModule = b._currentModule + this._exportedSymbols = b._exportedSymbols + this._resolvedImports = b._resolvedImports + this + } + } else { + Some(this) } + } + + private def toConcrete( + r: PackageRepository, + moduleMap: ModuleMap + ): Option[BindingsMap] = { + val newMap = this._currentModule + .toConcrete(moduleMap) + .map { module => + this._currentModule = module + this + } val withImports: Option[BindingsMap] = newMap.flatMap { bindings => - val newImports = this.resolvedImports.map(_.toConcrete(moduleMap)) + val newImports = this._resolvedImports.map { imp => + imp.targets.foreach { t => + LibraryName + .fromModuleName(t.qualifiedName.toString()) + .foreach(r.ensurePackageIsLoaded(_)); + } + imp.toConcrete(moduleMap) + } if (newImports.exists(_.isEmpty)) { None } else { - bindings.resolvedImports = newImports.map(_.get) + bindings._resolvedImports = newImports.map(_.get) Some(bindings) } } val withSymbols: Option[BindingsMap] = withImports.flatMap { bindings => - val newSymbols = this.exportedSymbols.map { case (key, value) => + val newSymbols = this._exportedSymbols.map { case (key, value) => val newValue = value.map(_.toConcrete(moduleMap)) if (newValue.exists(_.isEmpty)) { key -> None @@ -105,7 +161,7 @@ case class BindingsMap( if (newSymbols.exists { case (_, v) => v.isEmpty }) { None } else { - bindings.exportedSymbols = newSymbols.map { case (k, v) => + bindings._exportedSymbols = newSymbols.map { case (k, v) => k -> v.get } Some(bindings) diff --git a/engine/runtime-compiler/src/main/scala/org/enso/compiler/phase/ImportResolver.scala b/engine/runtime-compiler/src/main/scala/org/enso/compiler/phase/ImportResolver.scala index 576954148ea..dde1e905a30 100644 --- a/engine/runtime-compiler/src/main/scala/org/enso/compiler/phase/ImportResolver.scala +++ b/engine/runtime-compiler/src/main/scala/org/enso/compiler/phase/ImportResolver.scala @@ -11,6 +11,7 @@ import org.enso.common.CompilationStage import scala.collection.mutable import java.io.IOException +import java.util.logging.Level /** Runs imports resolution. Starts from a given module and then recursively * collects all modules that are reachable from it. @@ -46,7 +47,12 @@ final class ImportResolver(compiler: Compiler) extends ImportResolverForIR { ) (ir, currentLocal) } catch { - case _: IOException => + case ex: IOException => + context.logSerializationManager( + Level.WARNING, + "Deserialization of " + module.getName() + " failed", + ex + ) context.updateModule( current, u => { @@ -85,6 +91,7 @@ final class ImportResolver(compiler: Compiler) extends ImportResolverForIR { currentLocal.resolvedImports = resolvedImports ++ resolvedSyntheticImports + val newIr = ir.copy(imports = newImportIRs) context.updateModule( current, diff --git a/engine/runtime-compiler/src/main/scala/org/enso/compiler/phase/exports/ExportsResolution.scala b/engine/runtime-compiler/src/main/scala/org/enso/compiler/phase/exports/ExportsResolution.scala index 6b28d135766..5bdad4c8a8b 100644 --- a/engine/runtime-compiler/src/main/scala/org/enso/compiler/phase/exports/ExportsResolution.scala +++ b/engine/runtime-compiler/src/main/scala/org/enso/compiler/phase/exports/ExportsResolution.scala @@ -166,7 +166,7 @@ class ExportsResolution(private val context: CompilerContext) { .flatten .distinct - bindings.exportedSymbols = List( + val newSymbols = List( ownEntities, expSymbolsFromResolvedImps ).flatten.distinct @@ -179,6 +179,7 @@ class ExportsResolution(private val context: CompilerContext) { } (symbolName, resolvedNames) } + bindings.exportedSymbols = newSymbols } } diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/HelloWorldCacheTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/HelloWorldCacheTest.java new file mode 100644 index 00000000000..57d0e3a5e38 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/HelloWorldCacheTest.java @@ -0,0 +1,76 @@ +package org.enso.interpreter.caches; + +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintStream; +import java.util.logging.Level; +import java.util.stream.Collectors; +import org.enso.common.RuntimeOptions; +import org.enso.test.utils.ContextUtils; +import org.graalvm.polyglot.Source; +import org.junit.Test; + +public class HelloWorldCacheTest { + + @Test + public void loadingHelloWorldTwiceUsesCaching() throws Exception { + var root = new File("../..").getAbsoluteFile(); + assertTrue("build.sbt exists at " + root, new File(root, "build.sbt").exists()); + var helloWorld = children(root, "test", "Benchmarks", "src", "Startup", "Hello_World.enso"); + assertTrue("Hello_World.enso found", helloWorld.exists()); + + // the first run may or may not use caches + var firstMsgs = executeOnce(helloWorld); + assertTrue("Contains hello world:\n" + firstMsgs, firstMsgs.endsWith("Hello World")); + // after the first run the caches for Hello_World.enso must be generated + + // the second run must read Hello_World from its .ir file! + var secondMsgs = executeOnce(helloWorld); + assertTrue("Contains hello world:\n" + secondMsgs, secondMsgs.contains("Hello World")); + assertTrue( + "Properly deserialized:\n" + secondMsgs, + secondMsgs.contains("Deserializing module Hello_World from IR file: true")); + } + + private static String executeOnce(File src) throws Exception { + var backLog = new ByteArrayOutputStream(); + var log = new PrintStream(backLog); + + try (var ctx = + ContextUtils.defaultContextBuilder() + .out(log) + .err(log) + .logHandler(log) + .option(RuntimeOptions.LOG_LEVEL, Level.FINE.getName()) + .option(RuntimeOptions.DISABLE_IR_CACHES, "false") + .option(RuntimeOptions.PROJECT_ROOT, findBenchmarks(src).getAbsolutePath()) + .build()) { + var code = Source.newBuilder("enso", src).build(); + var res = ContextUtils.evalModule(ctx, code, "main"); + assertTrue("Result of IO.println is Nothing", res.isNull()); + } + return backLog + .toString() + .lines() + .filter(l -> l.toUpperCase().contains("HELLO")) + .collect(Collectors.joining("\n")); + } + + private static File children(File f, String... names) { + for (var n : names) { + f = new File(f, n); + } + return f; + } + + private static File findBenchmarks(File f) { + for (; ; ) { + if (f.getName().equals("Benchmarks")) { + return f; + } + f = f.getParentFile(); + } + } +} diff --git a/lib/java/test-utils/src/main/java/org/enso/test/utils/ContextUtils.java b/lib/java/test-utils/src/main/java/org/enso/test/utils/ContextUtils.java index fb05b0ea16b..b59709b4047 100644 --- a/lib/java/test-utils/src/main/java/org/enso/test/utils/ContextUtils.java +++ b/lib/java/test-utils/src/main/java/org/enso/test/utils/ContextUtils.java @@ -162,7 +162,19 @@ public final class ContextUtils { var b = Source.newBuilder("enso", src, name); s = b.buildLiteral(); } - var module = ctx.eval(s); + return evalModule(ctx, s, methodName); + } + + /** + * Evaluates the given source as if it was in a module with given name. + * + * @param ctx context to evaluate the module at + * @param src The source code of the module + * @param methodName name of main method to invoke + * @return The value returned from the main method of the unnamed module. + */ + public static Value evalModule(Context ctx, Source src, String methodName) { + var module = ctx.eval(src); var assocType = module.invokeMember(Module.GET_ASSOCIATED_TYPE); var method = module.invokeMember(Module.GET_METHOD, assocType, methodName); return "main".equals(methodName) ? method.execute() : method.execute(assocType);