diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/ExecCompilerTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/ExecCompilerTest.java index f3c52c146d9c..87d340a15641 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/ExecCompilerTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/compiler/test/ExecCompilerTest.java @@ -567,7 +567,7 @@ public void resultOfConversionIsTypeChecked() throws Exception { ex.getMessage().toLowerCase(), AllOf.allOf(containsString("type"), containsString("error"))); var typeError = ex.getGuestObject(); - assertEquals("Expected type", "First_Type", typeError.getMember("expected").toString()); + assertEquals("Expected type", "First_Type", typeError.getMember("expected").asString()); assertEquals("Got wrong value", 42, typeError.getMember("actual").asInt()); } } diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/BigNumberTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/BigNumberTest.java index 88419a34f7fc..3f51b5d76ba4 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/BigNumberTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/BigNumberTest.java @@ -1,5 +1,7 @@ package org.enso.interpreter.test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -177,4 +179,39 @@ public void doubleBigInteger() throws Exception { var fourtyTwo = assertMul(6.0, new BigInteger("7")); assertEquals(42, fourtyTwo.asInt()); } + + @Test + public void everyValueSmallerThanIntegerMaxVal_IsPrimitiveInt() { + var almostMaxInt = Integer.toString(Integer.MAX_VALUE - 1); + var intVal = ContextUtils.evalModule(ctx, "main = " + almostMaxInt); + assertThat("Is a number", intVal.isNumber(), is(true)); + assertThat("Fits in int", intVal.fitsInInt(), is(true)); + assertThat("Fits in long", intVal.fitsInLong(), is(true)); + assertThat("Fits in double", intVal.fitsInDouble(), is(true)); + assertThat("Fits in big int", intVal.fitsInBigInteger(), is(true)); + } + + @Test + public void everyValueSmallerThanLongMaxVal_IsPrimitiveLong() { + var almostMaxLong = Long.toString(Long.MAX_VALUE - 1); + var longVal = ContextUtils.evalModule(ctx, "main = " + almostMaxLong); + assertThat("Is a number", longVal.isNumber(), is(true)); + assertThat("Does not fit in int", longVal.fitsInInt(), is(false)); + assertThat("Fits in long", longVal.fitsInLong(), is(true)); + // Does not fit in double, because it is not a power of 2 and therefore a precision would + // be lost if converted to double. + assertThat("Does not fit in double", longVal.fitsInDouble(), is(false)); + assertThat("Fits in big int", longVal.fitsInBigInteger(), is(true)); + } + + @Test + public void everyValueBiggerThanLongMaxVal_IsEnsoBigInt() { + // This number is bigger than Long.MAX_VALUE, and not a power of 2. + var bigIntVal = ContextUtils.evalModule(ctx, "main = 9223372036854775808"); + assertThat("Is a number", bigIntVal.isNumber(), is(true)); + assertThat("Does not fit in int", bigIntVal.fitsInInt(), is(false)); + assertThat("Does not fit in long", bigIntVal.fitsInLong(), is(false)); + assertThat("Does not fit in double (not a power of 2)", bigIntVal.fitsInDouble(), is(false)); + assertThat("Fits in big int", bigIntVal.fitsInBigInteger(), is(true)); + } } 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 80de875fe76e..f10b10c34598 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 @@ -17,10 +17,13 @@ import com.oracle.truffle.api.debug.SuspendedCallback; import com.oracle.truffle.api.debug.SuspendedEvent; import com.oracle.truffle.api.nodes.LanguageInfo; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.net.URI; import java.nio.file.Paths; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -42,15 +45,20 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; +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(); @Before public void initContext() { + out.reset(); engine = Engine.newBuilder() .allowExperimentalOptions(true) @@ -58,7 +66,9 @@ public void initContext() { RuntimeOptions.LANGUAGE_HOME_OVERRIDE, Paths.get("../../distribution/component").toFile().getAbsolutePath()) .option(RuntimeOptions.LOG_LEVEL, Level.WARNING.getName()) - .logHandler(System.err) + .logHandler(out) + .err(out) + .out(out) .build(); context = @@ -76,13 +86,26 @@ public void initContext() { } @After - public void disposeContext() { + public void disposeContext() throws IOException { context.close(); context = null; engine.close(); engine = null; } + /** Only print warnings from the compiler if a test fails. */ + @Rule + public TestWatcher testWatcher = + new TestWatcher() { + @Override + protected void failed(Throwable e, Description description) { + System.err.println("Test failed: " + description.getMethodName()); + System.err.println("Error: " + e.getMessage()); + System.err.println("Logs from the compiler and the engine: "); + System.err.println(out); + } + }; + private static void expectStackFrame( DebugStackFrame actualFrame, Map expectedValues) { Map actualValues = new HashMap<>(); @@ -246,6 +269,125 @@ public void testHostValues() { } } + /** + * Both {@code Date.new 2024 12 15} and {@code Date.parse "2024-12-15"} should be seen by the + * debugger as the exact same objects. Internally, the value from {@code Date.parse} is a host + * value. + */ + @Test + public void hostValueIsTreatedAsItsEnsoCounterpart() { + Value fooFunc = + createEnsoMethod( + """ + from Standard.Base import Date, Date_Time, Dictionary + polyglot java import java.lang.String + polyglot java import java.util.List as JList + polyglot java import java.util.Map as JMap + + foreign js js_date = ''' + return new Date(); + + foreign js js_str = ''' + return "Hello_World"; + + foreign js js_list = ''' + return [1, 2, 3]; + + foreign js js_map = ''' + let m = new Map(); + m.set('A', 1); + m.set('B', 2); + return m; + + foreign python py_list = ''' + return [1, 2, 3] + + foreign python py_dict = ''' + return {'A': 1, 'B': 2} + + 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" + str_enso = "Hello_World" + str_js = js_str + str_java = String.new "Hello_World" + list_enso = [1, 2, 3] + list_js = js_list + list_py = py_list + list_java = JList.of 1 2 3 + dict_enso = Dictionary.from_vector [["A", 1], ["B", 2]] + dict_js = js_map + dict_py = py_dict + dict_java = JMap.of "A" 1 "B" 2 + end = 42 + """, + "foo"); + + try (DebuggerSession session = + debugger.startSession( + (SuspendedEvent event) -> { + switch (event.getSourceSection().getCharacters().toString().strip()) { + case "end = 42" -> { + DebugScope scope = event.getTopStackFrame().getScope(); + + 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"); + assertSameProperties(ensoDateTime.getProperties(), javaDateTime.getProperties()); + + DebugValue ensoString = scope.getDeclaredValue("str_enso"); + DebugValue javaString = scope.getDeclaredValue("str_java"); + DebugValue jsString = scope.getDeclaredValue("str_js"); + assertSameProperties(ensoString.getProperties(), javaString.getProperties()); + assertSameProperties(ensoString.getProperties(), jsString.getProperties()); + + DebugValue ensoList = scope.getDeclaredValue("list_enso"); + DebugValue javaList = scope.getDeclaredValue("list_java"); + DebugValue jsList = scope.getDeclaredValue("list_js"); + DebugValue pyList = scope.getDeclaredValue("list_py"); + assertSameProperties(ensoList.getProperties(), javaList.getProperties()); + assertSameProperties(ensoList.getProperties(), jsList.getProperties()); + assertSameProperties(ensoList.getProperties(), pyList.getProperties()); + + DebugValue ensoDict = scope.getDeclaredValue("dict_enso"); + DebugValue javaDict = scope.getDeclaredValue("dict_java"); + DebugValue jsDict = scope.getDeclaredValue("dict_js"); + DebugValue pyDict = scope.getDeclaredValue("dict_py"); + assertSameProperties(ensoDict.getProperties(), javaDict.getProperties()); + assertSameProperties(ensoDict.getProperties(), jsDict.getProperties()); + assertSameProperties(ensoDict.getProperties(), pyDict.getProperties()); + } + } + event.getSession().suspendNextExecution(); + })) { + session.suspendNextExecution(); + fooFunc.execute(0); + } + } + + /** Asserts that the given values have same property names. */ + private void assertSameProperties( + Collection expectedProps, Collection actualProps) { + if (expectedProps == null) { + assertThat(actualProps, anyOf(empty(), nullValue())); + return; + } + assertThat(actualProps.size(), is(expectedProps.size())); + var expectedPropNames = + expectedProps.stream().map(DebugValue::getName).collect(Collectors.toUnmodifiableSet()); + var actualPropNames = + actualProps.stream().map(DebugValue::getName).collect(Collectors.toUnmodifiableSet()); + assertThat(actualPropNames, is(expectedPropNames)); + } + @Test public void testHostValueAsAtomField() { Value fooFunc = diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/WarningsTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/WarningsTest.java index 3fc39252597d..129956012752 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/WarningsTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/WarningsTest.java @@ -3,6 +3,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -102,6 +103,17 @@ public void wrapAndUnwrap() { fail("One shall not be created WithWarnings without any warnings " + without); } + @Test + public void withWarningsDelegatesToMetaObject() { + var warning42 = wrap.execute("warn:1", "Text"); + var meta = warning42.getMetaObject(); + assertThat( + "Value (" + warning42 + ") wrapped in warning must have a meta object", + meta, + is(notNullValue())); + assertThat(meta.toString(), containsString("Text")); + } + @Test public void warningIsAnException() { var warning42 = wrap.execute("warn:1", 42); diff --git a/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/semantic/TypeSignaturesTest.scala b/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/semantic/TypeSignaturesTest.scala index 3bab4f826e92..1191d4676478 100644 --- a/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/semantic/TypeSignaturesTest.scala +++ b/engine/runtime-integration-tests/src/test/scala/org/enso/compiler/test/semantic/TypeSignaturesTest.scala @@ -243,7 +243,7 @@ class TypeSignaturesTest ) } - "XX resolve imported names" in { + "resolve imported names" in { val code = """ |from project.Util import all diff --git a/engine/runtime/src/main/java/org/enso/interpreter/EnsoLanguage.java b/engine/runtime/src/main/java/org/enso/interpreter/EnsoLanguage.java index 998276aa4551..4d52bc042670 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/EnsoLanguage.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/EnsoLanguage.java @@ -9,11 +9,15 @@ import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.instrumentation.ProvidedTags; import com.oracle.truffle.api.instrumentation.StandardTags; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.InvalidArrayIndexException; +import com.oracle.truffle.api.interop.UnsupportedMessageException; import com.oracle.truffle.api.nodes.ExecutableNode; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.nodes.RootNode; import java.io.ByteArrayOutputStream; import java.io.PrintStream; +import java.time.ZoneId; import java.util.List; import java.util.Objects; import org.enso.common.LanguageInfo; @@ -31,13 +35,30 @@ import org.enso.interpreter.node.EnsoRootNode; import org.enso.interpreter.node.ExpressionNode; import org.enso.interpreter.node.ProgramRootNode; +import org.enso.interpreter.node.callable.resolver.HostMethodCallNode; +import org.enso.interpreter.node.callable.resolver.MethodResolverNode; import org.enso.interpreter.runtime.EnsoContext; import org.enso.interpreter.runtime.IrToTruffle; +import org.enso.interpreter.runtime.callable.UnresolvedSymbol; +import org.enso.interpreter.runtime.data.EnsoDate; +import org.enso.interpreter.runtime.data.EnsoDateTime; +import org.enso.interpreter.runtime.data.EnsoDuration; +import org.enso.interpreter.runtime.data.EnsoObject; +import org.enso.interpreter.runtime.data.EnsoTimeOfDay; +import org.enso.interpreter.runtime.data.EnsoTimeZone; import org.enso.interpreter.runtime.data.atom.AtomNewInstanceNode; +import org.enso.interpreter.runtime.data.hash.EnsoHashMap; +import org.enso.interpreter.runtime.data.hash.HashMapInsertNode; +import org.enso.interpreter.runtime.data.hash.HashMapToVectorNode; +import org.enso.interpreter.runtime.data.text.Text; +import org.enso.interpreter.runtime.data.vector.ArrayLikeAtNode; +import org.enso.interpreter.runtime.data.vector.ArrayLikeHelpers; +import org.enso.interpreter.runtime.data.vector.ArrayLikeLengthNode; import org.enso.interpreter.runtime.instrument.NotificationHandler; import org.enso.interpreter.runtime.instrument.NotificationHandler.Forwarder; import org.enso.interpreter.runtime.instrument.NotificationHandler.TextMode$; import org.enso.interpreter.runtime.instrument.Timer; +import org.enso.interpreter.runtime.number.EnsoBigInteger; import org.enso.interpreter.runtime.state.ExecutionEnvironment; import org.enso.interpreter.runtime.tag.AvoidIdInstrumentationTag; import org.enso.interpreter.runtime.tag.IdentifiedTag; @@ -51,6 +72,8 @@ import org.graalvm.options.OptionDescriptors; import org.graalvm.options.OptionKey; import org.graalvm.options.OptionType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The root of the Enso implementation. @@ -91,6 +114,8 @@ public final class EnsoLanguage extends TruffleLanguage { private final ContextThreadLocal executionEnvironment = locals.createContextThreadLocal((ctx, thread) -> new ExecutionEnvironment[1]); + private static final Logger logger = LoggerFactory.getLogger(EnsoLanguage.class); + public static EnsoLanguage get(Node node) { return REFERENCE.get(node); } @@ -365,14 +390,85 @@ protected Object getScope(EnsoContext context) { return context.getTopScope(); } - /** Conversions of primitive values */ + /** Conversion of foreign/polyglot values to their Enso builtin counterparts. */ @Override - protected Object getLanguageView(EnsoContext context, Object value) { + public Object getLanguageView(EnsoContext context, Object value) { if (value instanceof Boolean b) { var bool = context.getBuiltins().bool(); var cons = b ? bool.getTrue() : bool.getFalse(); return AtomNewInstanceNode.getUncached().newInstance(cons); } + if (value instanceof EnsoObject ensoObject) { + return ensoObject; + } + var interop = InteropLibrary.getUncached(); + // We want to know if the `value` can be converted to some Enso builtin type. + // In order to do that, we are trying to infer PolyglotCallType of `value.to` method. + var anyModuleScope = context.getBuiltins().any().getDefinitionScope(); + var unresolvedSymbol = UnresolvedSymbol.build("to", anyModuleScope); + var methodResolverNode = MethodResolverNode.getUncached(); + var callType = + HostMethodCallNode.getPolyglotCallType( + value, unresolvedSymbol, interop, methodResolverNode); + try { + switch (callType) { + case CONVERT_TO_DATE -> { + var localDate = interop.asDate(value); + return new EnsoDate(localDate); + } + case CONVERT_TO_ARRAY -> { + return ArrayLikeHelpers.asVectorFromArray(value); + } + case CONVERT_TO_BIG_INT -> { + // long and doubles are valid primitive types in Enso + if (interop.fitsInLong(value)) { + return new LanguageViewWrapper(interop.asLong(value)); + } else if (interop.fitsInDouble(value)) { + return new LanguageViewWrapper(interop.asDouble(value)); + } else { + return new EnsoBigInteger(interop.asBigInteger(value)); + } + } + case CONVERT_TO_DATE_TIME, CONVERT_TO_ZONED_DATE_TIME -> { + var date = interop.asDate(value); + var time = interop.asTime(value); + var zonedDt = date.atTime(time).atZone(ZoneId.systemDefault()); + return new EnsoDateTime(zonedDt); + } + case CONVERT_TO_DURATION -> { + return new EnsoDuration(interop.asDuration(value)); + } + case CONVERT_TO_HASH_MAP -> { + var ensoHash = EnsoHashMap.empty(); + var insertNode = HashMapInsertNode.getUncached(); + var vec = HashMapToVectorNode.getUncached().execute(value); + var arrayAtNode = ArrayLikeAtNode.getUncached(); + var size = ArrayLikeLengthNode.getUncached().executeLength(vec); + for (long i = 0; i < size; i++) { + var pair = arrayAtNode.executeAt(vec, i); + var key = arrayAtNode.executeAt(pair, 0); + var val = arrayAtNode.executeAt(pair, 1); + ensoHash = insertNode.execute(null, ensoHash, key, val); + } + return ensoHash; + } + case CONVERT_TO_TEXT -> { + return Text.create(interop.asString(value)); + } + case CONVERT_TO_TIME_ZONE -> { + return new EnsoTimeZone(interop.asTimeZone(value)); + } + case CONVERT_TO_TIME_OF_DAY -> { + var time = interop.asTime(value); + return new EnsoTimeOfDay(time); + } + case NOT_SUPPORTED -> { + return null; + } + } + } catch (UnsupportedMessageException | InvalidArrayIndexException e) { + logger.warn("Unexpected exception", e); + } return null; } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/LanguageViewWrapper.java b/engine/runtime/src/main/java/org/enso/interpreter/LanguageViewWrapper.java new file mode 100644 index 000000000000..c6d122218fa8 --- /dev/null +++ b/engine/runtime/src/main/java/org/enso/interpreter/LanguageViewWrapper.java @@ -0,0 +1,34 @@ +package org.enso.interpreter; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.library.CachedLibrary; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; +import org.enso.interpreter.runtime.data.EnsoObject; + +/** + * Just a wrapper for a value providing {@link + * com.oracle.truffle.api.interop.InteropLibrary#hasLanguage(Object)} message implementation. + */ +@ExportLibrary(value = InteropLibrary.class, delegateTo = "delegate") +final class LanguageViewWrapper extends EnsoObject { + final Object delegate; + + LanguageViewWrapper(Object delegate) { + this.delegate = delegate; + } + + @ExportMessage + Object toDisplayString( + boolean allowSideEffects, @CachedLibrary("this.delegate") InteropLibrary interop) { + return interop.toDisplayString(delegate, allowSideEffects); + } + + @Override + @TruffleBoundary + @ExportMessage.Ignore + public Object toDisplayString(boolean allowSideEffects) { + return toDisplayString(allowSideEffects, InteropLibrary.getUncached()); + } +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/resolver/MethodResolverNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/resolver/MethodResolverNode.java index 40536bcc6ee1..2a7bef2d84c1 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/resolver/MethodResolverNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/resolver/MethodResolverNode.java @@ -18,6 +18,10 @@ public abstract class MethodResolverNode extends Node { protected static final int CACHE_SIZE = 10; + public static MethodResolverNode getUncached() { + return MethodResolverNodeGen.getUncached(); + } + @NonIdempotent EnsoContext getContext() { return EnsoContext.get(this); diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/DebugLocalScope.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/DebugLocalScope.java index 8a8bf4f4cfd5..ad8164569333 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/DebugLocalScope.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/scope/DebugLocalScope.java @@ -16,7 +16,9 @@ import java.util.Map.Entry; import java.util.stream.Collectors; import org.enso.compiler.pass.analyse.FramePointer; +import org.enso.interpreter.EnsoLanguage; import org.enso.interpreter.node.EnsoRootNode; +import org.enso.interpreter.runtime.EnsoContext; import org.enso.interpreter.runtime.callable.function.Function; import org.enso.interpreter.runtime.data.EnsoObject; import org.enso.interpreter.runtime.error.DataflowError; @@ -176,12 +178,17 @@ boolean isMemberReadable(String memberName) { @ExportMessage @TruffleBoundary Object readMember(String member, @CachedLibrary("this") InteropLibrary interop) - throws UnknownIdentifierException { + throws UnknownIdentifierException, UnsupportedMessageException { if (!allBindings.containsKey(member)) { throw UnknownIdentifierException.create(member); } FramePointer framePtr = allBindings.get(member); var value = getValue(frame, framePtr); + if (value != null) { + var ensoLang = EnsoLanguage.get(interop); + var ensoCtx = EnsoContext.get(interop); + value = ensoLang.getLanguageView(ensoCtx, value); + } return value != null ? value : DataflowError.UNINITIALIZED; }