From e8f781afbfe58537c730caa2c4c3e7a79f36b7b7 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Sat, 21 Dec 2024 08:07:54 +0100 Subject: [PATCH] Symetric, transitive and reflexive equality for intersection types (#11897) Fixes #11845 by comparing all the types an `EnsoMultiValue` _has been cast to_. --- CHANGELOG.md | 2 + docs/types/intersection-types.md | 24 +++- .../meta/TypeOfNodeMultiValueTest.java | 3 + .../org/enso/interpreter/test/AnyToTest.java | 120 ++++++++++++++++++ .../test/EnsoMultiValueInteropTest.java | 6 + .../test/EqualsMultiValueTest.java | 29 +++-- .../interpreter/test/ValuesGenerator.java | 40 ++++++ .../interpreter/test/hash/HashCodeTest.java | 1 + .../node/callable/InvokeConversionNode.java | 43 +++++-- .../node/callable/InvokeMethodNode.java | 11 +- .../builtin/meta/EqualsSimpleNode.java | 59 ++++++++- .../expression/builtin/meta/HashCodeNode.java | 31 ++++- .../src/Semantic/Conversion_Spec.enso | 28 ++-- .../Semantic/Multi_Value_Convert_Spec.enso | 67 ++++++---- 14 files changed, 390 insertions(+), 74 deletions(-) create mode 100644 engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/AnyToTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d4db090dd76..4dd78add11bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,10 +17,12 @@ - A constructor or type definition with a single inline argument definition was previously allowed to use spaces in the argument definition without parentheses. [This is now a syntax error.][11856] +- Symetric, transitive and reflexive [equality for intersection types][11897] [11777]: https://github.com/enso-org/enso/pull/11777 [11600]: https://github.com/enso-org/enso/pull/11600 [11856]: https://github.com/enso-org/enso/pull/11856 +[11897]: https://github.com/enso-org/enso/pull/11897 # Next Release diff --git a/docs/types/intersection-types.md b/docs/types/intersection-types.md index dd053d845709..603db3e40d17 100644 --- a/docs/types/intersection-types.md +++ b/docs/types/intersection-types.md @@ -68,7 +68,7 @@ Just as demonstrated at https://github.com/enso-org/enso/commit/3d8a0e1b90b20cfdfe5da8d2d3950f644a4b45b8#diff-c6ef852899778b52ce6a11ebf9564d102c273021b212a4848b7678e120776287R23 --> -## Narrowing Type Check +### Narrowing Type Check When an _intersection type_ value is being downcast to _some of the types it already represents_, these types become its _visible_ types. Any additional @@ -160,9 +160,9 @@ Table.join self right:Table -> Table = ... Such a `Table&Column` value can be returned from the above `Table.join` function and while having only `Table` behavior by default, still being able to be -explicitly casted by the visual environment to `Column`. +explicitly cast by the visual environment to `Column`. -## Converting Type Check +### Converting Type Check When an _intersection type_ is being checked against a type it doesn't represent, any of its component types can be used for @@ -180,3 +180,21 @@ case it looses its `Float` type and `ct:Float` would fail. In short: when a [conversion](../syntax/conversions.md) is needed to satisfy a type check a new value is created to satisfy just the types requested in the check. + +## Equality & Hash Code + +A value of an intersection type is equal with other value, if all values _it has +been cast to_ are equal to the other value. E.g. a value of `Complex&Float` is +equal to some other value only if its `Complex` part and `Float` part are equal +to the other value. The _hidden_ types of a value (e.g. those that it _can be +cast to_, if any) aren't considered in the equality check. + +The order of types isn't important for equality. E.g. `Complex&Float` value can +be equal to `Float&Complex` if the individual components (values _it has been +cast to_) match. As implied by (custom) +[equality rules](../syntax/functions.md#custom-equality) the `hash` of a value +of _intersection type_ must thus be a sum of `hash` values of all the values it +_has been cast to_. As a special case any value wrapped into an _intersection +type_, but _cast down_ to the original type is `==` and has the same `hash` as +the original value. E.g. `4.2 : Complex&Float : Float` is `==` and has the same +`hash` as `4.2` (in spite it _can be cast to_ `Complex`). diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/node/expression/builtin/meta/TypeOfNodeMultiValueTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/node/expression/builtin/meta/TypeOfNodeMultiValueTest.java index 835c24f62f24..b82ec6b2acf9 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/node/expression/builtin/meta/TypeOfNodeMultiValueTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/node/expression/builtin/meta/TypeOfNodeMultiValueTest.java @@ -88,6 +88,9 @@ private static void registerValue( if (!polyValue.isNull()) { assertTrue("Type of " + polyValue + " is " + t, t.isMetaObject()); var rawValue = ContextUtils.unwrapValue(ctx(), polyValue); + if (rawValue instanceof EnsoMultiValue) { + return; + } var rawType = ContextUtils.unwrapValue(ctx(), t); if (rawType instanceof Type type) { var singleMultiValue = EnsoMultiValue.create(new Type[] {type}, 1, new Object[] {rawValue}); diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/AnyToTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/AnyToTest.java new file mode 100644 index 000000000000..20311946ad6e --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/AnyToTest.java @@ -0,0 +1,120 @@ +package org.enso.interpreter.test; + +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import org.enso.interpreter.runtime.data.EnsoMultiValue; +import org.enso.interpreter.runtime.data.Type; +import org.enso.interpreter.runtime.data.text.Text; +import org.enso.test.utils.ContextUtils; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Source; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +public class AnyToTest { + private static Context ctx; + + private static final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + @BeforeClass + public static void initCtx() { + ctx = ContextUtils.createDefaultContext(out); + } + + @AfterClass + public static void disposeCtx() { + ctx.close(); + ctx = null; + } + + @Before + public void resetOutput() { + out.reset(); + } + + private String getStdOut() { + return out.toString(StandardCharsets.UTF_8); + } + + @Test + public void multiValueToInteger() throws Exception { + var ensoCtx = ContextUtils.leakContext(ctx); + var types = + new Type[] {ensoCtx.getBuiltins().number().getInteger(), ensoCtx.getBuiltins().text()}; + var code = + """ + from Standard.Base import all + + private eq a b = a == b + + conv style v = case style of + 0 -> v.to Integer + 1 -> v:Integer + 99 -> eq + + """; + var conv = + ContextUtils.evalModule(ctx, Source.newBuilder("enso", code, "conv.enso").build(), "conv"); + var both = EnsoMultiValue.create(types, types.length, new Object[] {2L, Text.create("Two")}); + var eq = + ContextUtils.executeInContext( + ctx, + () -> { + var bothValue = ctx.asValue(both); + var asIntegerTo = conv.execute(0, bothValue); + var asIntegerCast = conv.execute(1, bothValue); + var equals = conv.execute(99, null); + return equals.execute(asIntegerTo, asIntegerCast); + }); + assertTrue("Any.to and : give the same result", eq.asBoolean()); + } + + @Test + @Ignore + public void multiValueToText() throws Exception { + multiValueToText(2); + } + + @Test + @Ignore + public void multiValueToTextHidden() throws Exception { + multiValueToText(1); + } + + private void multiValueToText(int dispatchLength) throws Exception { + var ensoCtx = ContextUtils.leakContext(ctx); + var types = + new Type[] {ensoCtx.getBuiltins().number().getInteger(), ensoCtx.getBuiltins().text()}; + var code = + """ + from Standard.Base import all + + private eq a b = a == b + + conv style:Integer v = case style of + 2 -> v.to Text + 3 -> v:Text + 99 -> eq + + """; + var conv = + ContextUtils.evalModule(ctx, Source.newBuilder("enso", code, "conv.enso").build(), "conv"); + var both = EnsoMultiValue.create(types, dispatchLength, new Object[] {2L, Text.create("Two")}); + var eq = + ContextUtils.executeInContext( + ctx, + () -> { + var bothValue = ctx.asValue(both); + var asIntegerCast = conv.execute(3, bothValue); + var asIntegerTo = conv.execute(2, bothValue); + var equals = conv.execute(99, null); + return equals.execute(asIntegerTo, asIntegerCast); + }); + assertTrue("Any.to and : give the same result", eq.asBoolean()); + } +} diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/EnsoMultiValueInteropTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/EnsoMultiValueInteropTest.java index dad3a6100481..530122ceb651 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/EnsoMultiValueInteropTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/EnsoMultiValueInteropTest.java @@ -57,7 +57,13 @@ private static void registerValue( var rawT2 = ContextUtils.unwrapValue(ctx(), t2); if (rawT1 instanceof Type typ1 && rawT2 instanceof Type typ2) { var r1 = ContextUtils.unwrapValue(ctx, v1); + if (r1 instanceof EnsoMultiValue) { + return; + } var r2 = ContextUtils.unwrapValue(ctx, v2); + if (r2 instanceof EnsoMultiValue) { + return; + } var both = EnsoMultiValue.create(new Type[] {typ1, typ2}, 2, new Object[] {r1, r2}); data.add(new Object[] {both}); } diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/EqualsMultiValueTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/EqualsMultiValueTest.java index 57d60662bb43..90b001ebd16f 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/EqualsMultiValueTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/EqualsMultiValueTest.java @@ -111,15 +111,24 @@ public void testEqualityIntegerAndMultiValueWithBoth() { var intType = builtins.number().getInteger(); var textText = builtins.text(); var hi = Text.create("Hi"); - var fourExtraText = + var textFour = EnsoMultiValue.create(new Type[] {textText, intType}, 2, new Object[] {hi, 4L}); - - assertTrue("4 == 4t", equalityCheck(4L, fourExtraText)); - assertFalse("5 != 4t", equalityCheck(5L, fourExtraText)); - assertTrue("4t == 4", equalityCheck(fourExtraText, 4L)); - assertFalse("4t != 5", equalityCheck(fourExtraText, 5L)); - assertTrue("4t == 'Hi'", equalityCheck(fourExtraText, hi)); - assertTrue("'Hi' == 4t", equalityCheck(hi, fourExtraText)); + var textFive = + EnsoMultiValue.create(new Type[] {textText, intType}, 2, new Object[] {hi, 5L}); + var fourText = + EnsoMultiValue.create(new Type[] {intType, textText}, 2, new Object[] {4L, hi}); + + assertFalse("4 != t", equalityCheck(4L, hi)); + assertFalse("4 != 4t", equalityCheck(4L, textFour)); + assertFalse("5 != 4t", equalityCheck(5L, textFour)); + assertFalse("5t != 4t", equalityCheck(textFive, textFour)); + assertFalse("4t != 4", equalityCheck(textFour, 4L)); + assertFalse("4t != 5", equalityCheck(textFour, 5L)); + assertFalse("4t != 'Hi'", equalityCheck(textFour, hi)); + assertFalse("'Hi' != 4t", equalityCheck(hi, textFour)); + + assertTrue("t4 == 4t", equalityCheck(textFour, fourText)); + assertTrue("4t == t4", equalityCheck(fourText, textFour)); return null; }); @@ -137,9 +146,9 @@ public void testEqualityIntegerAndMultiValueWithIntText() { EnsoMultiValue.create( new Type[] {intType, textText}, 2, new Object[] {4L, Text.create("Hi")}); - assertTrue("4 == 4t", equalityCheck(4L, fourExtraText)); + assertFalse("4 != 4t", equalityCheck(4L, fourExtraText)); assertFalse("5 != 4t", equalityCheck(5L, fourExtraText)); - assertTrue("4t == 4", equalityCheck(fourExtraText, 4L)); + assertFalse("4t != 4", equalityCheck(fourExtraText, 4L)); assertFalse("4t != 5", equalityCheck(fourExtraText, 5L)); return null; diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/ValuesGenerator.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/ValuesGenerator.java index 4b23c3300299..a163a184655e 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/ValuesGenerator.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/ValuesGenerator.java @@ -25,6 +25,11 @@ import java.util.TimeZone; import org.enso.common.MethodNames; import org.enso.common.MethodNames.Module; +import org.enso.interpreter.node.expression.foreign.HostValueToEnsoNode; +import org.enso.interpreter.runtime.data.EnsoMultiValue; +import org.enso.interpreter.runtime.data.EnsoObject; +import org.enso.interpreter.runtime.data.Type; +import org.enso.test.utils.ContextUtils; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.PolyglotException; import org.graalvm.polyglot.Value; @@ -865,6 +870,41 @@ public List problemBehaviors() { return collect; } + public List numbersMultiText() { + var leak = ContextUtils.leakContext(ctx); + var numberTextTypes = + new Type[] { + leak.getBuiltins().number().getInteger(), leak.getBuiltins().text(), + }; + var textNumberTypes = + new Type[] { + leak.getBuiltins().text(), leak.getBuiltins().number().getInteger(), + }; + var collect = new ArrayList(); + var toEnso = HostValueToEnsoNode.getUncached(); + for (var n : numbers()) { + for (var t : textual()) { + var rawN = toEnso.execute(ContextUtils.unwrapValue(ctx, n)); + var rawT = ContextUtils.unwrapValue(ctx, t); + if (!(rawT instanceof EnsoObject)) { + continue; + } + addMultiToCollect(collect, numberTextTypes, 2, rawN, rawT); + addMultiToCollect(collect, numberTextTypes, 1, rawN, rawT); + addMultiToCollect(collect, textNumberTypes, 2, rawT, rawN); + addMultiToCollect(collect, textNumberTypes, 1, rawT, rawN); + } + } + return collect; + } + + private void addMultiToCollect( + List collect, Type[] types, int dispatchTypes, Object... values) { + var raw = EnsoMultiValue.create(types, dispatchTypes, values); + var wrap = ctx.asValue(raw); + collect.add(wrap); + } + public List noWrap() { var collect = new ArrayList(); if (languages.contains(Language.ENSO)) { diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/hash/HashCodeTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/hash/HashCodeTest.java index 558c3f85deb2..31f3d28d2bbb 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/hash/HashCodeTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/hash/HashCodeTest.java @@ -73,6 +73,7 @@ private static Object[] fetchAllUnwrappedValues() { values.addAll(valGenerator.numbers()); values.addAll(valGenerator.booleans()); values.addAll(valGenerator.textual()); + values.addAll(valGenerator.numbersMultiText()); values.addAll(valGenerator.arrayLike()); values.addAll(valGenerator.vectors()); values.addAll(valGenerator.maps()); diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/InvokeConversionNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/InvokeConversionNode.java index 2d0e4777fbad..4675a3d88882 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/InvokeConversionNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/InvokeConversionNode.java @@ -100,7 +100,10 @@ private Type extractType(Object self) { return extractType(this, self); } - static boolean hasType(TypeOfNode typeOfNode, Object value) { + static boolean hasTypeNoMulti(TypeOfNode typeOfNode, Object value) { + if (value instanceof EnsoMultiValue) { + return false; + } return typeOfNode.hasType(value); } @@ -109,7 +112,11 @@ static boolean isDataflowError(Object value) { } @Specialization( - guards = {"hasType(dispatch, that)", "!isDataflowError(self)", "!isDataflowError(that)"}) + guards = { + "hasTypeNoMulti(dispatch, that)", + "!isDataflowError(self)", + "!isDataflowError(that)" + }) Object doConvertFrom( VirtualFrame frame, State state, @@ -181,15 +188,23 @@ Object doMultiValue( Object self, EnsoMultiValue that, Object[] arguments, + @Shared("typeOfNode") @Cached TypeOfNode dispatch, @Cached EnsoMultiValue.CastToNode castTo) { var type = extractType(self); - var result = castTo.findTypeOrNull(type, that, true, true); - if (result == null) { - throw new PanicException( - EnsoContext.get(this).getBuiltins().error().makeNoSuchConversion(type, self, conversion), - this); + var hasBeenCastTo = dispatch.findAllTypesOrNull(that, false); + if (hasBeenCastTo != null) { + for (var t : hasBeenCastTo) { + var val = castTo.findTypeOrNull(t, that, false, false); + assert val != null; + var result = execute(frame, state, conversion, self, val, arguments); + if (result != null) { + return result; + } + } } - return result; + throw new PanicException( + EnsoContext.get(this).getBuiltins().error().makeNoSuchConversion(type, self, conversion), + this); } @Specialization @@ -265,7 +280,7 @@ Object doConvertText( @Specialization( guards = { - "!hasType(typeOfNode, that)", + "!hasTypeNoMulti(typeOfNode, that)", "!interop.isTime(that)", "interop.isDate(that)", }) @@ -287,7 +302,7 @@ Object doConvertDate( @Specialization( guards = { - "!hasType(typeOfNode, that)", + "!hasTypeNoMulti(typeOfNode, that)", "interop.isTime(that)", "!interop.isDate(that)", }) @@ -309,7 +324,7 @@ Object doConvertTime( @Specialization( guards = { - "!hasType(typeOfNode, that)", + "!hasTypeNoMulti(typeOfNode, that)", "interop.isTime(that)", "interop.isDate(that)", }) @@ -331,7 +346,7 @@ Object doConvertDateTime( @Specialization( guards = { - "!hasType(typeOfNode, that)", + "!hasTypeNoMulti(typeOfNode, that)", "interop.isDuration(that)", }) Object doConvertDuration( @@ -352,7 +367,7 @@ Object doConvertDuration( @Specialization( guards = { - "!hasType(typeOfNode, thatMap)", + "!hasTypeNoMulti(typeOfNode, thatMap)", "interop.hasHashEntries(thatMap)", }) Object doConvertMap( @@ -374,7 +389,7 @@ Object doConvertMap( return invokeFunctionNode.execute(function, frame, state, arguments); } - @Specialization(guards = {"!hasType(methods, that)", "!interop.isString(that)"}) + @Specialization(guards = {"!hasTypeNoMulti(methods, that)", "!interop.isString(that)"}) Object doFallback( VirtualFrame frame, State state, diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/InvokeMethodNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/InvokeMethodNode.java index 1d8c977c6e2c..179bef3b3e24 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/InvokeMethodNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/InvokeMethodNode.java @@ -299,10 +299,13 @@ Object doMultiValue( @Cached EnsoMultiValue.CastToNode castTo) { var fnAndType = self.resolveSymbol(methodResolverNode, symbol); if (fnAndType != null) { - var unwrapSelf = castTo.findTypeOrNull(fnAndType.getRight(), self, false, false); - if (unwrapSelf != null) { - assert arguments[0] == self; - arguments[0] = unwrapSelf; + var ctx = EnsoContext.get(this); + if (ctx.getBuiltins().any() != fnAndType.getRight()) { + var unwrapSelf = castTo.findTypeOrNull(fnAndType.getRight(), self, false, false); + if (unwrapSelf != null) { + assert arguments[0] == self; + arguments[0] = unwrapSelf; + } } return invokeFunctionNode.execute(fnAndType.getLeft(), frame, state, arguments); } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/EqualsSimpleNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/EqualsSimpleNode.java index aaf800c5d961..2846a92baedc 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/EqualsSimpleNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/EqualsSimpleNode.java @@ -155,7 +155,7 @@ EqualsAndInfo equalsDoubleText(double self, Text other) { return EqualsAndInfo.FALSE; } - @Specialization + @Specialization(guards = "!isMulti(other)") EqualsAndInfo equalsDoubleInterop( double self, Object other, @@ -323,7 +323,54 @@ EqualsAndInfo equalsAtoms( } } + static boolean isMulti(Object obj) { + return obj instanceof EnsoMultiValue; + } + @Specialization + EqualsAndInfo equalsMultiValueMultiValue( + VirtualFrame frame, + EnsoMultiValue self, + EnsoMultiValue other, + @Shared("multiCast") @Cached EnsoMultiValue.CastToNode castNode, + @Shared("multiType") @Cached TypeOfNode typesNode, + @Shared("multiEquals") @Cached EqualsSimpleNode delegate) { + if (self == other) { + return EqualsAndInfo.TRUE; + } + + var typesSelf = typesNode.findAllTypesOrNull(self, false); + var typesOther = typesNode.findAllTypesOrNull(other, false); + assert typesSelf != null; + assert typesOther != null; + for (var t : typesSelf) { + var selfValue = castNode.findTypeOrNull(t, self, false, false); + assert selfValue != null; + var otherValue = castNode.findTypeOrNull(t, other, false, false); + if (otherValue == null) { + return EqualsAndInfo.FALSE; + } + var res = delegate.execute(frame, selfValue, otherValue); + if (!res.isTrue()) { + return res; + } + } + for (var t : typesOther) { + var selfValue = castNode.findTypeOrNull(t, self, false, false); + if (selfValue == null) { + return EqualsAndInfo.FALSE; + } + var otherValue = castNode.findTypeOrNull(t, other, false, false); + assert otherValue != null; + var res = delegate.execute(frame, selfValue, otherValue); + if (!res.isTrue()) { + return res; + } + } + return EqualsAndInfo.TRUE; + } + + @Specialization(guards = "!isMulti(other)") EqualsAndInfo equalsMultiValue( VirtualFrame frame, EnsoMultiValue self, @@ -339,14 +386,14 @@ EqualsAndInfo equalsMultiValue( continue; } var res = delegate.execute(frame, value, other); - if (res.isTrue()) { + if (!res.isTrue()) { return res; } } - return EqualsAndInfo.FALSE; + return EqualsAndInfo.TRUE; } - @Specialization + @Specialization(guards = "!isMulti(self)") EqualsAndInfo equalsMultiValueReversed( VirtualFrame frame, Object self, @@ -440,6 +487,10 @@ static boolean isPrimitiveValue(Object object) { return object instanceof Boolean || object instanceof Long || object instanceof Double; } + static boolean isEnsoObject(Object v) { + return v instanceof EnsoObject; + } + static boolean isNotMulti(Object v) { return !(v instanceof EnsoMultiValue); } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/HashCodeNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/HashCodeNode.java index 1127cf61beef..1af1a8026f45 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/HashCodeNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/HashCodeNode.java @@ -40,11 +40,13 @@ import org.enso.interpreter.runtime.callable.argument.CallArgumentInfo; import org.enso.interpreter.runtime.callable.function.Function; import org.enso.interpreter.runtime.data.EnsoFile; +import org.enso.interpreter.runtime.data.EnsoMultiValue; import org.enso.interpreter.runtime.data.Type; import org.enso.interpreter.runtime.data.atom.Atom; import org.enso.interpreter.runtime.data.atom.AtomConstructor; import org.enso.interpreter.runtime.data.atom.StructsLibrary; import org.enso.interpreter.runtime.data.text.Text; +import org.enso.interpreter.runtime.library.dispatch.TypeOfNode; import org.enso.interpreter.runtime.library.dispatch.TypesLibrary; import org.enso.interpreter.runtime.number.EnsoBigInteger; import org.enso.interpreter.runtime.scope.ModuleScope; @@ -106,7 +108,7 @@ long hashCodeForBigInteger( return hashCodeForDouble(bigInteger.getValue().doubleValue()); } - @Specialization(guards = {"interop.fitsInBigInteger(v)"}) + @Specialization(guards = {"interop.fitsInBigInteger(v)", "!isMulti(v)"}) @TruffleBoundary long hashCodeForBigInteger( Object v, @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop) { @@ -451,6 +453,29 @@ long hashCodeForText( } } + @Specialization + long hashCodeForMultiValue( + EnsoMultiValue value, + @Cached TypeOfNode typesNode, + @Cached EnsoMultiValue.CastToNode castNode, + @Shared("hashCodeNode") @Cached HashCodeNode hashCodeNode) { + // multi value with single "has been cast to value" + // needs the same hash as the "has been cast to value" + // hence the sum has to start from 0L + var hash = 0L; + var types = typesNode.findAllTypesOrNull(value, false); + assert types != null; + for (var t : types) { + var v = castNode.findTypeOrNull(t, value, false, false); + assert v != null; + var vHash = hashCodeNode.execute(v); + // ordering of types in multivalue doesn't matter + // need commutative operation here + hash = hash + vHash; + } + return hash; + } + @TruffleBoundary @Specialization( guards = {"interop.isString(selfStr)"}, @@ -636,4 +661,8 @@ boolean isJavaObject(Object object) { boolean isJavaFunction(Object object) { return EnsoContext.get(this).isJavaPolyglotFunction(object); } + + static boolean isMulti(Object obj) { + return obj instanceof EnsoMultiValue; + } } diff --git a/test/Base_Tests/src/Semantic/Conversion_Spec.enso b/test/Base_Tests/src/Semantic/Conversion_Spec.enso index 4228168ccaa4..75116ff2f00e 100644 --- a/test/Base_Tests/src/Semantic/Conversion_Spec.enso +++ b/test/Base_Tests/src/Semantic/Conversion_Spec.enso @@ -320,10 +320,10 @@ add_specs suite_builder = x==x . should_be_true (x:Integer)==42 . should_be_true (x:Fool)==42 . should_be_false - x==42 . should_be_true + x==42 . should_be_false 42==(x.to Integer) . should_be_true 42==(x.to Fool) . should_be_false - 42==x . should_be_true + 42==x . should_be_false 100+(x:Integer) . should_equal 142 (x:Integer)+100 . should_equal 142 x+100 . should_equal 142 @@ -341,10 +341,10 @@ add_specs suite_builder = x==x . should_be_true (x:Float)==42.3 . should_be_true (x:Fool)==42.3 . should_be_false - x==42.3 . should_be_true + x==42.3 . should_be_false 42.3==(x.to Float) . should_be_true 42.3==(x.to Fool) . should_be_false - 42.3==x . should_be_true + 42.3==x . should_be_false 100+(x:Float) . should_equal 142.3 (x:Float)+100 . should_equal 142.3 x+100 . should_equal 142.3 @@ -378,10 +378,10 @@ add_specs suite_builder = x==x . should_be_true (x:Text)=="Hello" . should_be_true (x:Fool)=="Hello" . should_be_false - x=="Hello" . should_be_true + x=="Hello" . should_be_false "Hello"==(x:Text) . should_be_true "Hello"==(x:Fool) . should_be_false - "Hello"==x . should_be_true + "Hello"==x . should_be_false x.to_text . should_equal "Hello" (x:Fool).to_text . should_equal "{FOOL Hello}" (x:Text).to_text . should_equal "Hello" @@ -397,10 +397,10 @@ add_specs suite_builder = x==x . should_be_true (x:Time_Of_Day)==now . should_be_true (x:Fool)==now . should_be_false - x==now . should_be_true + x==now . should_be_false now==(x:Time_Of_Day) . should_be_true now==(x:Fool) . should_be_false - now==x . should_be_true + now==x . should_be_false x.to_text . should_equal now.to_text do_time now @@ -413,10 +413,10 @@ add_specs suite_builder = x==x . should_be_true (x:Date)==now . should_be_true (x:Fool)==now . should_be_false - x==now . should_be_true + x==now . should_be_false now==(x:Date) . should_be_true now==(x:Fool) . should_be_false - now==x . should_be_true + now==x . should_be_false x.to_text . should_equal "{FOOL "+now.to_text+"}" do_date now @@ -429,10 +429,10 @@ add_specs suite_builder = x==x . should_be_true (x:Date_Time)==now . should_be_true (x:Fool)==now . should_be_false - x==now . should_be_true + x==now . should_be_false now==(x:Date_Time) . should_be_true now==(x:Fool) . should_be_false - now==x . should_be_true + now==x . should_be_false x.to_text . should_equal now.to_text do_time now @@ -445,10 +445,10 @@ add_specs suite_builder = x==x . should_be_true (x:Duration)==now . should_be_true (x:Fool)==now . should_be_false - x==now . should_be_true + x==now . should_be_false now==(x:Duration) . should_be_true now==(x:Fool) . should_be_false - now==x . should_be_true + now==x . should_be_false x.to_text . should_equal "{FOOL "+now.to_text+"}" do_duration now diff --git a/test/Base_Tests/src/Semantic/Multi_Value_Convert_Spec.enso b/test/Base_Tests/src/Semantic/Multi_Value_Convert_Spec.enso index 81fcdd680410..ae94e0dd5d12 100644 --- a/test/Base_Tests/src/Semantic/Multi_Value_Convert_Spec.enso +++ b/test/Base_Tests/src/Semantic/Multi_Value_Convert_Spec.enso @@ -85,47 +85,66 @@ add_specs suite_builder = c1 . should_equal "c" suite_builder.group "Equals and hash" group_builder-> - group_builder.specify "Dictionary with value and multi value" <| - pi = 3.14 - a = pi : A - b = pi : B - c = pi : C - abc = pi : A&B&C - downcast_a = abc : A - downcast_ab = abc : A&B - downcast_ba = abc : B&A - downcast_b = abc : B - downcast_c = abc : C - + pi = 3.14 + a = pi : A + b = pi : B + c = pi : C + abc = pi : A&B&C + downcast_a = abc : A + downcast_ab = abc : A&B + downcast_ba = abc : B&A + downcast_b = abc : B + downcast_c = abc : C + + group_builder.specify "Ordering and multi value" <| Ordering.compare a b . catch Any e-> e.should_equal (Standard.Base.Errors.Common.Incomparable_Values.Error a b) Ordering.compare a downcast_a . should_equal Ordering.Equal - Ordering.compare a downcast_ab . should_equal Ordering.Equal - Ordering.compare a abc . should_equal Ordering.Equal - Ordering.compare a downcast_ba . should_equal Ordering.Equal - Ordering.compare downcast_ba b . should_equal Ordering.Equal - # if a == downcast_ba && downcast_ba == b then # due to transitivity - # Ordering.compare a b . should_equal Ordering.Equal + Ordering.hash a . should_equal (Ordering.hash downcast_a) + + Ordering.compare a downcast_ab . catch Any e-> + e.should_equal (Standard.Base.Errors.Common.Incomparable_Values.Error a downcast_ab) + Ordering.compare a abc . catch Any e-> + e.should_equal (Standard.Base.Errors.Common.Incomparable_Values.Error a abc) + + Ordering.compare a downcast_ba . catch Any e-> + e.should_equal (Standard.Base.Errors.Common.Incomparable_Values.Error a downcast_ba) + Ordering.compare downcast_ba b . catch Any e-> + e.should_equal (Standard.Base.Errors.Common.Incomparable_Values.Error downcast_ba b) + Ordering.compare a b . catch Any e-> + e.should_equal (Standard.Base.Errors.Common.Incomparable_Values.Error a b) + group_builder.specify "Dictionary with value and multi value" <| dict = Dictionary.empty . insert a "A" . insert b "B" . insert c "C" + . insert downcast_ab "AB_" + . insert downcast_ba "BA_" . insert downcast_a "A_" . insert downcast_b "B_" . insert downcast_c "C_" . insert abc "Multi" - # dict . get a . should_equal "A" # sometimes it is A and sometimes it is Multi - # dict . get b . should_equal "B" - # dict . get c . should_equal "C" # sometimes it is C and sometimes it is Multi - dict . get downcast_a . should_equal "Multi" - dict . get downcast_b . should_equal "Multi" - dict . get downcast_c . should_equal "Multi" + # downcast single value is equal to the value + dict . get downcast_a . should_equal "A_" + dict . get downcast_b . should_equal "B_" + dict . get downcast_c . should_equal "C_" + + # hence "A" ,"B", "C" were replaced + dict . get a . should_equal "A_" + dict . get b . should_equal "B_" + dict . get c . should_equal "C_" + + # multi value must be equal to all its values dict . get abc . should_equal "Multi" + # order of types in multi value isn't important + dict . get downcast_ab . should_equal "BA_" + dict . get downcast_ba . should_equal "BA_" + main filter=Nothing = suite = Test.build suite_builder-> add_specs suite_builder