Skip to content

Commit 0d9cb29

Browse files
JaroslavTulachFrizi
authored andcommitted
Symetric, transitive and reflexive equality for intersection types (#11897)
Fixes #11845 by comparing all the types an `EnsoMultiValue` _has been cast to_.
1 parent 4c0ccc0 commit 0d9cb29

File tree

14 files changed

+390
-74
lines changed

14 files changed

+390
-74
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
- A constructor or type definition with a single inline argument definition was
1818
previously allowed to use spaces in the argument definition without
1919
parentheses. [This is now a syntax error.][11856]
20+
- Symetric, transitive and reflexive [equality for intersection types][11897]
2021

2122
[11777]: https://github.com/enso-org/enso/pull/11777
2223
[11600]: https://github.com/enso-org/enso/pull/11600
2324
[11856]: https://github.com/enso-org/enso/pull/11856
25+
[11897]: https://github.com/enso-org/enso/pull/11897
2426

2527
# Next Release
2628

docs/types/intersection-types.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Just as demonstrated at
6868
https://github.com/enso-org/enso/commit/3d8a0e1b90b20cfdfe5da8d2d3950f644a4b45b8#diff-c6ef852899778b52ce6a11ebf9564d102c273021b212a4848b7678e120776287R23
6969
-->
7070

71-
## Narrowing Type Check
71+
### Narrowing Type Check
7272

7373
When an _intersection type_ value is being downcast to _some of the types it
7474
already represents_, these types become its _visible_ types. Any additional
@@ -160,9 +160,9 @@ Table.join self right:Table -> Table = ...
160160
161161
Such a `Table&Column` value can be returned from the above `Table.join` function
162162
and while having only `Table` behavior by default, still being able to be
163-
explicitly casted by the visual environment to `Column`.
163+
explicitly cast by the visual environment to `Column`.
164164

165-
## Converting Type Check
165+
### Converting Type Check
166166

167167
When an _intersection type_ is being checked against a type it doesn't
168168
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.
180180
In short: when a [conversion](../syntax/conversions.md) is needed to satisfy a
181181
type check a new value is created to satisfy just the types requested in the
182182
check.
183+
184+
## Equality & Hash Code
185+
186+
A value of an intersection type is equal with other value, if all values _it has
187+
been cast to_ are equal to the other value. E.g. a value of `Complex&Float` is
188+
equal to some other value only if its `Complex` part and `Float` part are equal
189+
to the other value. The _hidden_ types of a value (e.g. those that it _can be
190+
cast to_, if any) aren't considered in the equality check.
191+
192+
The order of types isn't important for equality. E.g. `Complex&Float` value can
193+
be equal to `Float&Complex` if the individual components (values _it has been
194+
cast to_) match. As implied by (custom)
195+
[equality rules](../syntax/functions.md#custom-equality) the `hash` of a value
196+
of _intersection type_ must thus be a sum of `hash` values of all the values it
197+
_has been cast to_. As a special case any value wrapped into an _intersection
198+
type_, but _cast down_ to the original type is `==` and has the same `hash` as
199+
the original value. E.g. `4.2 : Complex&Float : Float` is `==` and has the same
200+
`hash` as `4.2` (in spite it _can be cast to_ `Complex`).

engine/runtime-integration-tests/src/test/java/org/enso/interpreter/node/expression/builtin/meta/TypeOfNodeMultiValueTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ private static void registerValue(
8888
if (!polyValue.isNull()) {
8989
assertTrue("Type of " + polyValue + " is " + t, t.isMetaObject());
9090
var rawValue = ContextUtils.unwrapValue(ctx(), polyValue);
91+
if (rawValue instanceof EnsoMultiValue) {
92+
return;
93+
}
9194
var rawType = ContextUtils.unwrapValue(ctx(), t);
9295
if (rawType instanceof Type type) {
9396
var singleMultiValue = EnsoMultiValue.create(new Type[] {type}, 1, new Object[] {rawValue});
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package org.enso.interpreter.test;
2+
3+
import static org.junit.Assert.assertTrue;
4+
5+
import java.io.ByteArrayOutputStream;
6+
import java.nio.charset.StandardCharsets;
7+
import org.enso.interpreter.runtime.data.EnsoMultiValue;
8+
import org.enso.interpreter.runtime.data.Type;
9+
import org.enso.interpreter.runtime.data.text.Text;
10+
import org.enso.test.utils.ContextUtils;
11+
import org.graalvm.polyglot.Context;
12+
import org.graalvm.polyglot.Source;
13+
import org.junit.AfterClass;
14+
import org.junit.Before;
15+
import org.junit.BeforeClass;
16+
import org.junit.Ignore;
17+
import org.junit.Test;
18+
19+
public class AnyToTest {
20+
private static Context ctx;
21+
22+
private static final ByteArrayOutputStream out = new ByteArrayOutputStream();
23+
24+
@BeforeClass
25+
public static void initCtx() {
26+
ctx = ContextUtils.createDefaultContext(out);
27+
}
28+
29+
@AfterClass
30+
public static void disposeCtx() {
31+
ctx.close();
32+
ctx = null;
33+
}
34+
35+
@Before
36+
public void resetOutput() {
37+
out.reset();
38+
}
39+
40+
private String getStdOut() {
41+
return out.toString(StandardCharsets.UTF_8);
42+
}
43+
44+
@Test
45+
public void multiValueToInteger() throws Exception {
46+
var ensoCtx = ContextUtils.leakContext(ctx);
47+
var types =
48+
new Type[] {ensoCtx.getBuiltins().number().getInteger(), ensoCtx.getBuiltins().text()};
49+
var code =
50+
"""
51+
from Standard.Base import all
52+
53+
private eq a b = a == b
54+
55+
conv style v = case style of
56+
0 -> v.to Integer
57+
1 -> v:Integer
58+
99 -> eq
59+
60+
""";
61+
var conv =
62+
ContextUtils.evalModule(ctx, Source.newBuilder("enso", code, "conv.enso").build(), "conv");
63+
var both = EnsoMultiValue.create(types, types.length, new Object[] {2L, Text.create("Two")});
64+
var eq =
65+
ContextUtils.executeInContext(
66+
ctx,
67+
() -> {
68+
var bothValue = ctx.asValue(both);
69+
var asIntegerTo = conv.execute(0, bothValue);
70+
var asIntegerCast = conv.execute(1, bothValue);
71+
var equals = conv.execute(99, null);
72+
return equals.execute(asIntegerTo, asIntegerCast);
73+
});
74+
assertTrue("Any.to and : give the same result", eq.asBoolean());
75+
}
76+
77+
@Test
78+
@Ignore
79+
public void multiValueToText() throws Exception {
80+
multiValueToText(2);
81+
}
82+
83+
@Test
84+
@Ignore
85+
public void multiValueToTextHidden() throws Exception {
86+
multiValueToText(1);
87+
}
88+
89+
private void multiValueToText(int dispatchLength) throws Exception {
90+
var ensoCtx = ContextUtils.leakContext(ctx);
91+
var types =
92+
new Type[] {ensoCtx.getBuiltins().number().getInteger(), ensoCtx.getBuiltins().text()};
93+
var code =
94+
"""
95+
from Standard.Base import all
96+
97+
private eq a b = a == b
98+
99+
conv style:Integer v = case style of
100+
2 -> v.to Text
101+
3 -> v:Text
102+
99 -> eq
103+
104+
""";
105+
var conv =
106+
ContextUtils.evalModule(ctx, Source.newBuilder("enso", code, "conv.enso").build(), "conv");
107+
var both = EnsoMultiValue.create(types, dispatchLength, new Object[] {2L, Text.create("Two")});
108+
var eq =
109+
ContextUtils.executeInContext(
110+
ctx,
111+
() -> {
112+
var bothValue = ctx.asValue(both);
113+
var asIntegerCast = conv.execute(3, bothValue);
114+
var asIntegerTo = conv.execute(2, bothValue);
115+
var equals = conv.execute(99, null);
116+
return equals.execute(asIntegerTo, asIntegerCast);
117+
});
118+
assertTrue("Any.to and : give the same result", eq.asBoolean());
119+
}
120+
}

engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/EnsoMultiValueInteropTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,13 @@ private static void registerValue(
5757
var rawT2 = ContextUtils.unwrapValue(ctx(), t2);
5858
if (rawT1 instanceof Type typ1 && rawT2 instanceof Type typ2) {
5959
var r1 = ContextUtils.unwrapValue(ctx, v1);
60+
if (r1 instanceof EnsoMultiValue) {
61+
return;
62+
}
6063
var r2 = ContextUtils.unwrapValue(ctx, v2);
64+
if (r2 instanceof EnsoMultiValue) {
65+
return;
66+
}
6167
var both = EnsoMultiValue.create(new Type[] {typ1, typ2}, 2, new Object[] {r1, r2});
6268
data.add(new Object[] {both});
6369
}

engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/EqualsMultiValueTest.java

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,24 @@ public void testEqualityIntegerAndMultiValueWithBoth() {
111111
var intType = builtins.number().getInteger();
112112
var textText = builtins.text();
113113
var hi = Text.create("Hi");
114-
var fourExtraText =
114+
var textFour =
115115
EnsoMultiValue.create(new Type[] {textText, intType}, 2, new Object[] {hi, 4L});
116-
117-
assertTrue("4 == 4t", equalityCheck(4L, fourExtraText));
118-
assertFalse("5 != 4t", equalityCheck(5L, fourExtraText));
119-
assertTrue("4t == 4", equalityCheck(fourExtraText, 4L));
120-
assertFalse("4t != 5", equalityCheck(fourExtraText, 5L));
121-
assertTrue("4t == 'Hi'", equalityCheck(fourExtraText, hi));
122-
assertTrue("'Hi' == 4t", equalityCheck(hi, fourExtraText));
116+
var textFive =
117+
EnsoMultiValue.create(new Type[] {textText, intType}, 2, new Object[] {hi, 5L});
118+
var fourText =
119+
EnsoMultiValue.create(new Type[] {intType, textText}, 2, new Object[] {4L, hi});
120+
121+
assertFalse("4 != t", equalityCheck(4L, hi));
122+
assertFalse("4 != 4t", equalityCheck(4L, textFour));
123+
assertFalse("5 != 4t", equalityCheck(5L, textFour));
124+
assertFalse("5t != 4t", equalityCheck(textFive, textFour));
125+
assertFalse("4t != 4", equalityCheck(textFour, 4L));
126+
assertFalse("4t != 5", equalityCheck(textFour, 5L));
127+
assertFalse("4t != 'Hi'", equalityCheck(textFour, hi));
128+
assertFalse("'Hi' != 4t", equalityCheck(hi, textFour));
129+
130+
assertTrue("t4 == 4t", equalityCheck(textFour, fourText));
131+
assertTrue("4t == t4", equalityCheck(fourText, textFour));
123132

124133
return null;
125134
});
@@ -137,9 +146,9 @@ public void testEqualityIntegerAndMultiValueWithIntText() {
137146
EnsoMultiValue.create(
138147
new Type[] {intType, textText}, 2, new Object[] {4L, Text.create("Hi")});
139148

140-
assertTrue("4 == 4t", equalityCheck(4L, fourExtraText));
149+
assertFalse("4 != 4t", equalityCheck(4L, fourExtraText));
141150
assertFalse("5 != 4t", equalityCheck(5L, fourExtraText));
142-
assertTrue("4t == 4", equalityCheck(fourExtraText, 4L));
151+
assertFalse("4t != 4", equalityCheck(fourExtraText, 4L));
143152
assertFalse("4t != 5", equalityCheck(fourExtraText, 5L));
144153

145154
return null;

engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/ValuesGenerator.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
import java.util.TimeZone;
2626
import org.enso.common.MethodNames;
2727
import org.enso.common.MethodNames.Module;
28+
import org.enso.interpreter.node.expression.foreign.HostValueToEnsoNode;
29+
import org.enso.interpreter.runtime.data.EnsoMultiValue;
30+
import org.enso.interpreter.runtime.data.EnsoObject;
31+
import org.enso.interpreter.runtime.data.Type;
32+
import org.enso.test.utils.ContextUtils;
2833
import org.graalvm.polyglot.Context;
2934
import org.graalvm.polyglot.PolyglotException;
3035
import org.graalvm.polyglot.Value;
@@ -865,6 +870,41 @@ public List<Value> problemBehaviors() {
865870
return collect;
866871
}
867872

873+
public List<Value> numbersMultiText() {
874+
var leak = ContextUtils.leakContext(ctx);
875+
var numberTextTypes =
876+
new Type[] {
877+
leak.getBuiltins().number().getInteger(), leak.getBuiltins().text(),
878+
};
879+
var textNumberTypes =
880+
new Type[] {
881+
leak.getBuiltins().text(), leak.getBuiltins().number().getInteger(),
882+
};
883+
var collect = new ArrayList<Value>();
884+
var toEnso = HostValueToEnsoNode.getUncached();
885+
for (var n : numbers()) {
886+
for (var t : textual()) {
887+
var rawN = toEnso.execute(ContextUtils.unwrapValue(ctx, n));
888+
var rawT = ContextUtils.unwrapValue(ctx, t);
889+
if (!(rawT instanceof EnsoObject)) {
890+
continue;
891+
}
892+
addMultiToCollect(collect, numberTextTypes, 2, rawN, rawT);
893+
addMultiToCollect(collect, numberTextTypes, 1, rawN, rawT);
894+
addMultiToCollect(collect, textNumberTypes, 2, rawT, rawN);
895+
addMultiToCollect(collect, textNumberTypes, 1, rawT, rawN);
896+
}
897+
}
898+
return collect;
899+
}
900+
901+
private void addMultiToCollect(
902+
List<Value> collect, Type[] types, int dispatchTypes, Object... values) {
903+
var raw = EnsoMultiValue.create(types, dispatchTypes, values);
904+
var wrap = ctx.asValue(raw);
905+
collect.add(wrap);
906+
}
907+
868908
public List<Value> noWrap() {
869909
var collect = new ArrayList<Value>();
870910
if (languages.contains(Language.ENSO)) {

engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/hash/HashCodeTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ private static Object[] fetchAllUnwrappedValues() {
7373
values.addAll(valGenerator.numbers());
7474
values.addAll(valGenerator.booleans());
7575
values.addAll(valGenerator.textual());
76+
values.addAll(valGenerator.numbersMultiText());
7677
values.addAll(valGenerator.arrayLike());
7778
values.addAll(valGenerator.vectors());
7879
values.addAll(valGenerator.maps());

0 commit comments

Comments
 (0)