From c2f0925064918eb7851c5072e9f0fa0adfb63975 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Mon, 13 Jan 2025 16:45:48 +0100 Subject: [PATCH] Builtins expose Enso methods (#11687) * Add BuiltinsExposeMethodsTest * typo in docs * Test iteartes all the builtins and checks that all methods are invocable member * Add TypesExposeConstructorsTest * Naive implementation of BuiltinObject base class. * Ref extends BuiltinObject * Builtin type anot processor ensures that a class must extend BuiltinObject * Enrich BuiltinsExposeMethodsTest * All builtin types extend BuiltinObject * Text is BuiltinObject * EnsoBigInteger is BuiltinObject * BuiltinObject does no asserts in constructor * ArrayProxy is BuiltinObject * Test skips host values and Nothing * fmt * Remove outdated test. No longer true what this test tested * EqualsComplexNode: Timezone and duration are not treated as object with members * Fix DebuggingEnsoTest - Date in js is date time, not date * Fix DateTest - ensoDate now has members * Add interop.readMember test to BuiltinsExposeMethodsTest * VectorSortTest is executed in context * Add tests that invoke builtin methods on particular builtin types * member methods on BuiltinObject are behind TruffleBoundary * Cache builtin type in BuiltinObject. This fixes native image build of engine-runner. * Reuse context in DebuggingEnsoTest. This fixes the AssertionError in invalid sharing layer. * Reuse hardcoded builtinName constants from annotation * Move BuiltinObject to package org.enso.interpreter.runtime.builtin * Fix FQN of BuiltinObject in the annotation processor * Make exported messages on BuiltinObject final * Update generated class names in Builtins. Fixes compilation after 8acb04a4f948ab28ea028c55e6035ea35256799e * Exported messages in BuiltinObject are not behind TruffleBoundary * [WIP] Add BuiltinsJavaInteropTest * Storage.vectorizedOrFallbackBinaryMap accepts Value as parameter * Builtin types expose methods, not BuiltinObject * BuiltinObject has no members * Remove test Text.is_empty Text is a builtin type with Builtins module scope, which does not have `is_empty` method. * Fix tests - invocation is done via static methods * Type.InvokeMember uses InvokeFunctionNode instead of UnresolvedSymbol * fmt * Type.InvokeMember prepends receiver argument * Test invocation of File.path * Test invocation of Vector.to_text * Method fetching is done behind truffle boundary * Reassigning @CompilationFinal field should deoptimize * Fixes after merge * Add TruffleBoundary * Revert DateTest * Fix PrivateConstructor test * Methods on Type are internal members * fmt * Add TruffleBoundary * Revert Storage to develop * Remove FIXME comment * Simplify usages of Ref.get and Pth.root --------- Co-authored-by: Jaroslav Tulach --- .../Base/0.0.0-dev/src/Meta/Enso_Project.enso | 5 - .../org/enso/interpreter/runtime/RefTest.java | 49 +++--- .../interpreter/test/DebuggingEnsoTest.java | 32 ++-- .../enso/interpreter/test/VectorSortTest.java | 29 ++-- .../BuiltinTypesExposeMethodsTest.java | 121 ++++++++++++++ .../InvokeBuiltinMethodViaInteropTest.java | 151 ++++++++++++++++++ .../PrivateConstructorAccessTest.java | 14 +- .../test/semantic/PolyglotTest.scala | 14 -- .../builtin/meta/EqualsComplexNode.java | 6 + .../enso/interpreter/runtime/data/Type.java | 142 +++++++++++++++- .../org/enso/base/CurrentEnsoProject.java | 28 ++-- 11 files changed, 496 insertions(+), 95 deletions(-) create mode 100644 engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/builtins/BuiltinTypesExposeMethodsTest.java create mode 100644 engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/builtins/InvokeBuiltinMethodViaInteropTest.java diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Meta/Enso_Project.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Meta/Enso_Project.enso index cc7ecfd01db9..5e98128f3983 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Meta/Enso_Project.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Meta/Enso_Project.enso @@ -91,11 +91,6 @@ type Project_Description namespace : Text namespace self = self.ns - ## PRIVATE - Returns the path of the project root. - root_path : Text - root_path self = self.root.path - ## PRIVATE enso_project_builtin module = @Builtin_Method "Project_Description.enso_project_builtin" diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/runtime/RefTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/runtime/RefTest.java index 1e0f06682b90..b66846a07fb4 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/runtime/RefTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/runtime/RefTest.java @@ -7,10 +7,8 @@ import java.lang.ref.Reference; import java.lang.ref.WeakReference; -import org.enso.common.MethodNames; import org.enso.test.utils.ContextUtils; import org.graalvm.polyglot.Context; -import org.graalvm.polyglot.Source; import org.graalvm.polyglot.Value; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -19,31 +17,18 @@ public class RefTest { private static Context ctx; private static EnsoContext ensoCtx; - private static Value newRef; - private static Value createRef; - private static Value getRef; + private static Value refType; @BeforeClass public static void initCtx() throws Exception { ctx = ContextUtils.createDefaultContext(); ensoCtx = ContextUtils.leakContext(ctx); - var code = - """ - import Standard.Base.Runtime.Ref.Ref - - new_ref obj = - Ref.new obj - - create_ref obj allow_gc = - Ref.new obj allow_gc - - get_ref ref = ref.get - """; - var src = Source.newBuilder("enso", code, "gc.enso").build(); - var gcEnso = ctx.eval(src); - newRef = gcEnso.invokeMember(MethodNames.Module.EVAL_EXPRESSION, "new_ref"); - createRef = gcEnso.invokeMember(MethodNames.Module.EVAL_EXPRESSION, "create_ref"); - getRef = gcEnso.invokeMember(MethodNames.Module.EVAL_EXPRESSION, "get_ref"); + refType = + ContextUtils.evalModule( + ctx, """ + import Standard.Base.Runtime.Ref.Ref + main = Ref + """); } @AfterClass @@ -52,10 +37,18 @@ public static void closeCtx() throws Exception { ctx = null; } + private static Value getRef(Value ref) { + return refType.invokeMember("get", ref); + } + + private static Value newRef(Object object) { + return refType.invokeMember("new", object); + } + @Test public void regularReference() throws Exception { var obj = new Object(); - var ref = newRef.execute(obj); + var ref = newRef(obj); assertFalse("Value returned", ref.isNull()); assertEquals("Standard.Base.Runtime.Ref.Ref", ref.getMetaObject().getMetaQualifiedName()); @@ -63,17 +56,15 @@ public void regularReference() throws Exception { var weakRef = new WeakReference<>(obj); obj = null; - assertEquals("We get the object", weakRef.get(), getRef.execute(ref).asHostObject()); + assertEquals("We get the object", weakRef.get(), getRef(ref).asHostObject()); assertGC("Weak wasn't released", false, weakRef); - assertFalse("Value was not GCed", getRef.execute(ref).isNull()); - assertEquals("We get the object", weakRef.get(), getRef.execute(ref).asHostObject()); + assertFalse("Value was not GCed", getRef(ref).isNull()); + assertEquals("We get the object", weakRef.get(), getRef(ref).asHostObject()); // ensoCtx.getReferencesManager().releaseAll(); assertEquals( - "releaseAll has no effect on regular reference", - weakRef.get(), - getRef.execute(ref).asHostObject()); + "releaseAll has no effect on regular reference", weakRef.get(), getRef(ref).asHostObject()); } private static void assertGC(String msg, boolean expectGC, Reference ref) { diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/DebuggingEnsoTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/DebuggingEnsoTest.java index f10b10c34598..a2f15a0a3438 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/DebuggingEnsoTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/DebuggingEnsoTest.java @@ -42,23 +42,23 @@ import org.graalvm.polyglot.Source; import org.graalvm.polyglot.Value; import org.graalvm.polyglot.io.IOAccess; -import org.junit.After; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestWatcher; import org.junit.runner.Description; public class DebuggingEnsoTest { - private Context context; - private Engine engine; - private Debugger debugger; - private final ByteArrayOutputStream out = new ByteArrayOutputStream(); + private static Context context; + private static Engine engine; + private static Debugger debugger; + private static final ByteArrayOutputStream out = new ByteArrayOutputStream(); - @Before - public void initContext() { - out.reset(); + @BeforeClass + public static void initContext() { engine = Engine.newBuilder() .allowExperimentalOptions(true) @@ -85,12 +85,18 @@ public void initContext() { Assert.assertNotNull("Enso found: " + langs, langs.get("enso")); } - @After - public void disposeContext() throws IOException { + @AfterClass + public static void disposeContext() throws IOException { context.close(); context = null; engine.close(); engine = null; + debugger = null; + } + + @Before + public void resetOut() { + out.reset(); } /** Only print warnings from the compiler if a test fails. */ @@ -307,10 +313,10 @@ public void hostValueIsTreatedAsItsEnsoCounterpart() { foo _ = d_enso = Date.new 2024 12 15 - d_js = js_date d_java = Date.parse "2024-12-15" dt_enso = Date_Time.now dt_java = Date_Time.parse "2020-05-06 04:30:20" "yyyy-MM-dd HH:mm:ss" + dt_js = js_date str_enso = "Hello_World" str_js = js_str str_java = String.new "Hello_World" @@ -335,13 +341,13 @@ public void hostValueIsTreatedAsItsEnsoCounterpart() { DebugValue ensoDate = scope.getDeclaredValue("d_enso"); DebugValue javaDate = scope.getDeclaredValue("d_java"); - DebugValue jsDate = scope.getDeclaredValue("d_js"); assertSameProperties(ensoDate.getProperties(), javaDate.getProperties()); - assertSameProperties(ensoDate.getProperties(), jsDate.getProperties()); DebugValue ensoDateTime = scope.getDeclaredValue("dt_enso"); DebugValue javaDateTime = scope.getDeclaredValue("dt_java"); + DebugValue jsDateTime = scope.getDeclaredValue("dt_js"); assertSameProperties(ensoDateTime.getProperties(), javaDateTime.getProperties()); + assertSameProperties(ensoDateTime.getProperties(), jsDateTime.getProperties()); DebugValue ensoString = scope.getDeclaredValue("str_enso"); DebugValue javaString = scope.getDeclaredValue("str_java"); diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/VectorSortTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/VectorSortTest.java index d8a6c8b7498e..e6259b7de62a 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/VectorSortTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/VectorSortTest.java @@ -61,18 +61,23 @@ public static void disposeCtx() { @Theory public void testSortHandlesAllValues(Value value1, Value value2) { - Assume.assumeFalse(isNan(value1) || isNan(value2)); - Value res = sortFunc.execute(value1, value2); - assertTrue(res.hasArrayElements()); - assertEquals(2, res.getArraySize()); - List resArray = readPolyglotArray(res); - // check that value1 is there unchanged on some index, and the same for value2 - assertTrue( - "Sorted vector should contain the first value at any index", - invokeEquals(value1, resArray.get(0)) || invokeEquals(value1, resArray.get(1))); - assertTrue( - "Sorted vector should contain the second value at any index", - invokeEquals(value2, resArray.get(0)) || invokeEquals(value2, resArray.get(1))); + ContextUtils.executeInContext( + context, + () -> { + Assume.assumeFalse(isNan(value1) || isNan(value2)); + Value res = sortFunc.execute(value1, value2); + assertTrue(res.hasArrayElements()); + assertEquals(2, res.getArraySize()); + List resArray = readPolyglotArray(res); + // check that value1 is there unchanged on some index, and the same for value2 + assertTrue( + "Sorted vector should contain the first value at any index", + invokeEquals(value1, resArray.get(0)) || invokeEquals(value1, resArray.get(1))); + assertTrue( + "Sorted vector should contain the second value at any index", + invokeEquals(value2, resArray.get(0)) || invokeEquals(value2, resArray.get(1))); + return null; + }); } private boolean isNan(Value value) { diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/builtins/BuiltinTypesExposeMethodsTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/builtins/BuiltinTypesExposeMethodsTest.java new file mode 100644 index 000000000000..7d750c4b72d0 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/builtins/BuiltinTypesExposeMethodsTest.java @@ -0,0 +1,121 @@ +package org.enso.interpreter.test.builtins; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +import java.util.ArrayList; +import java.util.List; +import org.enso.interpreter.runtime.data.Type; +import org.enso.interpreter.runtime.library.dispatch.TypeOfNode; +import org.enso.interpreter.test.ValuesGenerator; +import org.enso.interpreter.test.ValuesGenerator.Language; +import org.enso.test.utils.ContextUtils; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Value; +import org.junit.AfterClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Gathers all the builtin objects from {@link ValuesGenerator}. From their types, gathers all their + * methods via their {@link org.enso.interpreter.runtime.scope.ModuleScope definition scope} and + * checks that {@link Value#canInvokeMember(String)} returns true. + */ +@RunWith(Parameterized.class) +public class BuiltinTypesExposeMethodsTest { + private static Context ctx; + + private final Value type; + + public BuiltinTypesExposeMethodsTest(Value type) { + this.type = type; + } + + private static Context ctx() { + if (ctx == null) { + ctx = ContextUtils.createDefaultContext(); + } + return ctx; + } + + @Parameters(name = "{index}: {0}") + public static Iterable generateBuiltinObjects() { + var valuesGenerator = ValuesGenerator.create(ctx(), Language.ENSO); + var builtinTypes = new ArrayList(); + ContextUtils.executeInContext( + ctx(), + () -> { + valuesGenerator.allTypes().stream() + .filter( + val -> { + var asType = getType(val); + return !shouldSkipType(asType); + }) + .forEach(builtinTypes::add); + return null; + }); + return builtinTypes; + } + + private static Type getType(Value object) { + var unwrapped = ContextUtils.unwrapValue(ctx(), object); + return TypeOfNode.getUncached().findTypeOrNull(unwrapped); + } + + @AfterClass + public static void disposeCtx() { + if (ctx != null) { + ctx.close(); + ctx = null; + } + } + + @Test + public void builtinExposeMethods() { + ContextUtils.executeInContext( + ctx(), + () -> { + assertThat(type, is(notNullValue())); + var typeDefScope = getType(type).getDefinitionScope(); + var methodsDefinedInScope = typeDefScope.getMethodsForType(getType(type)); + if (methodsDefinedInScope != null) { + for (var methodInScope : methodsDefinedInScope) { + var methodName = methodInScope.getName(); + if (methodName.contains(".")) { + var items = methodName.split("\\."); + methodName = items[items.length - 1]; + } + assertThat( + "Builtin type " + type + " should have members", type.hasMembers(), is(true)); + assertThat( + "Member " + methodName + " should be present", + type.hasMember(methodName), + is(true)); + assertThat( + "Member " + methodName + " should be invocable", + type.canInvokeMember(methodName), + is(true)); + } + } + return null; + }); + } + + private static boolean shouldSkipType(Type type) { + if (type == null) { + return true; + } + if (!type.isBuiltin()) { + return true; + } + var builtins = ContextUtils.leakContext(ctx()).getBuiltins(); + var typesToSkip = + List.of( + builtins.function(), builtins.dataflowError(), builtins.warning(), builtins.nothing()); + var shouldBeSkipped = typesToSkip.stream().anyMatch(toSkip -> toSkip == type); + return shouldBeSkipped; + } +} diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/builtins/InvokeBuiltinMethodViaInteropTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/builtins/InvokeBuiltinMethodViaInteropTest.java new file mode 100644 index 000000000000..48285ea4c4c6 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/builtins/InvokeBuiltinMethodViaInteropTest.java @@ -0,0 +1,151 @@ +package org.enso.interpreter.test.builtins; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +import com.oracle.truffle.api.interop.InteropLibrary; +import org.enso.test.utils.ContextUtils; +import org.graalvm.polyglot.Context; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * This test tries to invoke some builtin methods on builtin types via the {@link + * com.oracle.truffle.api.interop.InteropLibrary interop} protocol. + */ +public class InvokeBuiltinMethodViaInteropTest { + private static Context ctx; + + @BeforeClass + public static void setUp() { + ctx = ContextUtils.createDefaultContext(); + } + + @AfterClass + public static void tearDown() { + ctx.close(); + ctx = null; + } + + @Test + public void invokeGetMethodOnRef() { + var code = + """ + import Standard.Base.Runtime.Ref.Ref + + main = Ref.new 42 + """; + var ref = ContextUtils.evalModule(ctx, code); + ContextUtils.executeInContext( + ctx, + () -> { + var interop = InteropLibrary.getUncached(); + var refUnwrapped = ContextUtils.unwrapValue(ctx, ref); + assertThat( + "Ref builtin object should not have any members", + interop.hasMembers(refUnwrapped), + is(false)); + assertThat( + "Ref should have a meta-object (Ref type)", + interop.hasMetaObject(refUnwrapped), + is(true)); + var refMeta = interop.getMetaObject(refUnwrapped); + assertThat( + "Ref meta-object should have a 'get' method", + interop.isMemberInvocable(refMeta, "get"), + is(true)); + var res = interop.invokeMember(refMeta, "get", new Object[] {refUnwrapped}); + assertThat("Ref.get should return a number", interop.isNumber(res), is(true)); + assertThat("Ref.get should return 42", interop.asInt(res), is(42)); + return null; + }); + } + + @Test + public void invokePathMethodOnFile() { + var code = + """ + from Standard.Base import File + + main = + File.current_directory + """; + var file = ContextUtils.evalModule(ctx, code); + ContextUtils.executeInContext( + ctx, + () -> { + var fileType = file.getMetaObject(); + assertThat(fileType, is(notNullValue())); + assertThat(fileType.hasMember("path"), is(true)); + var res = fileType.invokeMember("path", new Object[] {file}); + assertThat("path method can be invoked", res, is(notNullValue())); + assertThat("path method returns correct result", res.isString(), is(true)); + return null; + }); + } + + @Test + public void invokeToTextOnVector() { + var code = """ + main = [1,2,3] + """; + var vec = ContextUtils.evalModule(ctx, code); + ContextUtils.executeInContext( + ctx, + () -> { + var vecType = vec.getMetaObject(); + assertThat(vecType, is(notNullValue())); + assertThat(vecType.hasMember("to_text"), is(true)); + var res = vecType.invokeMember("to_text", new Object[] {vec}); + assertThat("to_text method can be invoked", res, is(notNullValue())); + assertThat("to_text method returns correct result", res.isString(), is(true)); + return null; + }); + } + + /** + * 'Text.reverse' is an extension method defined outside builtins module scope, so it cannot be + * resolved. + */ + @Test + public void extensionMethodOnBuiltinTypeIsNotResolved() { + var text = ContextUtils.evalModule(ctx, "main = 'Hello'"); + ContextUtils.executeInContext( + ctx, + () -> { + var interop = InteropLibrary.getUncached(); + var textUnwrapped = ContextUtils.unwrapValue(ctx, text); + var textMeta = interop.getMetaObject(textUnwrapped); + assertThat( + "Text type should not be able to resolve 'reverse' method", + interop.isMemberInvocable(textMeta, "reverse"), + is(false)); + return null; + }); + } + + @Test + public void invokePlusOnTextWithParameter() { + var text1 = ContextUtils.evalModule(ctx, "main = 'First'"); + var text2 = ContextUtils.evalModule(ctx, "main = 'Second'"); + ContextUtils.executeInContext( + ctx, + () -> { + var interop = InteropLibrary.getUncached(); + var text1Unwrapped = ContextUtils.unwrapValue(ctx, text1); + var text2Unwrapped = ContextUtils.unwrapValue(ctx, text2); + var textMeta = interop.getMetaObject(text1Unwrapped); + assertThat( + "Text type should have a '+' method", + interop.isMemberInvocable(textMeta, "+"), + is(true)); + var res = interop.invokeMember(textMeta, "+", text1Unwrapped, text2Unwrapped); + assertThat("Text.+ should return a text", interop.isString(res), is(true)); + assertThat( + "Text.+ should return 'FirstSecond'", interop.asString(res), is("FirstSecond")); + return null; + }); + } +} diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/privateaccess/PrivateConstructorAccessTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/privateaccess/PrivateConstructorAccessTest.java index b4dc714d873d..5d7833e7b728 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/privateaccess/PrivateConstructorAccessTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/privateaccess/PrivateConstructorAccessTest.java @@ -6,6 +6,7 @@ import static org.hamcrest.core.AllOf.allOf; import static org.junit.Assert.fail; +import com.oracle.truffle.api.interop.InteropLibrary; import java.io.ByteArrayOutputStream; import java.io.IOException; import org.enso.common.RuntimeOptions; @@ -53,7 +54,18 @@ public void privateConstructorIsNotExposedToPolyglot() throws IOException { var polyCtx = new PolyglotContext(ctx); var mainMod = polyCtx.evalModule(mainSrcPath.toFile()); var myType = mainMod.getType("My_Type"); - assertThat(myType.hasMember("Cons"), is(false)); + ContextUtils.executeInContext( + ctx, + () -> { + var myTypeUnwrapped = ContextUtils.unwrapValue(ctx, myType); + var interop = InteropLibrary.getUncached(); + var members = interop.getMembers(myTypeUnwrapped, false); + assertThat( + "My_Type should not have any 'public' members", + interop.getArraySize(members), + is(0L)); + return null; + }); } } diff --git a/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/semantic/PolyglotTest.scala b/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/semantic/PolyglotTest.scala index c0f8463a0b21..85a2ab2917b4 100644 --- a/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/semantic/PolyglotTest.scala +++ b/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/semantic/PolyglotTest.scala @@ -92,20 +92,6 @@ class PolyglotTest extends InterpreterTest { ) } - "empty members when message not supported" in { - val code = - """from Standard.Base import all - | - |main = - | instance = "Hi There" - | members = Polyglot.get_members instance - | IO.println members.length - | IO.println members - |""".stripMargin - eval(code) - consumeOut shouldEqual List("0", "[]") - } - "fail to match on Polyglot symbol when imported everything from stdlib" in { val code = """from Standard.Base import all diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/EqualsComplexNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/EqualsComplexNode.java index d742b45f646c..20ee96d276eb 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/EqualsComplexNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/EqualsComplexNode.java @@ -543,6 +543,12 @@ boolean isObjectWithMembers(Object object, InteropLibrary interop) { if (interop.isTime(object)) { return false; } + if (interop.isDuration(object)) { + return false; + } + if (interop.isTimeZone(object)) { + return false; + } if (interop.hasHashEntries(object)) { return false; } 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 76740d14916d..41002bf14f26 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 @@ -3,9 +3,13 @@ import com.oracle.truffle.api.CompilerAsserts; import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.interop.ArityException; import com.oracle.truffle.api.interop.InteropLibrary; import com.oracle.truffle.api.interop.UnknownIdentifierException; import com.oracle.truffle.api.interop.UnsupportedMessageException; +import com.oracle.truffle.api.interop.UnsupportedTypeException; import com.oracle.truffle.api.library.CachedLibrary; import com.oracle.truffle.api.library.ExportLibrary; import com.oracle.truffle.api.library.ExportMessage; @@ -13,13 +17,18 @@ import com.oracle.truffle.api.nodes.Node; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Objects; import org.enso.interpreter.Constants; import org.enso.interpreter.EnsoLanguage; import org.enso.interpreter.node.ConstantNode; +import org.enso.interpreter.node.callable.InvokeCallableNode.ArgumentsExecutionMode; +import org.enso.interpreter.node.callable.InvokeCallableNode.DefaultsExecutionMode; +import org.enso.interpreter.node.callable.dispatch.InvokeFunctionNode; import org.enso.interpreter.runtime.EnsoContext; import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition; +import org.enso.interpreter.runtime.callable.argument.CallArgumentInfo; import org.enso.interpreter.runtime.callable.function.Function; import org.enso.interpreter.runtime.callable.function.FunctionSchema; import org.enso.interpreter.runtime.data.atom.AtomConstructor; @@ -42,6 +51,7 @@ public final class Type extends EnsoObject { private final boolean isProjectPrivate; private boolean gettersGenerated; + private Map methods; private Type( String name, @@ -342,14 +352,28 @@ boolean hasMembers() { return true; } + /** + * Members are constructors and methods from this type, and eigen type. Not extension methods as + * they are most likely not in the module scope of this type. + * + * @param includeInternal + * @return + */ @ExportMessage @CompilerDirectives.TruffleBoundary EnsoObject getMembers(boolean includeInternal) { if (isProjectPrivate) { return ArrayLikeHelpers.empty(); - } else { - return ArrayLikeHelpers.wrapStrings(constructors.keySet().toArray(String[]::new)); } + var consNames = constructors.keySet(); + if (!includeInternal) { + return ArrayLikeHelpers.wrapStrings(consNames.toArray(String[]::new)); + } + var methodNames = methods().keySet(); + var allNames = new HashSet(); + allNames.addAll(consNames); + allNames.addAll(methodNames); + return ArrayLikeHelpers.wrapStrings(allNames.toArray(String[]::new)); } @ExportMessage @@ -358,7 +382,69 @@ boolean isMemberReadable(String member) { if (isProjectPrivate) { return false; } else { - return constructors.containsKey(member); + return constructors.containsKey(member) || methods().containsKey(member); + } + } + + @ExportMessage + @TruffleBoundary + boolean isMemberInvocable(String member) { + return methods().containsKey(member); + } + + @ExportMessage + static final class InvokeMember { + @Specialization( + guards = {"cachedMember.equals(member)", "func != null"}, + limit = "3") + static Object doCached( + Type receiver, + String member, + Object[] args, + @Cached("member") String cachedMember, + @Cached("findMethod(receiver, member)") Function func, + @Cached("buildInvokeFuncNode(func)") InvokeFunctionNode invokeFuncNode) + throws UnsupportedMessageException, UnsupportedTypeException, ArityException { + var argsWithReceiver = new Object[args.length + 1]; + argsWithReceiver[0] = receiver; + System.arraycopy(args, 0, argsWithReceiver, 1, args.length); + return invokeFuncNode.execute(func, null, null, argsWithReceiver); + } + + @Specialization(replaces = "doCached") + @TruffleBoundary + static Object doUncached( + Type receiver, + String member, + Object[] args, + @CachedLibrary(limit = "3") InteropLibrary interop) + throws UnsupportedMessageException, + UnsupportedTypeException, + ArityException, + UnknownIdentifierException { + var method = findMethod(receiver, member); + if (method == null) { + throw UnknownIdentifierException.create(member); + } + var invokeFuncNode = buildInvokeFuncNode(method); + return doCached(receiver, member, args, member, method, invokeFuncNode); + } + + static Function findMethod(Type receiver, String name) { + return receiver.methods().get(name); + } + + static InvokeFunctionNode buildInvokeFuncNode(Function func) { + assert func != null; + var argumentInfos = func.getSchema().getArgumentInfos(); + var callArgInfos = new CallArgumentInfo[argumentInfos.length]; + for (var i = 0; i < argumentInfos.length; i++) { + var argInfo = argumentInfos[i]; + var callArgInfo = new CallArgumentInfo(argInfo.getName()); + callArgInfos[i] = callArgInfo; + } + return InvokeFunctionNode.build( + callArgInfos, DefaultsExecutionMode.EXECUTE, ArgumentsExecutionMode.EXECUTE); } } @@ -368,12 +454,15 @@ Object readMember(String member) throws UnknownIdentifierException { if (isProjectPrivate) { throw UnknownIdentifierException.create(member); } - var result = constructors.get(member); - if (result == null) { - throw UnknownIdentifierException.create(member); - } else { - return result; + var cons = constructors.get(member); + if (cons != null) { + return cons; } + var method = methods().get(member); + if (method != null) { + return method; + } + throw UnknownIdentifierException.create(member); } @ExportMessage @@ -412,4 +501,41 @@ private boolean isNothing(Node lib) { var b = EnsoContext.get(lib).getBuiltins(); return this == b.nothing(); } + + private Map methods() { + if (methods == null) { + CompilerDirectives.transferToInterpreter(); + var allMethods = new HashMap(); + var defScope = definitionScope.asModuleScope(); + var methodsFromThisScope = defScope.getMethodsForType(this); + if (methodsFromThisScope != null) { + methodsFromThisScope.forEach( + func -> { + var simpleName = simpleFuncName(func); + allMethods.put(simpleName, func); + }); + } + if (eigentype != null) { + var methodsFromEigenScope = eigentype.getDefinitionScope().getMethodsForType(eigentype); + if (methodsFromEigenScope != null) { + methodsFromEigenScope.forEach( + func -> { + var simpleName = simpleFuncName(func); + allMethods.put(simpleName, func); + }); + } + } + methods = allMethods; + } + return methods; + } + + private static String simpleFuncName(Function func) { + assert func.getName() != null; + if (func.getName().contains(".")) { + var items = func.getName().split("\\."); + return items[items.length - 1]; + } + return func.getName(); + } } diff --git a/std-bits/base/src/main/java/org/enso/base/CurrentEnsoProject.java b/std-bits/base/src/main/java/org/enso/base/CurrentEnsoProject.java index 5f387e21477b..343f29ddbff9 100644 --- a/std-bits/base/src/main/java/org/enso/base/CurrentEnsoProject.java +++ b/std-bits/base/src/main/java/org/enso/base/CurrentEnsoProject.java @@ -22,20 +22,13 @@ public static CurrentEnsoProject get() { if (!isCached) { Value ensoProject = EnsoMeta.callStaticModuleMethod("Standard.Base.Meta.Enso_Project", "enso_project"); - if (ensoProject.hasMember("name") - && ensoProject.hasMember("namespace") - && ensoProject.hasMember("root_path")) { - Value namespace = ensoProject.invokeMember("namespace"); - Value name = ensoProject.invokeMember("name"); - Value rootPath = ensoProject.invokeMember("root_path"); - if (namespace == null || name == null || rootPath == null) { - cached = null; - } else { - cached = - new CurrentEnsoProject(name.asString(), namespace.asString(), rootPath.asString()); - } - } else { + Value namespace = invokeMember("namespace", ensoProject); + Value name = invokeMember("name", ensoProject); + Value rootPath = invokeMember("path", invokeMember("root", ensoProject)); + if (namespace == null || name == null || rootPath == null) { cached = null; + } else { + cached = new CurrentEnsoProject(name.asString(), namespace.asString(), rootPath.asString()); } isCached = true; @@ -44,6 +37,15 @@ public static CurrentEnsoProject get() { return cached; } + private static Value invokeMember(String member, Value object, Object... args) { + var meta = object.getMetaObject(); + if (meta.hasMember(member)) { + return meta.invokeMember(member, object, args); + } else { + return null; + } + } + public String getName() { return name; }