Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Builtins expose Enso methods #11687

Open
wants to merge 46 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
f9a00c6
Add BuiltinsExposeMethodsTest
Akirathan Nov 27, 2024
587904a
typo in docs
Akirathan Nov 27, 2024
bdbe60f
Test iteartes all the builtins and checks that all methods are invoca…
Akirathan Nov 29, 2024
7f0d4bf
Add TypesExposeConstructorsTest
Akirathan Nov 29, 2024
59710b9
Merge branch 'develop' into wip/akirathan/11589-builtins-expose-methods
Akirathan Dec 3, 2024
48005e6
Naive implementation of BuiltinObject base class.
Akirathan Dec 4, 2024
477de23
Ref extends BuiltinObject
Akirathan Dec 4, 2024
86c360b
Builtin type anot processor ensures that a class must extend BuiltinO…
Akirathan Dec 4, 2024
bac459e
Enrich BuiltinsExposeMethodsTest
Akirathan Dec 4, 2024
a9681e3
All builtin types extend BuiltinObject
Akirathan Dec 4, 2024
ab7a246
Text is BuiltinObject
Akirathan Dec 4, 2024
4c9ea78
EnsoBigInteger is BuiltinObject
Akirathan Dec 4, 2024
e806f9f
BuiltinObject does no asserts in constructor
Akirathan Dec 4, 2024
3b98c60
ArrayProxy is BuiltinObject
Akirathan Dec 4, 2024
643aa36
Test skips host values and Nothing
Akirathan Dec 4, 2024
5a8ef4e
fmt
Akirathan Dec 4, 2024
7cd6469
Remove outdated test.
Akirathan Dec 4, 2024
475819a
EqualsComplexNode: Timezone and duration are not treated as object wi…
Akirathan Dec 4, 2024
7856324
Fix DebuggingEnsoTest - Date in js is date time, not date
Akirathan Dec 4, 2024
a00ef60
Fix DateTest - ensoDate now has members
Akirathan Dec 4, 2024
0658ca5
Add interop.readMember test to BuiltinsExposeMethodsTest
Akirathan Dec 4, 2024
c1d8f41
VectorSortTest is executed in context
Akirathan Dec 4, 2024
47125a7
Add tests that invoke builtin methods on particular builtin types
Akirathan Dec 4, 2024
e8ed818
member methods on BuiltinObject are behind TruffleBoundary
Akirathan Dec 4, 2024
7dfb427
Cache builtin type in BuiltinObject.
Akirathan Dec 4, 2024
193db45
Reuse context in DebuggingEnsoTest.
Akirathan Dec 5, 2024
934afe1
Merge branch 'develop' into wip/akirathan/11589-builtins-expose-methods
Akirathan Dec 10, 2024
8acb04a
Reuse hardcoded builtinName constants from annotation
Akirathan Dec 10, 2024
f187211
Move BuiltinObject to package org.enso.interpreter.runtime.builtin
Akirathan Dec 10, 2024
0cc0b72
Fix FQN of BuiltinObject in the annotation processor
Akirathan Dec 10, 2024
6f31ebf
Make exported messages on BuiltinObject final
Akirathan Dec 10, 2024
2462523
Update generated class names in Builtins.
Akirathan Dec 10, 2024
e441fab
Exported messages in BuiltinObject are not behind TruffleBoundary
Akirathan Dec 10, 2024
a77691c
[WIP] Add BuiltinsJavaInteropTest
Akirathan Dec 11, 2024
b6ec2ae
Storage.vectorizedOrFallbackBinaryMap accepts Value as parameter
Akirathan Dec 11, 2024
752ac90
Builtin types expose methods, not BuiltinObject
Akirathan Dec 12, 2024
3a3f302
BuiltinObject has no members
Akirathan Dec 12, 2024
6f40c65
Remove test Text.is_empty
Akirathan Dec 12, 2024
c34408b
Fix tests - invocation is done via static methods
Akirathan Dec 12, 2024
b21384a
Type.InvokeMember uses InvokeFunctionNode instead of UnresolvedSymbol
Akirathan Dec 12, 2024
70a18c6
fmt
Akirathan Dec 12, 2024
3969177
Type.InvokeMember prepends receiver argument
Akirathan Dec 12, 2024
a001eab
Test invocation of File.path
Akirathan Dec 12, 2024
def1f6b
Test invocation of Vector.to_text
Akirathan Dec 12, 2024
eb125ba
Method fetching is done behind truffle boundary
Akirathan Dec 12, 2024
0b5758c
Reassigning @CompilationFinal field should deoptimize
JaroslavTulach Dec 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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. */
Expand Down Expand Up @@ -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"
Expand All @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Value> 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<Value> 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
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 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 BuiltinsExposeMethodsTest {
private static Context ctx;

private final ValueWithType valueWithType;

public BuiltinsExposeMethodsTest(ValueWithType valueWithType) {
this.valueWithType = valueWithType;
}

private static Context ctx() {
if (ctx == null) {
ctx = ContextUtils.createDefaultContext();
}
return ctx;
}

@Parameters(name = "{index}: {0}")
public static Iterable<ValueWithType> generateBuiltinObjects() {
var valuesGenerator = ValuesGenerator.create(ctx(), Language.ENSO);
var builtinObjectsWithTypes = new ArrayList<ValueWithType>();
ContextUtils.executeInContext(
ctx(),
() -> {
valuesGenerator.allValues().stream()
.map(val -> new ValueWithType(val, getType(val)))
.filter(valWithType -> !shouldSkipType(valWithType.type))
.filter(valWithType -> !isPrimitive(valWithType.value))
.filter(valWithType -> !isHostValue(valWithType.value))
.forEach(builtinObjectsWithTypes::add);
return null;
});
return builtinObjectsWithTypes;
}

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(valueWithType, is(notNullValue()));
assertThat(valueWithType.type.isBuiltin(), is(true));
var typeDefScope = valueWithType.type.getDefinitionScope();
var methodsDefinedInScope = typeDefScope.getMethodsForType(valueWithType.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 " + valueWithType.type.getQualifiedName() + " should have members",
valueWithType.value.hasMembers(),
is(true));
assertThat(
"Member " + methodName + " should be present",
valueWithType.value.hasMember(methodName),
is(true));
assertThat(
"Member " + methodName + " should be invocable",
valueWithType.value.canInvokeMember(methodName),
is(true));
var interop = InteropLibrary.getUncached();
var unwrappedValue = ContextUtils.unwrapValue(ctx(), valueWithType.value);
var memberViaInterop = interop.readMember(unwrappedValue, methodName);
assertThat(
"Member via interop should be the same as in scope, but was: " + memberViaInterop,
memberViaInterop == methodInScope,
is(true));
}
}
return null;
});
}

private static boolean isPrimitive(Value object) {
var unwrapped = ContextUtils.unwrapValue(ctx(), object);
return unwrapped instanceof Long
|| unwrapped instanceof Boolean
|| unwrapped instanceof Integer
|| unwrapped instanceof Double;
}

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;
}

private static boolean isHostValue(Value value) {
var unwrapped = ContextUtils.unwrapValue(ctx(), value);
var ensoCtx = ContextUtils.leakContext(ctx());
return ensoCtx.isJavaPolyglotObject(unwrapped);
}

public record ValueWithType(Value value, Type type) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package org.enso.interpreter.test.builtins;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

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 should have a 'get' method",
interop.isMemberInvocable(refUnwrapped, "get"),
is(true));
var res = interop.invokeMember(refUnwrapped, "get");
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;
});
}

/**
* '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);
assertThat(
"Text should not be able to resolve 'reverse' method",
interop.isMemberInvocable(textUnwrapped, "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);
assertThat(
"Text should have a '+' method",
interop.isMemberInvocable(text1Unwrapped, "+"),
is(true));
var res = interop.invokeMember(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;
});
}

/**
* 'Text.is_empty' is not a builtin method, defined on a builtin type. It should be treated as a
* builtin method, thus be invocable via interop.
*/
@Test
public void invokeNonBuiltinMethodOnBuiltinType() {
var text = ContextUtils.evalModule(ctx, "main = 'Hello'");
ContextUtils.executeInContext(
ctx,
() -> {
var interop = InteropLibrary.getUncached();
var textUnwrapped = ContextUtils.unwrapValue(ctx, text);
assertThat(
"Text should have a 'is_empty' method",
interop.isMemberInvocable(textUnwrapped, "is_empty"),
is(true));
var res = interop.invokeMember(textUnwrapped, "is_empty");
assertThat("Text.is_empty should return a boolean", interop.isBoolean(res), is(true));
assertThat("Text.is_empty should return false", interop.asBoolean(res), is(false));
return null;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class DateTest extends InterpreterTest {
|""".stripMargin
val ensoDate = eval(code)
consumeOut shouldEqual List("2022", "4", "1", "2022-04-01")
ensoDate.getMemberKeys().size() shouldEqual 0
ensoDate.getMemberKeys().size() shouldNot be(0)
}
}
}
Loading
Loading