diff --git a/engine/runtime-compiler/src/main/java/org/enso/compiler/dump/IRDumper.java b/engine/runtime-compiler/src/main/java/org/enso/compiler/dump/IRDumper.java index 54f21a9673be..3c34d7269574 100644 --- a/engine/runtime-compiler/src/main/java/org/enso/compiler/dump/IRDumper.java +++ b/engine/runtime-compiler/src/main/java/org/enso/compiler/dump/IRDumper.java @@ -162,6 +162,9 @@ private void createIRGraph(Definition definitionIr) { var body = explicitMethodIr.body(); createIRGraph(body); createEdge(explicitMethodIr, body, "body"); + var methodRef = explicitMethodIr.methodReference(); + createIRGraph(methodRef); + createEdge(explicitMethodIr, methodRef, "methodReference"); } case Method.Conversion conversionMethod -> { var bldr = @@ -171,6 +174,9 @@ private void createIRGraph(Definition definitionIr) { var body = conversionMethod.body(); createIRGraph(body); createEdge(conversionMethod, body, "body"); + var methodRef = conversionMethod.methodReference(); + createIRGraph(methodRef); + createEdge(conversionMethod, methodRef, "methodReference"); } case Method.Binding binding -> { var bldr = GraphVizNode.Builder.fromIr(binding); @@ -183,6 +189,9 @@ private void createIRGraph(Definition definitionIr) { var body = binding.body(); createIRGraph(body); createEdge(binding, body, "body"); + var methodRef = binding.methodReference(); + createIRGraph(methodRef); + createEdge(binding, methodRef, "methodReference"); } case Definition.Type type -> { var typeNode = @@ -331,6 +340,17 @@ private void createIRGraph(Expression expression) { var literalNode = bldr.build(); addNode(literalNode); } + case Name.MethodReference methodRef -> { + var bldr = GraphVizNode.Builder.fromIr(methodRef); + bldr.addLabelLine("methodName: " + methodRef.methodName().name()); + if (methodRef.typePointer().isDefined()) { + bldr.addLabelLine("typePointer: " + methodRef.typePointer().get().name()); + } else { + bldr.addLabelLine("typePointer: null"); + } + var methodRefNode = bldr.build(); + addNode(methodRefNode); + } default -> { var node = GraphVizNode.Builder.fromIr(expression).build(); addNode(node); diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/ExtensionMethodResolutionTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/ExtensionMethodResolutionTest.java new file mode 100644 index 000000000000..1e3e887e6ace --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/ExtensionMethodResolutionTest.java @@ -0,0 +1,327 @@ +package org.enso.interpreter.test; + +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.fail; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Set; +import org.enso.pkg.QualifiedName; +import org.enso.polyglot.PolyglotContext; +import org.enso.polyglot.RuntimeOptions; +import org.enso.polyglot.TopScope; +import org.graalvm.polyglot.PolyglotException; +import org.hamcrest.Matcher; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** + * Shadowing identifiers from imported modules is not a compilation error, neither runtime error. + * But we need to make sure that the shadowing is deterministic and that the shadowing is not + * affecting the resolution of extension methods. In other words, we need to make sure that the + * method resolution is deterministic. + */ +public class ExtensionMethodResolutionTest extends TestBase { + @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); + private static final Matcher methodsOverloadErrorMessageMatcher = + allOf( + containsString("Method overloads are not supported"), + containsString("defined multiple times")); + + @Test + public void twoExtensionMethodsWithSameNameInOneModuleShouldFail() throws IOException { + var src = """ + type T + T.foo x = x + T.foo x y = x + y + """; + testProjectCompilationFailure(src, methodsOverloadErrorMessageMatcher); + } + + @Test + public void extensionMethodAndNormalMethodConflictInOneModule() throws IOException { + var src = """ + type T + foo x = x + T.foo x y = x + y + """; + testProjectCompilationFailure(src, methodsOverloadErrorMessageMatcher); + } + + @Test + public void firstResolutionIsInTypesScope() throws IOException { + var xMod = + new SourceModule( + QualifiedName.fromString("X"), + """ + type T + T.foo = "X" + """); + var yMod = + new SourceModule( + QualifiedName.fromString("Y"), + """ + from project.X import T + T.foo = "Y" + """); + var mainMod = + new SourceModule( + QualifiedName.fromString("Main"), + """ + from project.X import T + from project.Y import all + T.foo = "Main" + main = T.foo + """); + var projDir = createProject("Proj", Set.of(xMod, yMod, mainMod), tempFolder); + testProjectRun( + projDir, + res -> { + assertThat( + "Method should be first resolved in the types module scope", res.asString(), is("X")); + }); + } + + @Test + public void secondResolutionIsInCurrentModuleScope() throws IOException { + var xMod = + new SourceModule(QualifiedName.fromString("X"), """ + type T + """); + var yMod = + new SourceModule( + QualifiedName.fromString("Y"), + """ + from project.X import T + T.foo = "Y" + """); + var mainMod = + new SourceModule( + QualifiedName.fromString("Main"), + """ + from project.X import T + from project.Y import all + T.foo = "Main" + main = T.foo + """); + var projDir = createProject("Proj", Set.of(xMod, yMod, mainMod), tempFolder); + testProjectRun( + projDir, + res -> { + assertThat( + "Method should be secondly resolved in the current scope", + res.asString(), + is("Main")); + }); + } + + @Test + public void resolutionFromImportedModulesIsDeterministic1() throws IOException { + var xMod = + new SourceModule(QualifiedName.fromString("X"), """ + type T + """); + var yMod = + new SourceModule( + QualifiedName.fromString("Y"), + """ + from project.X import T + T.foo = "Y" + """); + var zMod = + new SourceModule( + QualifiedName.fromString("Z"), + """ + from project.X import T + T.foo = "Z" + """); + var mainMod = + new SourceModule( + QualifiedName.fromString("Main"), + """ + from project.X import T + from project.Y import all + from project.Z import all + main = T.foo + """); + var projDir = createProject("Proj", Set.of(xMod, yMod, zMod, mainMod), tempFolder); + testProjectRun( + projDir, + res -> { + assertThat( + "Method resolution from imported modules should be deterministic " + + "(Y module is imported first)", + res.asString(), + is("Y")); + }); + } + + @Test + public void resolutionFromImportedModulesIsDeterministic2() throws IOException { + var xMod = + new SourceModule(QualifiedName.fromString("X"), """ + type T + """); + var yMod = + new SourceModule( + QualifiedName.fromString("Y"), + """ + from project.X import T + T.foo = "Y" + """); + var zMod = + new SourceModule( + QualifiedName.fromString("Z"), + """ + from project.X import T + T.foo = "Z" + """); + var mainMod = + new SourceModule( + QualifiedName.fromString("Main"), + """ + from project.X import T + from project.Z import all + from project.Y import all + main = T.foo + """); + var projDir = createProject("Proj", Set.of(xMod, yMod, zMod, mainMod), tempFolder); + testProjectRun( + projDir, + res -> { + assertThat( + "Method resolution from imported modules should be deterministic " + + "(Z module is imported first)", + res.asString(), + is("Z")); + }); + } + + @Test + public void sameMethodInShadowedType() throws IOException { + var mod = + new SourceModule( + QualifiedName.fromString("Mod"), + """ + type T + method = 42 + """); + // This is type-shadowing, which is allowed. + var mainMod = + new SourceModule( + QualifiedName.fromString("Main"), + """ + from project.Mod import T + type T + method = 23 + main = + T.method + """); + var projDir = createProject("Proj", Set.of(mod, mainMod), tempFolder); + testProjectRun( + projDir, + res -> { + assertThat(res.isNumber(), is(true)); + assertThat(res.asInt(), is(23)); + }); + } + + @Test + public void sameExtensionMethodInDifferentTypes() throws IOException { + var mod = + new SourceModule( + QualifiedName.fromString("Mod"), + """ + type T + T.method = 42 + """); + // main module imports just the `Mod` and not the type - it should succeed. + var mainMod = + new SourceModule( + QualifiedName.fromString("Main"), + """ + import project.Mod + type T + T.method = 23 + main = + T.method + """); + var projDir = createProject("Proj", Set.of(mod, mainMod), tempFolder); + testProjectRun( + projDir, + res -> { + assertThat(res.isNumber(), is(true)); + assertThat(res.asInt(), is(23)); + }); + } + + @Test + public void sameExtensionMethodInDifferentTypesInThreeModules() throws IOException { + var mod2 = + new SourceModule( + QualifiedName.fromString("Mod2"), """ + # An empty module + """); + // The type T defined in mod1 and mainMod have exactly the same location on purpose. + var mod1 = + new SourceModule( + QualifiedName.fromString("Mod1"), + """ + import project.Mod2 + type T + T.method = 1 + """); + // main module imports just the `Mod` and not the type - it should succeed. + var mainMod = + new SourceModule( + QualifiedName.fromString("Main"), + """ + import project.Mod1 + type T + T.method = 2 + main = + T.method + """); + var projDir = createProject("Proj", Set.of(mod2, mod1, mainMod), tempFolder); + testProjectRun( + projDir, + res -> { + assertThat(res.isNumber(), is(true)); + assertThat(res.asInt(), is(2)); + }); + } + + private void testProjectCompilationFailure(String mainSrc, Matcher errorMessageMatcher) + throws IOException { + var projDir = createProject("Proj", mainSrc, tempFolder); + testProjectCompilationFailure(projDir, errorMessageMatcher); + } + + private void testProjectCompilationFailure( + Path mainProjDir, Matcher errorMessageMatcher) { + var out = new ByteArrayOutputStream(); + try (var ctx = + defaultContextBuilder() + .option(RuntimeOptions.PROJECT_ROOT, mainProjDir.toAbsolutePath().toString()) + .option(RuntimeOptions.STRICT_ERRORS, "true") + .option(RuntimeOptions.DISABLE_IR_CACHES, "true") + .out(out) + .err(out) + .build()) { + var polyCtx = new PolyglotContext(ctx); + TopScope topScope = polyCtx.getTopScope(); + try { + topScope.compile(true); + fail("Expected compilation error: " + out); + } catch (PolyglotException e) { + assertThat(e.isSyntaxError(), is(true)); + assertThat(out.toString(), errorMessageMatcher); + } + } + } +} diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/PrivateAccessTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/PrivateAccessTest.java index d5de2f6a0600..089344fcf8ac 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/PrivateAccessTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/PrivateAccessTest.java @@ -9,8 +9,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.List; import org.enso.interpreter.util.ScalaConversions; import org.enso.polyglot.PolyglotContext; @@ -48,7 +46,7 @@ public void privateFieldIsNotExposedToPolyglot() throws IOException { private Cons data main = My_Type.Cons 42 """; - var projDir = createProject("My_Project", mainSrc); + var projDir = createProject("My_Project", mainSrc, tempFolder); var mainSrcPath = projDir.resolve("src").resolve("Main.enso"); try (var ctx = defaultContextBuilder() @@ -71,7 +69,7 @@ public void privateConstructorIsNotExposedToPolyglot() throws IOException { type My_Type private Cons data """; - var projDir = createProject("My_Project", mainSrc); + var projDir = createProject("My_Project", mainSrc, tempFolder); var mainSrcPath = projDir.resolve("src").resolve("Main.enso"); try (var ctx = defaultContextBuilder() @@ -94,7 +92,7 @@ public void typeWithPrivateConstructorExposesPublicMethodsToPolyglot() throws IO main = My_Type.Cons 42 """; - var projDir = createProject("My_Project", mainSrc); + var projDir = createProject("My_Project", mainSrc, tempFolder); var mainSrcPath = projDir.resolve("src").resolve("Main.enso"); try (var ctx = defaultContextBuilder() @@ -128,7 +126,7 @@ public void canPatternMatchOnPrivateConstructorFromSameProject() throws IOExcept My_Type.Cons x -> x _ -> 0 """; - var projDir = createProject("My_Project", mainSrc); + var projDir = createProject("My_Project", mainSrc, tempFolder); var mainSrcPath = projDir.resolve("src").resolve("Main.enso"); try (var ctx = defaultContextBuilder() @@ -153,7 +151,7 @@ public void cannotPatternMatchOnPrivateConstructorFromDifferentProject() throws private Cons data create x = My_Type.Cons x """; - createProject("Lib", libSrc); + createProject("Lib", libSrc, tempFolder); var projSrc = """ from local.Lib import My_Type @@ -162,7 +160,7 @@ public void cannotPatternMatchOnPrivateConstructorFromDifferentProject() throws case obj of My_Type.Cons x -> x """; - var projDir = createProject("Proj", projSrc); + var projDir = createProject("Proj", projSrc, tempFolder); var out = new ByteArrayOutputStream(); try (var ctx = defaultContextBuilder() @@ -186,33 +184,4 @@ public void cannotPatternMatchOnPrivateConstructorFromDifferentProject() throws } } } - - /** - * Creates temporary project directory structure with a given main source content. No need to - * clean it up, as it is managed by JUnit TemporaryFolder rule. Note that we need to create a - * project, otherwise the private stuff won't work. - * - * @param projName Name of the project (as defined in package.yaml). - * @param mainSrc Main.enso source content - * @return Path to the newly created directly structure - a project directory. - */ - private Path createProject(String projName, String mainSrc) throws IOException { - var projDir = tempFolder.newFolder(projName); - assert projDir.exists(); - var projYaml = - """ -name: %s -version: 0.0.1 -prefer-local-libraries: true - """.formatted(projName); - var yamlPath = projDir.toPath().resolve("package.yaml"); - Files.writeString(yamlPath, projYaml); - assert yamlPath.toFile().exists(); - var srcDir = tempFolder.newFolder(projName, "src"); - assert srcDir.exists(); - var mainSrcPath = srcDir.toPath().resolve("Main.enso"); - Files.writeString(mainSrcPath, mainSrc); - assert mainSrcPath.toFile().exists(); - return projDir.toPath(); - } } diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/TestBase.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/TestBase.java index acd20f01f76d..b79d9ee50ab0 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/TestBase.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/TestBase.java @@ -9,10 +9,16 @@ import com.oracle.truffle.api.library.ExportMessage; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.nodes.RootNode; +import java.io.File; +import java.io.IOException; import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; +import java.util.Set; import java.util.concurrent.Callable; +import java.util.function.Consumer; import java.util.function.Function; import java.util.logging.Level; import org.enso.common.LanguageInfo; @@ -20,6 +26,8 @@ import org.enso.common.MethodNames.TopScope; import org.enso.interpreter.EnsoLanguage; import org.enso.interpreter.runtime.EnsoContext; +import org.enso.pkg.QualifiedName; +import org.enso.polyglot.PolyglotContext; import org.enso.polyglot.RuntimeOptions; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Language; @@ -27,6 +35,7 @@ import org.graalvm.polyglot.Value; import org.graalvm.polyglot.io.IOAccess; import org.graalvm.polyglot.proxy.ProxyExecutable; +import org.junit.rules.TemporaryFolder; public abstract class TestBase { protected static Context createDefaultContext() { @@ -157,6 +166,92 @@ protected static Value getMethodFromModule(Context ctx, String moduleSrc, String return module.invokeMember(Module.EVAL_EXPRESSION, methodName); } + /** + * Creates temporary project directory structure with a given main source content. No need to + * clean it up, as it is managed by JUnit TemporaryFolder rule. Note that we need to create a + * project, otherwise the private stuff won't work. + * + * @param projName Name of the project (as defined in package.yaml). + * @param mainSrc Main.enso source content + * @param tempFolder Temporary folder from JUnit rule. + * @return Path to the newly created directly structure - a project directory. + */ + protected static Path createProject(String projName, String mainSrc, TemporaryFolder tempFolder) + throws IOException { + var modules = Set.of(new SourceModule(QualifiedName.fromString("Main"), mainSrc)); + return createProject(projName, modules, tempFolder); + } + + /** + * Creates a temporary project directory structure with all the given modules and their content. + * Creates also the package descriptor. The created project directory structure is eligible for + * running via {@code enso --run }. + * + * @param projName Name of the project + * @param modules Set of modules. Must contain `Main` module. + * @param tempFolder Temporary folder. From jUnit rule. + * @return Path to the root directory of the project. + */ + protected static Path createProject( + String projName, Set modules, TemporaryFolder tempFolder) throws IOException { + var projDir = tempFolder.newFolder(projName); + assert projDir.exists(); + var projYaml = + """ +name: %s +version: 0.0.1 +prefer-local-libraries: true + """.formatted(projName); + var yamlPath = projDir.toPath().resolve("package.yaml"); + Files.writeString(yamlPath, projYaml); + assert yamlPath.toFile().exists(); + var srcDir = tempFolder.newFolder(projName, "src"); + var srcDirPath = srcDir.toPath(); + assert srcDir.exists(); + boolean mainModuleFound = false; + for (var module : modules) { + var relativePath = String.join(File.pathSeparator, module.name.pathAsJava()); + var modDirPath = srcDirPath.resolve(relativePath); + modDirPath.toFile().mkdirs(); + var modPath = modDirPath.resolve(module.name.item() + ".enso"); + Files.writeString(modPath, module.code); + if (module.name.equals(QualifiedName.fromString("Main"))) { + mainModuleFound = true; + } + } + assert mainModuleFound; + return projDir.toPath(); + } + + /** + * Tests running the project located in the given {@code projDir}. Is equal to running {@code enso + * --run }. + * + * @param projDir Root directory of the project. + * @param resultConsumer Any action that is to be evaluated on the result of running the {@code + * main} method + */ + protected void testProjectRun(Path projDir, Consumer resultConsumer) { + assert projDir.toFile().exists() && projDir.toFile().isDirectory(); + try (var ctx = + defaultContextBuilder() + .option(RuntimeOptions.PROJECT_ROOT, projDir.toAbsolutePath().toString()) + .option(RuntimeOptions.STRICT_ERRORS, "true") + .option(RuntimeOptions.DISABLE_IR_CACHES, "true") + .build()) { + var polyCtx = new PolyglotContext(ctx); + var mainSrcPath = projDir.resolve("src").resolve("Main.enso"); + var mainMod = polyCtx.evalModule(mainSrcPath.toFile()); + var assocMainModType = mainMod.getAssociatedType(); + var mainMethod = mainMod.getMethod(assocMainModType, "main").get(); + var res = mainMethod.execute(); + resultConsumer.accept(res); + } + } + + /** A simple structure corresponding to an Enso module. */ + public record SourceModule(QualifiedName name, String code) {} + /** * An artificial RootNode. Used for tests of nodes that need to be adopted. Just create this root * node inside a context, all the other nodes, and insert them via {@link diff --git a/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/pass/resolve/OverloadsResolutionTest.scala b/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/pass/resolve/OverloadsResolutionTest.scala index d1a98e411785..afca8b2b17ce 100644 --- a/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/pass/resolve/OverloadsResolutionTest.scala +++ b/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/pass/resolve/OverloadsResolutionTest.scala @@ -73,8 +73,8 @@ class OverloadsResolutionTest extends CompilerTest { val redef1 = ir.bindings(1).asInstanceOf[errors.Redefined.Method] val redef2 = ir.bindings(2).asInstanceOf[errors.Redefined.Method] - redef1.atomName.get.name shouldEqual atomName - redef2.atomName.get.name shouldEqual atomName + redef1.typeName.get.name shouldEqual atomName + redef2.typeName.get.name shouldEqual atomName redef1.methodName.name shouldEqual methodName redef2.methodName.name shouldEqual methodName diff --git a/engine/runtime-parser/src/main/scala/org/enso/compiler/core/ir/expression/errors/Redefined.scala b/engine/runtime-parser/src/main/scala/org/enso/compiler/core/ir/expression/errors/Redefined.scala index ebb169ac1998..1099fcb056be 100644 --- a/engine/runtime-parser/src/main/scala/org/enso/compiler/core/ir/expression/errors/Redefined.scala +++ b/engine/runtime-parser/src/main/scala/org/enso/compiler/core/ir/expression/errors/Redefined.scala @@ -236,7 +236,7 @@ object Redefined { /** An error representing the redefinition of a method in a given module. * This is also known as a method overload. * - * @param atomName the name of the atom the method was being redefined on + * @param typeName the name of the type the method was being redefined on * @param methodName the method name being redefined on `atomName` * @param location the location in the source to which this error * corresponds @@ -244,7 +244,7 @@ object Redefined { * @param diagnostics any diagnostics associated with this error. */ sealed case class Method( - atomName: Option[Name], + typeName: Option[Name], methodName: Name, override val location: Option[IdentifiedLocation], override val passData: MetadataStorage = new MetadataStorage(), @@ -267,7 +267,7 @@ object Redefined { * @return a copy of `this`, updated with the specified values */ def copy( - atomName: Option[Name] = atomName, + atomName: Option[Name] = typeName, methodName: Name = methodName, location: Option[IdentifiedLocation] = location, passData: MetadataStorage = passData, @@ -288,7 +288,7 @@ object Redefined { keepIdentifiers: Boolean = false ): Method = copy( - atomName = atomName.map( + atomName = typeName.map( _.duplicate( keepLocations, keepMetadata, @@ -317,11 +317,11 @@ object Redefined { /** @inheritdoc */ override def message(source: (IdentifiedLocation => String)): String = - s"Method overloads are not supported: ${atomName.map(_.name + ".").getOrElse("")}" + + s"Method overloads are not supported: ${typeName.map(_.name + ".").getOrElse("")}" + s"${methodName.name} is defined multiple times in this module." override def diagnosticKeys(): Array[Any] = { - atomName + typeName .map(_.name :: methodName.name :: Nil) .getOrElse(methodName.name :: Nil) .toArray @@ -336,7 +336,7 @@ object Redefined { override def toString: String = s""" |Error.Redefined.Method( - |atomName = $atomName, + |atomName = $typeName, |methodName = $methodName, |location = $location, |passData = ${this.showPassData}, @@ -347,13 +347,13 @@ object Redefined { /** @inheritdoc */ override def children: List[IR] = - atomName + typeName .map(_ :: methodName :: Nil) .getOrElse(methodName :: Nil) /** @inheritdoc */ override def showCode(indent: Int): String = - s"(Redefined (Method ${atomName.map(_.showCode() + ".").getOrElse("")}$methodName))" + s"(Redefined (Method ${typeName.map(_.showCode() + ".").getOrElse("")}$methodName))" } /** An error representing the redefinition of a method in a given module, diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/AmbiguousMethodException.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/AmbiguousMethodException.java new file mode 100644 index 000000000000..2b9529dc06bf --- /dev/null +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/AmbiguousMethodException.java @@ -0,0 +1,32 @@ +package org.enso.interpreter.runtime.error; + +import com.oracle.truffle.api.exception.AbstractTruffleException; +import com.oracle.truffle.api.nodes.Node; +import java.util.List; +import java.util.Objects; +import org.enso.interpreter.runtime.callable.function.Function; +import org.enso.interpreter.runtime.data.Type; + +public class AmbiguousMethodException extends AbstractTruffleException { + public AmbiguousMethodException( + Node location, Type type, String methodName, List candidates) { + super(constructMessage(type, methodName, candidates), location); + } + + private static String constructMessage(Type type, String methodName, List candidates) { + Objects.requireNonNull(type); + Objects.requireNonNull(methodName); + Objects.requireNonNull(candidates); + if (candidates.size() < 2) { + throw new IllegalArgumentException("candidates list must contain at least two functions"); + } + var candidateNames = candidates.stream().map(f -> f.toString(false)).toList(); + var sb = new StringBuilder(); + sb.append("Ambiguous method call '"); + sb.append(type.getName()).append(".").append(methodName); + sb.append("': "); + sb.append("definition candidates are: "); + sb.append(candidateNames); + return sb.toString(); + } +} 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 fb57a8b27a68..2b433955dbd2 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 @@ -4,14 +4,13 @@ import com.oracle.truffle.api.library.ExportLibrary; import com.oracle.truffle.api.library.ExportMessage; import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; import java.util.stream.Collectors; import org.enso.interpreter.runtime.Module; @@ -47,12 +46,12 @@ public final class ModuleScope implements EnsoObject { * @param module the module related to the newly created scope. */ public ModuleScope(Module module) { - this.polyglotSymbols = new HashMap<>(); - this.types = new HashMap<>(); - this.methods = new ConcurrentHashMap<>(); - this.conversions = new ConcurrentHashMap<>(); - this.imports = new HashSet<>(); - this.exports = new HashSet<>(); + this.polyglotSymbols = new LinkedHashMap<>(); + this.types = new LinkedHashMap<>(); + this.methods = new LinkedHashMap<>(); + this.conversions = new LinkedHashMap<>(); + this.imports = new LinkedHashSet<>(); + this.exports = new LinkedHashSet<>(); this.module = module; this.associatedType = Type.createSingleton(module.getName().item(), this, null, false, false); } @@ -110,14 +109,14 @@ public Set getImports() { */ private Map> ensureMethodMapFor(Type type) { Type tpeKey = type == null ? noTypeKey : type; - return methods.computeIfAbsent(tpeKey, k -> new HashMap<>()); + return methods.computeIfAbsent(tpeKey, k -> new LinkedHashMap<>()); } private Map> getMethodMapFor(Type type) { Type tpeKey = type == null ? noTypeKey : type; Map> result = methods.get(tpeKey); if (result == null) { - return new HashMap<>(); + return new LinkedHashMap<>(); } return result; } @@ -167,13 +166,13 @@ public void registerMethod(Type type, String method, Supplier supply) * @return a list containing all the defined conversions in definition order */ private Map ensureConversionsFor(Type type) { - return conversions.computeIfAbsent(type, k -> new HashMap<>()); + return conversions.computeIfAbsent(type, k -> new LinkedHashMap<>()); } private Map getConversionsFor(Type type) { var result = conversions.get(type); if (result == null) { - return new HashMap<>(); + return new LinkedHashMap<>(); } return result; } @@ -394,11 +393,11 @@ public Object getPolyglotSymbol(String symbolName) { } public void reset() { - imports = new HashSet<>(); - exports = new HashSet<>(); - methods = new HashMap<>(); - conversions = new HashMap<>(); - polyglotSymbols = new HashMap<>(); + imports = new LinkedHashSet<>(); + exports = new LinkedHashSet<>(); + methods = new LinkedHashMap<>(); + conversions = new LinkedHashMap<>(); + polyglotSymbols = new LinkedHashMap<>(); } /** @@ -408,12 +407,12 @@ public void reset() { * @return a copy of this scope modulo the requested types */ public ModuleScope withTypes(List typeNames) { - Map polyglotSymbols = new HashMap<>(this.polyglotSymbols); - Map requestedTypes = new HashMap<>(this.types); - Map>> methods = new ConcurrentHashMap<>(); - Map> conversions = new ConcurrentHashMap<>(); - Set imports = new HashSet<>(this.imports); - Set exports = new HashSet<>(this.exports); + Map polyglotSymbols = new LinkedHashMap<>(this.polyglotSymbols); + Map requestedTypes = new LinkedHashMap<>(this.types); + Map>> methods = new LinkedHashMap<>(); + Map> conversions = new LinkedHashMap<>(); + Set imports = new LinkedHashSet<>(this.imports); + Set exports = new LinkedHashSet<>(this.exports); this.types .entrySet() .forEach(