diff --git a/distribution/lib/Standard/Test/0.0.0-dev/src/Extensions.enso b/distribution/lib/Standard/Test/0.0.0-dev/src/Extensions.enso index 1c7dd5f4076..b0a4700ac1c 100644 --- a/distribution/lib/Standard/Test/0.0.0-dev/src/Extensions.enso +++ b/distribution/lib/Standard/Test/0.0.0-dev/src/Extensions.enso @@ -296,7 +296,7 @@ Number.should_equal : Float -> Float -> Integer -> Spec_Result Number.should_equal self that epsilon=0 frames_to_skip=0 = matches = case that of _ : Number -> self.equals that epsilon - _ -> False + _ -> self==that case matches of True -> Spec_Result.Success False -> @@ -519,7 +519,7 @@ Error.should_contain_the_same_elements_as self that frames_to_skip=0 = It checks that all elements from `self` are also present in `that`. It does not require that all elements of `that` are contained in `self`. Arities of - elements are not checked, so `self` may still contain more elements than + elements are not checked, so `self` may still contain more elements than `that` by containing duplicates. It will work on any collection which supports the methods diff --git a/docs/syntax/functions.md b/docs/syntax/functions.md index a4e332bb53c..7554d395871 100644 --- a/docs/syntax/functions.md +++ b/docs/syntax/functions.md @@ -304,6 +304,44 @@ the addition by invoking `Num.+`. This behavior allows one to write libraries that extend existing number types with `Complex_Number`, `Rational_Number` and make them behave as first class citizen numbers. +### Custom Equality + +The `==` operator is special. A consistency with hash code is necessary to make +any Enso object behave correctly and work effectively in `Set` and `Map` +implementations. To guarantee such level of consistency there is a `Any.==` +definition providing _universal equality_ that **shall not be overriden**. + +The `==` behavior is predefined for builtin types, atoms and other Enso objects. +In addition to that it remains possible to define own _comparators_, including a +comparator capable to work with already existing types. To create such +comparator define: + +- conversion between existing type and the new type (as described in + [previous section](#type-ascriptions-and-operator-resolution)) +- comparator (see documentation of `Ordering` type) +- define **two conversion method** that return the same comparator + +To extend the previous definition of `Num` also for equality one might do for +example: + +```ruby +type Num_Comparator + compare a:Num b:Num = # compare somehow + hash a:Num = # hash somehow + +Num.from (that:Integer) = # convert somehow +Comparable.from (_:Num) = Num_Comparator +Comparable.from (_:Integer) = Num_Comparator +``` + +with such a structure the internal implementation of `Any.==` performs necessary +conversions of `Integer` argument in case the other argument is `Num` and +invokes the `Num_Comparator.compare` to handle the comparision. + +A care must be taken to keep consistency between `hash` values of original and +converted types - e.g. hash of `n:Integer` and hash of `Num.from n` must be the +same (otherwise consistency required for `Set` and `Map` would be compromised). + ### Precedence Operator precedence in Enso is a collection of rules that reflect conventions diff --git a/docs/types/dynamic-dispatch.md b/docs/types/dynamic-dispatch.md index f715b8d9a27..013ddfc42fa 100644 --- a/docs/types/dynamic-dispatch.md +++ b/docs/types/dynamic-dispatch.md @@ -63,10 +63,13 @@ another. ## Multiple Dispatch -It is an open question as to whether we want to support proper multiple dispatch -in Enso. Multiple dispatch refers to the dynamic dispatch target being -determined based not only on the type of the `this` argument, but the types of -the other arguments to the function. +Multiple dispatch is currently used for +[binary operators](../syntax/functions.md#type-ascriptions-and-operator-resolution). + +Supporting it for general functions remains an open question as to whether we +want to support proper multiple dispatch in Enso. Multiple dispatch refers to +the dynamic dispatch target being determined based not only on the type of the +`this` argument, but the types of the other arguments to the function. To do multiple dispatch properly, it is very important to get a rigorous specification of the specificity algorithm. It must account for: diff --git a/engine/runtime-benchmarks/src/main/java/org/enso/interpreter/bench/benchmarks/semantic/EqualsBenchmarks.java b/engine/runtime-benchmarks/src/main/java/org/enso/interpreter/bench/benchmarks/semantic/EqualsBenchmarks.java index 7b7c9d8e130..0b739c96731 100644 --- a/engine/runtime-benchmarks/src/main/java/org/enso/interpreter/bench/benchmarks/semantic/EqualsBenchmarks.java +++ b/engine/runtime-benchmarks/src/main/java/org/enso/interpreter/bench/benchmarks/semantic/EqualsBenchmarks.java @@ -75,6 +75,21 @@ public class EqualsBenchmarks { new StringBuilder( """ import Standard.Base.Data.Range.Extensions + import Standard.Base.Data.Numbers.Number + import Standard.Base.Data.Ordering.Ordering + import Standard.Base.Data.Ordering.Comparable + import Standard.Base.Data.Ordering.Default_Comparator + + type Num + Value n + + Num.from (that:Number) = Num.Value that + Comparable.from (_:Number) = Num_Comparator + Comparable.from (_:Num) = Num_Comparator + + type Num_Comparator + compare x:Num y:Num = Ordering.compare x.n y.n + hash x:Num = Default_Comparator.hash x.n type Node C1 f1 @@ -87,7 +102,9 @@ public class EqualsBenchmarks { eq_vec vec1 vec2 = (0.up_to vec1.length).map idx-> - (vec1.at idx) == (vec2.at idx) + v1 = vec1.at idx + v2 = vec2.at idx + v1 == v2 eq x y = x == y """); @@ -108,10 +125,37 @@ public class EqualsBenchmarks { primitiveVectorSize / 64); codeBuilder .append( - generateVectorOfPrimitives(primitiveVectorSize, "vec1", 42, trueExpectedAt, random)) + generateVectorOfPrimitives( + primitiveVectorSize, "vec1", 42, trueExpectedAt, random, "%d", "%f")) .append("\n") .append( - generateVectorOfPrimitives(primitiveVectorSize, "vec2", 42, trueExpectedAt, random)) + generateVectorOfPrimitives( + primitiveVectorSize, "vec2", 42, trueExpectedAt, random, "%d", "%f")) + .append("\n"); + } + case "equalsWithConversion" -> { + trueExpectedAt = + Set.of( + primitiveVectorSize / 2, + primitiveVectorSize / 4, + primitiveVectorSize / 8, + primitiveVectorSize / 16, + primitiveVectorSize / 32, + primitiveVectorSize / 64); + codeBuilder + .append( + generateVectorOfPrimitives( + primitiveVectorSize, "vec1", 42, trueExpectedAt, random, "%d", "%f")) + .append("\n") + .append( + generateVectorOfPrimitives( + primitiveVectorSize, + "vec2", + 42, + trueExpectedAt, + random, + "Num.Value %d", + "Num.Value %f")) .append("\n"); } case "equalsStrings" -> { @@ -141,7 +185,7 @@ public class EqualsBenchmarks { } codeBuilder.append(""" - bench x = eq_vec vec1 vec2 + bench _ = eq_vec vec1 vec2 """); module = ctx.eval(SrcUtil.source(benchmarkName, codeBuilder.toString())); @@ -171,6 +215,11 @@ public class EqualsBenchmarks { performBenchmark(blackHole); } + @Benchmark + public void equalsWithConversion(Blackhole blackHole) { + performBenchmark(blackHole); + } + @Benchmark public void equalsStrings(Blackhole blackhole) { performBenchmark(blackhole); @@ -207,7 +256,9 @@ public class EqualsBenchmarks { String vecName, Object identityElem, Collection constantIdxs, - Random random) { + Random random, + String intFormat, + String doubleFormat) { var partSize = totalSize / 2; List primitiveValues = new ArrayList<>(); random.ints(partSize).forEach(primitiveValues::add); @@ -221,9 +272,9 @@ public class EqualsBenchmarks { sb.append(vecName).append(" = ["); for (Object primitiveValue : primitiveValues) { if (primitiveValue instanceof Double dbl) { - sb.append(String.format("%f", dbl)).append(","); + sb.append(String.format(doubleFormat, dbl)).append(","); } else { - sb.append(primitiveValue).append(","); + sb.append(String.format(intFormat, primitiveValue)).append(","); } } // Replace last comma diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/EqualsConversionsTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/EqualsConversionsTest.java new file mode 100644 index 00000000000..c47144e646c --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/EqualsConversionsTest.java @@ -0,0 +1,174 @@ +package org.enso.interpreter.test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.List; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.PolyglotException; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class EqualsConversionsTest extends TestBase { + private static Context context; + + @BeforeClass + public static void initContextAndData() { + context = createDefaultContext(); + } + + @AfterClass + public static void disposeContext() { + context.close(); + } + + @Test + public void testBasicInequalities() { + var results = + TestBase.evalModule( + context, + """ + from Standard.Base import all + + Text.from (that:Number) = that.to_text + + main = + r0 = "4"+"2" == "42" + r1 = 42 == "42" + r2 = "42" == 42 + [ r0, r1, r2 ] + """) + .as(List.class); + + assertTrue("strings are equal: " + results, (boolean) results.get(0)); + assertFalse("string is not equal to number: " + results, (boolean) results.get(1)); + assertFalse("number is not equal to string: " + results, (boolean) results.get(2)); + } + + @Test + public void testNumWrapperAroundIntegerIsEqualToInteger() { + var gen = new DefineComparableWrapper(); + gen.intNumConversion = true; + gen.intComparator = true; + gen.numComparator = true; + assertTrue("Num.Value equal to Integer: ", gen.evaluate()); + } + + @Test + public void testMissingIntegerNumConversion() { + var gen = new DefineComparableWrapper(); + gen.intNumConversion = false; + gen.intComparator = true; + gen.numComparator = true; + assertFalse("Num.Value not equal to Integer: ", gen.evaluate()); + } + + @Test + public void testMissingIntComparator() { + var gen = new DefineComparableWrapper(); + gen.intNumConversion = true; + gen.intComparator = false; + gen.numComparator = true; + assertFalse("Num.Value not equal to Integer: ", gen.evaluate()); + } + + @Test + public void testMissingNumComparator() { + var gen = new DefineComparableWrapper(); + gen.intNumConversion = true; + gen.intComparator = true; + gen.numComparator = false; + assertFalse("Num.Value not equal to Integer: ", gen.evaluate()); + } + + @Test + public void testDifferentComparators() { + var gen = new DefineComparableWrapper(); + gen.intNumConversion = true; + gen.intComparator = true; + gen.numComparator = false; + gen.extraBlock = + """ + type Second_Comparator + compare a:Num b:Num = Num_Comparator.compare a b + hash a:Num = Num_Comparator.hash a + + Comparable.from (_:Num) = Second_Comparator + """; + assertFalse("Num.Value not equal to Integer: ", gen.evaluate()); + } + + @Test + public void testInconsistentHashFunction() { + var gen = new DefineComparableWrapper(); + gen.intNumConversion = true; + gen.intComparator = true; + gen.numComparator = true; + gen.hashFn = "x.n*31"; + + boolean r; + try { + r = gen.evaluate(); + } catch (PolyglotException ex) { + assertTrue(ex.getMessage(), ex.getMessage().contains("Different hash code!")); + return; + } + fail("Expecting assertion error, not: " + r); + } + + private static final class DefineComparableWrapper { + boolean intNumConversion; + boolean numComparator; + boolean intComparator; + String hashFn = "Default_Comparator.hash x.n"; + String extraBlock = ""; + + boolean evaluate() { + var block0 = + """ + from Standard.Base import all + + type Num + Value n:Integer + + type Num_Comparator + compare x:Num y:Num = Ordering.compare x.n y.n + hash x:Num = {hashFn} + """ + .replace("{hashFn}", hashFn); + + var block1 = + !intNumConversion + ? "" + : """ + Num.from (that:Integer) = Num.Value that + """; + + var block2 = + !numComparator + ? "" + : """ + Comparable.from (_:Num) = Num_Comparator + """; + + var block3 = + !intComparator ? "" : """ + Comparable.from (_:Integer) = Num_Comparator + """; + + var mainBlock = + """ + main = + num42 = Num.Value 42 + + r0 = 42 == num42 + r0 + """; + var res = + TestBase.evalModule(context, block0 + block1 + block2 + block3 + mainBlock + extraBlock); + return res.asBoolean(); + } + } +} diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/EqualsTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/EqualsTest.java index fa9c8803e0b..7ea7b87feb6 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/EqualsTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/EqualsTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.oracle.truffle.api.frame.VirtualFrame; import java.math.BigInteger; import java.time.LocalDate; import java.time.LocalTime; @@ -16,7 +17,6 @@ import java.util.List; import java.util.stream.Collectors; import org.enso.interpreter.node.expression.builtin.interop.syntax.HostValueToEnsoNode; import org.enso.interpreter.node.expression.builtin.meta.EqualsNode; -import org.enso.interpreter.node.expression.builtin.meta.EqualsNodeGen; import org.enso.interpreter.runtime.callable.UnresolvedConversion; import org.enso.interpreter.runtime.number.EnsoBigInteger; import org.enso.polyglot.MethodNames; @@ -44,8 +44,8 @@ public class EqualsTest extends TestBase { executeInContext( context, () -> { - testRootNode = new TestRootNode(); - equalsNode = EqualsNode.build(); + testRootNode = new TestRootNode(EqualsTest::equalityCheck); + equalsNode = EqualsNode.create(); hostValueToEnsoNode = HostValueToEnsoNode.build(); testRootNode.insertChildren(equalsNode, hostValueToEnsoNode); return null; @@ -88,13 +88,22 @@ public class EqualsTest extends TestBase { } } + private static boolean equalityCheck(VirtualFrame frame) { + var args = frame.getArguments(); + return equalsNode.execute(frame, args[0], args[1]); + } + + private boolean equalityCheck(Object first, Object second) { + return (Boolean) testRootNode.getCallTarget().call(first, second); + } + @Theory public void equalsOperatorShouldBeSymmetric(Object firstValue, Object secondValue) { executeInContext( context, () -> { - boolean firstResult = equalsNode.execute(firstValue, secondValue); - boolean secondResult = equalsNode.execute(secondValue, firstValue); + boolean firstResult = equalityCheck(firstValue, secondValue); + boolean secondResult = equalityCheck(secondValue, firstValue); assertEquals("equals should be symmetric", firstResult, secondResult); return null; }); @@ -105,8 +114,8 @@ public class EqualsTest extends TestBase { executeInContext( context, () -> { - Object firstResult = equalsNode.execute(value, value); - Object secondResult = equalsNode.execute(value, value); + Object firstResult = equalityCheck(value, value); + Object secondResult = equalityCheck(value, value); assertEquals("equals should be consistent", firstResult, secondResult); return null; }); @@ -117,8 +126,8 @@ public class EqualsTest extends TestBase { executeInContext( context, () -> { - Object uncachedRes = EqualsNodeGen.getUncached().execute(firstVal, secondVal); - Object cachedRes = equalsNode.execute(firstVal, secondVal); + Object uncachedRes = EqualsNode.getUncached().execute(null, firstVal, secondVal); + Object cachedRes = equalityCheck(firstVal, secondVal); assertEquals( "Result from uncached EqualsNode should be the same as result from its cached" + " variant", @@ -140,7 +149,7 @@ public class EqualsTest extends TestBase { executeInContext( context, () -> { - assertTrue(equalsNode.execute(ensoDate, javaDate)); + assertTrue(equalityCheck(ensoDate, javaDate)); return null; }); } @@ -158,7 +167,7 @@ public class EqualsTest extends TestBase { executeInContext( context, () -> { - assertTrue(equalsNode.execute(ensoTime, javaDate)); + assertTrue(equalityCheck(ensoTime, javaDate)); return null; }); } @@ -181,7 +190,7 @@ public class EqualsTest extends TestBase { executeInContext( context, () -> { - assertTrue(equalsNode.execute(ensoDateTime, javaDateTime)); + assertTrue(equalityCheck(ensoDateTime, javaDateTime)); return null; }); } @@ -194,7 +203,7 @@ public class EqualsTest extends TestBase { executeInContext( context, () -> { - assertTrue(javaNumber + " == " + ensoNumber, equalsNode.execute(javaNumber, ensoNumber)); + assertTrue(javaNumber + " == " + ensoNumber, equalityCheck(javaNumber, ensoNumber)); return null; }); } @@ -207,7 +216,7 @@ public class EqualsTest extends TestBase { executeInContext( context, () -> { - assertTrue(ensoNumber + " == " + javaNumber, equalsNode.execute(ensoNumber, javaNumber)); + assertTrue(ensoNumber + " == " + javaNumber, equalityCheck(ensoNumber, javaNumber)); return null; }); } @@ -220,7 +229,7 @@ public class EqualsTest extends TestBase { executeInContext( context, () -> { - assertTrue(javaNumber + " == " + hostNumber, equalsNode.execute(javaNumber, hostNumber)); + assertTrue(javaNumber + " == " + hostNumber, equalityCheck(javaNumber, hostNumber)); return null; }); } @@ -233,7 +242,7 @@ public class EqualsTest extends TestBase { executeInContext( context, () -> { - assertTrue(hostNumber + " == " + javaNumber, equalsNode.execute(hostNumber, javaNumber)); + assertTrue(hostNumber + " == " + javaNumber, equalityCheck(hostNumber, javaNumber)); return null; }); } @@ -246,7 +255,7 @@ public class EqualsTest extends TestBase { executeInContext( context, () -> { - assertTrue(equalsNode.execute(ensoVector, javaVector)); + assertTrue(equalityCheck(ensoVector, javaVector)); return null; }); } @@ -258,9 +267,9 @@ public class EqualsTest extends TestBase { executeInContext( context, () -> { - assertTrue(equalsNode.execute(ensoNumber, foreignNumber.asDirect())); - assertTrue(equalsNode.execute(ensoNumber, foreignNumber)); - assertTrue(equalsNode.execute(foreignNumber, ensoNumber)); + assertTrue(equalityCheck(ensoNumber, foreignNumber.asDirect())); + assertTrue(equalityCheck(ensoNumber, foreignNumber)); + assertTrue(equalityCheck(foreignNumber, ensoNumber)); return null; }); } @@ -272,9 +281,9 @@ public class EqualsTest extends TestBase { executeInContext( context, () -> { - assertTrue(equalsNode.execute(ensoNumber, foreignNumber.asDirect())); - assertTrue(equalsNode.execute(ensoNumber, foreignNumber)); - assertTrue(equalsNode.execute(foreignNumber, ensoNumber)); + assertTrue(equalityCheck(ensoNumber, foreignNumber.asDirect())); + assertTrue(equalityCheck(ensoNumber, foreignNumber)); + assertTrue(equalityCheck(foreignNumber, ensoNumber)); return null; }); } @@ -287,8 +296,8 @@ public class EqualsTest extends TestBase { executeInContext( context, () -> { - assertTrue(equalsNode.execute(ensoNumber, foreignNumber)); - assertTrue(equalsNode.execute(foreignNumber, ensoNumber)); + assertTrue(equalityCheck(ensoNumber, foreignNumber)); + assertTrue(equalityCheck(foreignNumber, ensoNumber)); return null; }); } @@ -301,9 +310,9 @@ public class EqualsTest extends TestBase { executeInContext( context, () -> { - assertTrue(equalsNode.execute(ensoBoolean, foreignBoolean.asDirect())); - assertTrue(equalsNode.execute(ensoBoolean, foreignBoolean)); - assertTrue(equalsNode.execute(foreignBoolean, ensoBoolean)); + assertTrue(equalityCheck(ensoBoolean, foreignBoolean.asDirect())); + assertTrue(equalityCheck(ensoBoolean, foreignBoolean)); + assertTrue(equalityCheck(foreignBoolean, ensoBoolean)); return null; }); } @@ -315,9 +324,9 @@ public class EqualsTest extends TestBase { executeInContext( context, () -> { - assertTrue(equalsNode.execute(ensoText, foreignString.asDirect())); - assertTrue(equalsNode.execute(ensoText, foreignString)); - assertTrue(equalsNode.execute(foreignString, ensoText)); + assertTrue(equalityCheck(ensoText, foreignString.asDirect())); + assertTrue(equalityCheck(ensoText, foreignString)); + assertTrue(equalityCheck(foreignString, ensoText)); return null; }); } @@ -336,8 +345,8 @@ public class EqualsTest extends TestBase { executeInContext( context, () -> { - assertTrue(equalsNode.execute(142L, hundred42)); - assertTrue(equalsNode.execute(hundred42, 142L)); + assertTrue(equalityCheck(142L, hundred42)); + assertTrue(equalityCheck(hundred42, 142L)); return null; }); } @@ -375,15 +384,12 @@ public class EqualsTest extends TestBase { executeInContext( context, () -> { - assertTrue( - "Conversions from same module are the same", equalsNode.execute(conv1, conv1_2)); - assertTrue( - "Conversions from same module are the same", equalsNode.execute(conv2, conv2_2)); + assertTrue("Conversions from same module are the same", equalityCheck(conv1, conv1_2)); + assertTrue("Conversions from same module are the same", equalityCheck(conv2, conv2_2)); assertFalse( - "Conversions from other modules aren't the same", equalsNode.execute(conv1, conv2)); + "Conversions from other modules aren't the same", equalityCheck(conv1, conv2)); assertFalse( - "Conversions from other modueles aren't the same", - equalsNode.execute(conv2_2, conv1_2)); + "Conversions from other modueles aren't the same", equalityCheck(conv2_2, conv1_2)); return null; }); diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/HashCodeTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/HashCodeTest.java index ade0d7fda03..cfe04819640 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/HashCodeTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/HashCodeTest.java @@ -38,7 +38,7 @@ public class HashCodeTest extends TestBase { context, () -> { hashCodeNode = HashCodeNode.build(); - equalsNode = EqualsNode.build(); + equalsNode = EqualsNode.create(); hostValueToEnsoNode = HostValueToEnsoNode.build(); testRootNode = new TestRootNode(); testRootNode.insertChildren(hashCodeNode, equalsNode, hostValueToEnsoNode); @@ -94,7 +94,7 @@ public class HashCodeTest extends TestBase { () -> { long firstHash = hashCodeNode.execute(firstValue); long secondHash = hashCodeNode.execute(secondValue); - Object valuesAreEqual = equalsNode.execute(firstValue, secondValue); + Object valuesAreEqual = equalsNode.execute(null, firstValue, secondValue); // if o1 == o2 then hash(o1) == hash(o2) if (isTrue(valuesAreEqual)) { assertEquals( diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/TestBase.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/TestBase.java index 34e2e475270..b80e06e218c 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/TestBase.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/TestBase.java @@ -13,6 +13,7 @@ import java.io.OutputStream; import java.nio.file.Paths; import java.util.Map; import java.util.concurrent.Callable; +import java.util.function.Function; import java.util.logging.Level; import org.enso.interpreter.EnsoLanguage; import org.enso.polyglot.MethodNames.Module; @@ -45,6 +46,7 @@ public abstract class TestBase { .allowIO(IOAccess.ALL) .allowAllAccess(true) .option(RuntimeOptions.LOG_LEVEL, Level.WARNING.getName()) + .option(RuntimeOptions.DISABLE_IR_CACHES, "true") .logHandler(System.err) .option(RuntimeOptions.STRICT_ERRORS, "true") .option( @@ -152,8 +154,15 @@ public abstract class TestBase { * #insertChildren(Node...)}. */ static class TestRootNode extends RootNode { + private final Function callback; + TestRootNode() { + this(null); + } + + TestRootNode(Function callback) { super(EnsoLanguage.get(null)); + this.callback = callback; } void insertChildren(Node... children) { @@ -165,7 +174,11 @@ public abstract class TestBase { /** In the tests, do not execute this root node, but execute directly the child nodes. */ @Override public Object execute(VirtualFrame frame) { - throw new AssertionError("should not reach here"); + if (callback == null) { + throw new AssertionError("should not reach here"); + } else { + return callback.apply(frame); + } } } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/EqualsAtomNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/EqualsAtomNode.java index c65921194ab..8914feaabb4 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/EqualsAtomNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/EqualsAtomNode.java @@ -7,6 +7,8 @@ import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Cached.Shared; import com.oracle.truffle.api.dsl.GenerateUncached; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.MaterializedFrame; +import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.library.CachedLibrary; import com.oracle.truffle.api.nodes.ExplodeLoop; import com.oracle.truffle.api.nodes.Node; @@ -32,7 +34,7 @@ public abstract class EqualsAtomNode extends Node { return EqualsAtomNodeGen.create(); } - public abstract boolean execute(Atom left, Atom right); + public abstract boolean execute(VirtualFrame frame, Atom left, Atom right); static EqualsNode[] createEqualsNodes(int size) { EqualsNode[] nodes = new EqualsNode[size]; @@ -48,6 +50,7 @@ public abstract class EqualsAtomNode extends Node { limit = "10") @ExplodeLoop boolean equalsAtomsWithDefaultComparator( + VirtualFrame frame, Atom self, Atom other, @Cached("self.getConstructor()") AtomConstructor selfCtorCached, @@ -65,7 +68,7 @@ public abstract class EqualsAtomNode extends Node { for (int i = 0; i < fieldsLenCached; i++) { var selfValue = structsLib.getField(self, i); var otherValue = structsLib.getField(other, i); - var fieldsAreEqual = fieldEqualsNodes[i].execute(selfValue, otherValue); + var fieldsAreEqual = fieldEqualsNodes[i].execute(frame, selfValue, otherValue); if (!fieldsAreEqual) { return false; } @@ -116,13 +119,18 @@ public abstract class EqualsAtomNode extends Node { } } - @CompilerDirectives.TruffleBoundary @Specialization( replaces = {"equalsAtomsWithDefaultComparator", "equalsAtomsWithCustomComparator"}) - boolean equalsAtomsUncached(Atom self, Atom other) { + boolean equalsAtomsUncached(VirtualFrame frame, Atom self, Atom other) { if (self.getConstructor() != other.getConstructor()) { return false; + } else { + return equalsAtomsUncached(frame == null ? null : frame.materialize(), self, other); } + } + + @CompilerDirectives.TruffleBoundary + private boolean equalsAtomsUncached(MaterializedFrame frame, Atom self, Atom other) { Type customComparator = CustomComparatorNode.getUncached().execute(self); if (customComparator != null) { Function compareFunc = findCompareMethod(customComparator); @@ -139,7 +147,7 @@ public abstract class EqualsAtomNode extends Node { for (int i = 0; i < self.getConstructor().getArity(); i++) { var selfField = StructsLibrary.getUncached().getField(self, i); var otherField = StructsLibrary.getUncached().getField(other, i); - boolean areFieldsSame = EqualsNodeGen.getUncached().execute(selfField, otherField); + boolean areFieldsSame = EqualsNode.getUncached().execute(frame, selfField, otherField); if (!areFieldsSame) { return false; } 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 ff580800227..bada651826a 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 @@ -6,6 +6,7 @@ import com.oracle.truffle.api.dsl.Cached.Exclusive; import com.oracle.truffle.api.dsl.Cached.Shared; import com.oracle.truffle.api.dsl.GenerateUncached; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.interop.ArityException; import com.oracle.truffle.api.interop.InteropLibrary; import com.oracle.truffle.api.interop.InvalidArrayIndexException; @@ -18,9 +19,7 @@ import com.oracle.truffle.api.library.CachedLibrary; import com.oracle.truffle.api.nodes.Node; import java.time.LocalDateTime; import java.time.ZonedDateTime; -import org.enso.interpreter.dsl.AcceptsError; import org.enso.interpreter.node.expression.builtin.interop.syntax.HostValueToEnsoNode; -import org.enso.interpreter.node.expression.builtin.ordering.CustomComparatorNode; import org.enso.interpreter.runtime.EnsoContext; import org.enso.interpreter.runtime.Module; import org.enso.interpreter.runtime.callable.UnresolvedConversion; @@ -39,24 +38,26 @@ public abstract class EqualsComplexNode extends Node { return EqualsComplexNodeGen.create(); } - public abstract boolean execute(@AcceptsError Object left, @AcceptsError Object right); + public abstract boolean execute(VirtualFrame frame, Object left, Object right); /** Enso specific types */ @Specialization boolean equalsUnresolvedSymbols( + VirtualFrame frame, UnresolvedSymbol self, UnresolvedSymbol otherSymbol, @Shared("equalsNode") @Cached EqualsNode equalsNode) { return self.getName().equals(otherSymbol.getName()) - && equalsNode.execute(self.getScope(), otherSymbol.getScope()); + && equalsNode.execute(frame, self.getScope(), otherSymbol.getScope()); } @Specialization boolean equalsUnresolvedConversion( + VirtualFrame frame, UnresolvedConversion selfConversion, UnresolvedConversion otherConversion, @Shared("equalsNode") @Cached EqualsNode equalsNode) { - return equalsNode.execute(selfConversion.getScope(), otherConversion.getScope()); + return equalsNode.execute(frame, selfConversion.getScope(), otherConversion.getScope()); } @Specialization @@ -71,8 +72,11 @@ public abstract class EqualsComplexNode extends Node { @Specialization boolean equalsFiles( - EnsoFile selfFile, EnsoFile otherFile, @Shared("equalsNode") @Cached EqualsNode equalsNode) { - return equalsNode.execute(selfFile.getPath(), otherFile.getPath()); + VirtualFrame frame, + EnsoFile selfFile, + EnsoFile otherFile, + @Shared("equalsNode") @Cached EqualsNode equalsNode) { + return equalsNode.execute(frame, selfFile.getPath(), otherFile.getPath()); } /** @@ -82,12 +86,13 @@ public abstract class EqualsComplexNode extends Node { */ @Specialization(guards = {"typesLib.hasType(selfType)", "typesLib.hasType(otherType)"}) boolean equalsTypes( + VirtualFrame frame, Type selfType, Type otherType, @Shared("equalsNode") @Cached EqualsNode equalsNode, @Shared("typesLib") @CachedLibrary(limit = "10") TypesLibrary typesLib) { return equalsNode.execute( - selfType.getQualifiedName().toString(), otherType.getQualifiedName().toString()); + frame, selfType.getQualifiedName().toString(), otherType.getQualifiedName().toString()); } /** @@ -99,6 +104,7 @@ public abstract class EqualsComplexNode extends Node { }, limit = "3") boolean equalsWithWarnings( + VirtualFrame frame, Object selfWithWarnings, Object otherWithWarnings, @CachedLibrary("selfWithWarnings") WarningsLibrary selfWarnLib, @@ -113,7 +119,7 @@ public abstract class EqualsComplexNode extends Node { otherWarnLib.hasWarnings(otherWithWarnings) ? otherWarnLib.removeWarnings(otherWithWarnings) : otherWithWarnings; - return equalsNode.execute(self, other); + return equalsNode.execute(frame, self, other); } catch (UnsupportedMessageException e) { throw EnsoContext.get(this).raiseAssertionPanic(this, null, e); } @@ -278,12 +284,12 @@ public abstract class EqualsComplexNode extends Node { }, limit = "3") boolean equalsArrays( + VirtualFrame frame, Object selfArray, Object otherArray, @CachedLibrary("selfArray") InteropLibrary selfInterop, @CachedLibrary("otherArray") InteropLibrary otherInterop, @Shared("equalsNode") @Cached EqualsNode equalsNode, - @Cached CustomComparatorNode hasCustomComparatorNode, @Shared("hostValueToEnsoNode") @Cached HostValueToEnsoNode valueToEnsoNode) { try { long selfSize = selfInterop.getArraySize(selfArray); @@ -293,7 +299,7 @@ public abstract class EqualsComplexNode extends Node { for (long i = 0; i < selfSize; i++) { Object selfElem = valueToEnsoNode.execute(selfInterop.readArrayElement(selfArray, i)); Object otherElem = valueToEnsoNode.execute(otherInterop.readArrayElement(otherArray, i)); - boolean elemsAreEqual = equalsNode.execute(selfElem, otherElem); + boolean elemsAreEqual = equalsNode.execute(frame, selfElem, otherElem); if (!elemsAreEqual) { return false; } @@ -313,6 +319,7 @@ public abstract class EqualsComplexNode extends Node { }, limit = "3") boolean equalsHashMaps( + VirtualFrame frame, Object selfHashMap, Object otherHashMap, @CachedLibrary("selfHashMap") InteropLibrary selfInterop, @@ -337,7 +344,7 @@ public abstract class EqualsComplexNode extends Node { && otherInterop.isHashEntryReadable(otherHashMap, key)) { Object otherValue = valueToEnsoNode.execute(otherInterop.readHashValue(otherHashMap, key)); - if (!equalsNode.execute(selfValue, otherValue)) { + if (!equalsNode.execute(frame, selfValue, otherValue)) { return false; } } else { @@ -391,13 +398,14 @@ public abstract class EqualsComplexNode extends Node { // It has well-defined equality based on the qualified name. @Specialization(guards = {"isJavaFunction(selfHostFunc)", "isJavaFunction(otherHostFunc)"}) boolean equalsHostFunctions( + VirtualFrame frame, Object selfHostFunc, Object otherHostFunc, @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop, @Shared("equalsNode") @Cached EqualsNode equalsNode) { Object selfFuncStrRepr = interop.toDisplayString(selfHostFunc); Object otherFuncStrRepr = interop.toDisplayString(otherHostFunc); - return equalsNode.execute(selfFuncStrRepr, otherFuncStrRepr); + return equalsNode.execute(frame, selfFuncStrRepr, otherFuncStrRepr); } @Specialization(guards = "fallbackGuard(left, right, interop, warningsLib)") @@ -419,7 +427,8 @@ public abstract class EqualsComplexNode extends Node { // all the other guards on purpose. boolean fallbackGuard( Object left, Object right, InteropLibrary interop, WarningsLibrary warnings) { - if (EqualsNode.isPrimitive(left, interop) && EqualsNode.isPrimitive(right, interop)) { + if (EqualsSimpleNode.isPrimitive(left, interop) + && EqualsSimpleNode.isPrimitive(right, interop)) { return false; } if (isJavaObject(left) && isJavaObject(right)) { diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/EqualsNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/EqualsNode.java index eec00c5f2c0..b368fb7fe80 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/EqualsNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/EqualsNode.java @@ -1,27 +1,29 @@ package org.enso.interpreter.node.expression.builtin.meta; -import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Cached.Shared; import com.oracle.truffle.api.dsl.GenerateUncached; +import com.oracle.truffle.api.dsl.NeverDefault; import com.oracle.truffle.api.dsl.Specialization; -import com.oracle.truffle.api.interop.InteropLibrary; -import com.oracle.truffle.api.interop.TruffleObject; -import com.oracle.truffle.api.interop.UnsupportedMessageException; -import com.oracle.truffle.api.library.CachedLibrary; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.interop.ArityException; import com.oracle.truffle.api.nodes.Node; -import java.math.BigInteger; import org.enso.interpreter.dsl.AcceptsError; import org.enso.interpreter.dsl.BuiltinMethod; -import org.enso.interpreter.node.expression.builtin.number.utils.BigIntegerOps; +import org.enso.interpreter.node.EnsoRootNode; +import org.enso.interpreter.node.callable.InteropConversionCallNode; +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.data.EnsoMultiValue; -import org.enso.interpreter.runtime.data.atom.Atom; -import org.enso.interpreter.runtime.data.atom.AtomConstructor; -import org.enso.interpreter.runtime.data.text.Text; -import org.enso.interpreter.runtime.error.WarningsLibrary; -import org.enso.interpreter.runtime.number.EnsoBigInteger; -import org.enso.polyglot.common_utils.Core_Text_Utils; +import org.enso.interpreter.runtime.callable.UnresolvedConversion; +import org.enso.interpreter.runtime.callable.argument.CallArgumentInfo; +import org.enso.interpreter.runtime.callable.function.Function; +import org.enso.interpreter.runtime.data.Type; +import org.enso.interpreter.runtime.error.PanicException; +import org.enso.interpreter.runtime.scope.ModuleScope; +import org.enso.interpreter.runtime.state.State; @BuiltinMethod( type = "Any", @@ -40,367 +42,252 @@ import org.enso.polyglot.common_utils.Core_Text_Utils; references point to the same object on the heap. Moreover, `Meta.is_same_object` implies `Any.==` for all object with the exception of `Number.nan`. """) -@GenerateUncached -public abstract class EqualsNode extends Node { +public final class EqualsNode extends Node { + @Child private EqualsSimpleNode node; + @Child private TypeOfNode types; + @Child private WithConversionNode convert; - public static EqualsNode build() { - return EqualsNodeGen.create(); - } + private static final EqualsNode UNCACHED = + new EqualsNode(EqualsSimpleNodeGen.getUncached(), TypeOfNode.getUncached(), true); - public abstract boolean execute(@AcceptsError Object self, @AcceptsError Object right); - - @Specialization - boolean equalsBoolBool(boolean self, boolean other) { - return self == other; - } - - @Specialization - boolean equalsBoolDouble(boolean self, double other) { - return false; - } - - @Specialization - boolean equalsBoolLong(boolean self, long other) { - return false; - } - - @Specialization - boolean equalsBoolBigInt(boolean self, EnsoBigInteger other) { - return false; - } - - @Specialization - boolean equalsBoolText(boolean self, Text other) { - return false; - } - - @Specialization - boolean equalsBoolInterop( - boolean self, - Object other, - @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop) { - try { - return self == interop.asBoolean(other); - } catch (UnsupportedMessageException ex) { - return false; + private EqualsNode(EqualsSimpleNode node, TypeOfNode types, boolean uncached) { + this.node = node; + this.types = types; + if (uncached) { + convert = EqualsNodeFactory.WithConversionNodeGen.getUncached(); } } - @Specialization - boolean equalsLongLong(long self, long other) { - return self == other; + @NeverDefault + static EqualsNode build() { + return create(); } - @Specialization - boolean equalsLongBool(long self, boolean other) { - return false; + @NeverDefault + public static EqualsNode create() { + return new EqualsNode(EqualsSimpleNode.build(), TypeOfNode.build(), false); } - @Specialization - boolean equalsLongDouble(long self, double other) { - return (double) self == other; + @NeverDefault + public static EqualsNode getUncached() { + return UNCACHED; } - @Specialization - boolean equalsLongText(long self, Text other) { - return false; - } - - @Specialization - @TruffleBoundary - boolean equalsLongBigInt(long self, EnsoBigInteger other) { - if (BigIntegerOps.fitsInLong(other.getValue())) { - return BigInteger.valueOf(self).compareTo(other.getValue()) == 0; - } else { - return false; - } - } - - @Specialization - boolean equalsLongInterop( - long self, - Object other, - @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop) { - try { - return self == interop.asLong(other); - } catch (UnsupportedMessageException ex) { - return false; - } - } - - @Specialization - boolean equalsDoubleDouble(double self, double other) { - if (Double.isNaN(self) || Double.isNaN(other)) { - return false; - } else { - return self == other; - } - } - - @Specialization - boolean equalsDoubleLong(double self, long other) { - return self == (double) other; - } - - @Specialization - boolean equalsDoubleBool(double self, boolean other) { - return false; - } - - @Specialization - boolean equalsDoubleText(double self, Text other) { - return false; - } - - @Specialization - boolean equalsDoubleInterop( - double self, - Object other, - @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop) { - try { - if (interop.fitsInDouble(other)) { - return self == interop.asDouble(other); + /** + * Compares two objects for equality. If the {@link EqualsSimpleNode simple check} fails, it tries + * to convert first argument to the second one and compare again. + * + * @param frame the stack frame we are executing at + * @param self the self object + * @param other the other object + * @return {@code true} if {@code self} and {@code that} seem equal + */ + public boolean execute( + VirtualFrame frame, @AcceptsError Object self, @AcceptsError Object other) { + var areEqual = node.execute(frame, self, other); + if (!areEqual) { + var selfType = types.execute(self); + var otherType = types.execute(other); + if (selfType != otherType) { + if (convert == null) { + CompilerDirectives.transferToInterpreter(); + convert = insert(WithConversionNode.create()); + } + return convert.executeWithConversion(frame, other, self); } - var otherBig = interop.asBigInteger(other); - return self == asDouble(otherBig); - } catch (UnsupportedMessageException ex) { - return false; } - } - - @TruffleBoundary - private static double asDouble(BigInteger big) { - return big.doubleValue(); - } - - @Specialization(guards = {"isBigInteger(iop, self)", "isBigInteger(iop, other)"}) - @TruffleBoundary - boolean other( - Object self, - Object other, - @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) { - return asBigInteger(iop, self).equals(asBigInteger(iop, other)); - } - - @Specialization(guards = "isBigInteger(iop, self)") - @TruffleBoundary - boolean equalsBigIntDouble( - Object self, - double other, - @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) { - return asBigInteger(iop, self).doubleValue() == other; - } - - @Specialization(guards = "isBigInteger(iop, self)") - @TruffleBoundary - boolean equalsBigIntLong( - Object self, long other, @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) { - var v = asBigInteger(iop, self); - if (BigIntegerOps.fitsInLong(v)) { - return v.compareTo(BigInteger.valueOf(other)) == 0; - } else { - return false; - } - } - - @Specialization - boolean equalsBigIntBool(EnsoBigInteger self, boolean other) { - return false; - } - - @Specialization - boolean equalsBigIntText(EnsoBigInteger self, Text other) { - return false; - } - - @TruffleBoundary - @Specialization(guards = {"isBigInteger(iop, self)", "!isPrimitiveValue(other)"}) - boolean equalsBigIntInterop( - Object self, - Object other, - @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) { - try { - var otherBigInteger = InteropLibrary.getUncached().asBigInteger(other); - return asBigInteger(iop, self).equals(otherBigInteger); - } catch (UnsupportedMessageException ex) { - return false; - } - } - - @Specialization(guards = {"selfText.is_normalized()", "otherText.is_normalized()"}) - boolean equalsTextText(Text selfText, Text otherText) { - return selfText.toString().compareTo(otherText.toString()) == 0; - } - - @Specialization - boolean equalsTextBool(Text self, boolean other) { - return false; - } - - @Specialization - boolean equalsTextLong(Text selfText, long otherLong) { - return false; - } - - @Specialization - boolean equalsTextDouble(Text selfText, double otherDouble) { - return false; - } - - @Specialization - boolean equalsTextBigInt(Text self, EnsoBigInteger other) { - return false; + return areEqual; } /** - * Compares interop string with other object. If the other object doesn't support conversion to - * String, it is not equal. Otherwise the two strings are compared according to the - * lexicographical order, handling Unicode normalization. See {@code Text_Utils.compare_to}. + * A node that checks the type of {@code that} argument, performs conversion of {@code self} to + * {@code that} type, and executes equality check again. */ - @TruffleBoundary - @Specialization( - guards = {"selfInterop.isString(selfString)"}, - limit = "3") - boolean equalsStrings( - Object selfString, - Object otherString, - @CachedLibrary("selfString") InteropLibrary selfInterop, - @CachedLibrary("otherString") InteropLibrary otherInterop) { - String selfJavaString; - String otherJavaString; - try { - selfJavaString = selfInterop.asString(selfString); - otherJavaString = otherInterop.asString(otherString); - } catch (UnsupportedMessageException e) { + @GenerateUncached + abstract static class WithConversionNode extends Node { + + @NeverDefault + static WithConversionNode create() { + return EqualsNodeFactory.WithConversionNodeGen.create(); + } + + /** + * @return {code false} if the conversion makes no sense or result of equality check after doing + * the conversion + */ + abstract boolean executeWithConversion(VirtualFrame frame, Object self, Object that); + + static Type findType(TypeOfNode typeOfNode, Object obj) { + var rawType = typeOfNode.execute(obj); + return rawType instanceof Type type ? type : null; + } + + static Type findTypeUncached(Object obj) { + return findType(TypeOfNode.getUncached(), obj); + } + + private static boolean isDefinedIn(ModuleScope scope, Function fn) { + if (fn.getCallTarget().getRootNode() instanceof EnsoRootNode ensoRoot) { + return ensoRoot.getModuleScope() == scope; + } else { + return false; + } + } + + @CompilerDirectives.TruffleBoundary + private static Object convertor(EnsoContext ctx, Function convFn, Object value) { + var argSchema = new CallArgumentInfo[] {new CallArgumentInfo(), new CallArgumentInfo()}; + var node = + InvokeFunctionNode.build( + argSchema, DefaultsExecutionMode.EXECUTE, ArgumentsExecutionMode.EXECUTE); + var state = State.create(ctx); + return node.execute( + convFn, null, state, new Object[] {ctx.getBuiltins().comparable(), value}); + } + + /** + * @return {@code null} if no conversion found + */ + Boolean findConversions(Type selfType, Type thatType, Object self, Object that) { + if (selfType == null || thatType == null) { + return null; + } + var ctx = EnsoContext.get(this); + + if (findConversionImpl(ctx, selfType, thatType, self, that)) { + return false; + } else { + if (findConversionImpl(ctx, thatType, selfType, that, self)) { + return true; + } else { + return null; + } + } + } + + private static boolean findConversionImpl( + EnsoContext ctx, Type selfType, Type thatType, Object self, Object that) { + var selfScope = selfType.getDefinitionScope(); + var comparableType = ctx.getBuiltins().comparable().getType(); + + var fromSelfType = + UnresolvedConversion.build(selfScope).resolveFor(ctx, comparableType, selfType); + var fromThatType = + UnresolvedConversion.build(selfScope).resolveFor(ctx, comparableType, thatType); + var betweenBoth = UnresolvedConversion.build(selfScope).resolveFor(ctx, selfType, thatType); + + if (isDefinedIn(selfScope, fromSelfType) + && isDefinedIn(selfScope, fromThatType) + && convertor(ctx, fromSelfType, self) == convertor(ctx, fromThatType, that) + && betweenBoth != null) { + return true; + } else { + return false; + } + } + + @Specialization( + limit = "10", + guards = { + "selfType != null", + "thatType != null", + "selfType == findType(typeOfNode, self)", + "thatType == findType(typeOfNode, that)" + }) + final boolean doConversionCached( + VirtualFrame frame, + Object self, + Object that, + @Shared("typeOf") @Cached TypeOfNode typeOfNode, + @Cached(value = "findType(typeOfNode, self)", uncached = "findTypeUncached(self)") + Type selfType, + @Cached(value = "findType(typeOfNode, that)", uncached = "findTypeUncached(that)") + Type thatType, + @Cached("findConversions(selfType, thatType, self, that)") Boolean convert, + @Shared("convert") @Cached InteropConversionCallNode convertNode, + @Shared("invoke") @Cached(allowUncached = true) EqualsSimpleNode equalityNode) { + if (convert == null) { + return false; + } + if (convert) { + return doDispatch(frame, that, self, thatType, convertNode, equalityNode); + } else { + return doDispatch(frame, self, that, selfType, convertNode, equalityNode); + } + } + + @Specialization(replaces = "doConversionCached") + final boolean doConversionUncached( + VirtualFrame frame, + Object self, + Object that, + @Shared("typeOf") @Cached TypeOfNode typeOfNode, + @Shared("convert") @Cached InteropConversionCallNode convertNode, + @Shared("invoke") @Cached(allowUncached = true) EqualsSimpleNode equalityNode) { + var selfType = findType(typeOfNode, self); + var thatType = findType(typeOfNode, that); + var conv = findConversions(selfType, thatType, self, that); + if (conv != null) { + var result = + conv + ? doDispatch(frame, that, self, thatType, convertNode, equalityNode) + : doDispatch(frame, self, that, selfType, convertNode, equalityNode); + return result; + } return false; } - return Core_Text_Utils.equals(selfJavaString, otherJavaString); - } - @Specialization(guards = "isPrimitive(self, interop) != isPrimitive(other, interop)") - boolean equalsDifferent( - Object self, - Object other, - @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop) { - return false; - } + private boolean doDispatch( + VirtualFrame frame, + Object self, + Object that, + Type selfType, + InteropConversionCallNode convertNode, + EqualsSimpleNode equalityNode) + throws PanicException { + var convert = UnresolvedConversion.build(selfType.getDefinitionScope()); - /** Equals for Atoms and AtomConstructors */ - @Specialization - boolean equalsAtomConstructors(AtomConstructor self, AtomConstructor other) { - return self == other; - } - - @Specialization - boolean equalsAtoms( - Atom self, - Atom other, - @Cached EqualsAtomNode equalsAtomNode, - @Shared("isSameObjectNode") @Cached IsSameObjectNode isSameObjectNode) { - return isSameObjectNode.execute(self, other) || equalsAtomNode.execute(self, other); - } - - @Specialization - boolean equalsReverseBoolean( - TruffleObject self, - boolean other, - @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop, - @Shared("reverse") @Cached EqualsNode reverse) { - return reverse.execute(other, self); - } - - @Specialization - boolean equalsReverseLong( - TruffleObject self, - long other, - @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop, - @Shared("reverse") @Cached EqualsNode reverse) { - return reverse.execute(other, self); - } - - @Specialization - boolean equalsReverseDouble( - TruffleObject self, - double other, - @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop, - @Shared("reverse") @Cached EqualsNode reverse) { - return reverse.execute(other, self); - } - - @Specialization - boolean equalsReverseBigInt( - TruffleObject self, - EnsoBigInteger other, - @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop, - @Shared("reverse") @Cached EqualsNode reverse) { - return reverse.execute(other, self); - } - - @Specialization(guards = "isNotPrimitive(self, other, interop, warnings)") - boolean equalsComplex( - Object self, - Object other, - @Cached EqualsComplexNode equalsComplex, - @Shared("isSameObjectNode") @Cached IsSameObjectNode isSameObjectNode, - @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop, - @CachedLibrary(limit = "5") WarningsLibrary warnings) { - return isSameObjectNode.execute(self, other) || equalsComplex.execute(self, other); - } - - static boolean isNotPrimitive( - Object a, Object b, InteropLibrary interop, WarningsLibrary warnings) { - if (a instanceof AtomConstructor && b instanceof AtomConstructor) { - return false; - } - if (a instanceof Atom && b instanceof Atom) { - return false; - } - if (warnings.hasWarnings(a) || warnings.hasWarnings(b)) { - return true; - } - if (a instanceof EnsoMultiValue || b instanceof EnsoMultiValue) { - return true; - } - return !isPrimitive(a, interop) && !isPrimitive(b, interop); - } - - /** - * Return true iff object is a primitive value used in some specializations guard. By primitive - * value we mean any value that can be present in Enso, so, for example, not Integer, as that - * cannot be present in Enso. All the primitive types should be handled in their corresponding - * specializations. See {@link - * org.enso.interpreter.node.expression.builtin.interop.syntax.HostValueToEnsoNode}. - */ - static boolean isPrimitive(Object object, InteropLibrary interop) { - return isPrimitiveValue(object) - || object instanceof EnsoBigInteger - || object instanceof Text - || interop.isString(object) - || interop.isNumber(object) - || interop.isBoolean(object); - } - - static boolean isPrimitiveValue(Object object) { - return object instanceof Boolean || object instanceof Long || object instanceof Double; - } - - static boolean isBigInteger(InteropLibrary iop, Object v) { - if (v instanceof EnsoBigInteger) { - return true; - } else { - return !iop.fitsInDouble(v) && !iop.fitsInLong(v) && iop.fitsInBigInteger(v); - } - } - - BigInteger asBigInteger(InteropLibrary iop, Object v) { - if (v instanceof EnsoBigInteger big) { - return big.getValue(); - } else { + var ctx = EnsoContext.get(this); + var state = State.create(ctx); try { - return iop.asBigInteger(v); - } catch (UnsupportedMessageException ex) { - throw EnsoContext.get(this).raiseAssertionPanic(this, "Expecting BigInteger", ex); + var thatAsSelf = convertNode.execute(convert, state, new Object[] {selfType, that}); + var result = equalityNode.execute(frame, self, thatAsSelf); + assert !result || assertHashCodeIsTheSame(that, thatAsSelf); + return result; + } catch (ArityException ex) { + var assertsOn = false; + assert assertsOn = true; + if (assertsOn) { + throw new AssertionError("Unexpected arity exception", ex); + } + return false; + } catch (PanicException ex) { + if (ctx.getBuiltins().error().isNoSuchConversionError(ex.getPayload())) { + return false; + } + throw ex; } } + + private boolean assertHashCodeIsTheSame(Object self, Object converted) { + var selfHash = HashCodeNode.getUncached().execute(self); + var convertedHash = HashCodeNode.getUncached().execute(converted); + var ok = selfHash == convertedHash; + if (!ok) { + var msg = + "Different hash code! Original " + + self + + "[#" + + Long.toHexString(selfHash) + + "] got converted to " + + converted + + "[#" + + Long.toHexString(convertedHash) + + "]"; + var ctx = EnsoContext.get(this); + throw ctx.raiseAssertionPanic(this, msg, new AssertionError(msg)); + } + return ok; + } } } 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 new file mode 100644 index 00000000000..78b2369f431 --- /dev/null +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/meta/EqualsSimpleNode.java @@ -0,0 +1,386 @@ +package org.enso.interpreter.node.expression.builtin.meta; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Cached.Shared; +import com.oracle.truffle.api.dsl.GenerateUncached; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.interop.UnsupportedMessageException; +import com.oracle.truffle.api.library.CachedLibrary; +import com.oracle.truffle.api.nodes.Node; +import java.math.BigInteger; +import org.enso.interpreter.node.expression.builtin.number.utils.BigIntegerOps; +import org.enso.interpreter.runtime.EnsoContext; +import org.enso.interpreter.runtime.data.EnsoMultiValue; +import org.enso.interpreter.runtime.data.atom.Atom; +import org.enso.interpreter.runtime.data.atom.AtomConstructor; +import org.enso.interpreter.runtime.data.text.Text; +import org.enso.interpreter.runtime.error.WarningsLibrary; +import org.enso.interpreter.runtime.number.EnsoBigInteger; +import org.enso.polyglot.common_utils.Core_Text_Utils; + +@GenerateUncached +public abstract class EqualsSimpleNode extends Node { + + public static EqualsSimpleNode build() { + return EqualsSimpleNodeGen.create(); + } + + public abstract boolean execute(VirtualFrame frame, Object self, Object right); + + @Specialization + boolean equalsBoolBool(boolean self, boolean other) { + return self == other; + } + + @Specialization + boolean equalsBoolDouble(boolean self, double other) { + return false; + } + + @Specialization + boolean equalsBoolLong(boolean self, long other) { + return false; + } + + @Specialization + boolean equalsBoolBigInt(boolean self, EnsoBigInteger other) { + return false; + } + + @Specialization + boolean equalsBoolText(boolean self, Text other) { + return false; + } + + @Specialization + boolean equalsBoolInterop( + boolean self, + Object other, + @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop) { + try { + return self == interop.asBoolean(other); + } catch (UnsupportedMessageException ex) { + return false; + } + } + + @Specialization + boolean equalsInteropBool( + TruffleObject self, + boolean other, + @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop) { + try { + return other == interop.asBoolean(self); + } catch (UnsupportedMessageException ex) { + return false; + } + } + + @Specialization + boolean equalsLongLong(long self, long other) { + return self == other; + } + + @Specialization + boolean equalsLongBool(long self, boolean other) { + return false; + } + + @Specialization + boolean equalsLongDouble(long self, double other) { + return (double) self == other; + } + + @Specialization + boolean equalsLongText(long self, Text other) { + return false; + } + + @Specialization + @TruffleBoundary + boolean equalsLongBigInt(long self, EnsoBigInteger other) { + if (BigIntegerOps.fitsInLong(other.getValue())) { + return BigInteger.valueOf(self).compareTo(other.getValue()) == 0; + } else { + return false; + } + } + + @Specialization + boolean equalsLongInterop( + long self, + Object other, + @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop) { + try { + return self == interop.asLong(other); + } catch (UnsupportedMessageException ex) { + return false; + } + } + + @Specialization + boolean equalsDoubleDouble(double self, double other) { + if (Double.isNaN(self) || Double.isNaN(other)) { + return false; + } else { + return self == other; + } + } + + @Specialization + boolean equalsDoubleLong(double self, long other) { + return self == (double) other; + } + + @Specialization + boolean equalsDoubleBool(double self, boolean other) { + return false; + } + + @Specialization + boolean equalsDoubleText(double self, Text other) { + return false; + } + + @Specialization + boolean equalsDoubleInterop( + double self, + Object other, + @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop) { + try { + if (interop.fitsInDouble(other)) { + return self == interop.asDouble(other); + } + var otherBig = interop.asBigInteger(other); + return self == asDouble(otherBig); + } catch (UnsupportedMessageException ex) { + return false; + } + } + + @TruffleBoundary + private static double asDouble(BigInteger big) { + return big.doubleValue(); + } + + @Specialization(guards = {"isBigInteger(iop, self)", "isBigInteger(iop, other)"}) + @TruffleBoundary + boolean other( + Object self, + Object other, + @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) { + return asBigInteger(iop, self).equals(asBigInteger(iop, other)); + } + + @Specialization(guards = "isBigInteger(iop, self)") + @TruffleBoundary + boolean equalsBigIntDouble( + Object self, + double other, + @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) { + return asBigInteger(iop, self).doubleValue() == other; + } + + @Specialization(guards = "isBigInteger(iop, self)") + @TruffleBoundary + boolean equalsBigIntLong( + Object self, long other, @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) { + var v = asBigInteger(iop, self); + if (BigIntegerOps.fitsInLong(v)) { + return v.compareTo(BigInteger.valueOf(other)) == 0; + } else { + return false; + } + } + + @Specialization + boolean equalsBigIntText(EnsoBigInteger self, Text other) { + return false; + } + + @TruffleBoundary + @Specialization(guards = {"isBigInteger(iop, self)", "!isPrimitiveValue(other)"}) + boolean equalsBigIntInterop( + Object self, + Object other, + @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) { + try { + var otherBigInteger = InteropLibrary.getUncached().asBigInteger(other); + return asBigInteger(iop, self).equals(otherBigInteger); + } catch (UnsupportedMessageException ex) { + return false; + } + } + + @Specialization(guards = {"selfText.is_normalized()", "otherText.is_normalized()"}) + boolean equalsTextText(Text selfText, Text otherText) { + return selfText.toString().compareTo(otherText.toString()) == 0; + } + + @Specialization + boolean equalsTextLong(Text selfText, long otherLong) { + return false; + } + + @Specialization + boolean equalsTextDouble(Text selfText, double otherDouble) { + return false; + } + + @Specialization + boolean equalsTextBigInt(Text self, EnsoBigInteger other) { + return false; + } + + /** + * Compares interop string with other object. If the other object doesn't support conversion to + * String, it is not equal. Otherwise the two strings are compared according to the + * lexicographical order, handling Unicode normalization. See {@code Text_Utils.compare_to}. + */ + @TruffleBoundary + @Specialization( + guards = {"selfInterop.isString(selfString)"}, + limit = "3") + boolean equalsStrings( + Object selfString, + Object otherString, + @CachedLibrary("selfString") InteropLibrary selfInterop, + @CachedLibrary("otherString") InteropLibrary otherInterop) { + String selfJavaString; + String otherJavaString; + try { + selfJavaString = selfInterop.asString(selfString); + otherJavaString = otherInterop.asString(otherString); + } catch (UnsupportedMessageException e) { + return false; + } + return Core_Text_Utils.equals(selfJavaString, otherJavaString); + } + + @Specialization(guards = "isPrimitive(self, interop) != isPrimitive(other, interop)") + boolean equalsDifferent( + Object self, + Object other, + @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop) { + return false; + } + + /** Equals for Atoms and AtomConstructors */ + @Specialization + boolean equalsAtomConstructors(AtomConstructor self, AtomConstructor other) { + return self == other; + } + + @Specialization + boolean equalsAtoms( + VirtualFrame frame, + Atom self, + Atom other, + @Cached EqualsAtomNode equalsAtomNode, + @Shared("isSameObjectNode") @Cached IsSameObjectNode isSameObjectNode) { + return isSameObjectNode.execute(self, other) || equalsAtomNode.execute(frame, self, other); + } + + @Specialization + boolean equalsReverseLong( + VirtualFrame frame, + TruffleObject self, + long other, + @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop, + @Shared("reverse") @Cached EqualsSimpleNode reverse) { + return reverse.execute(frame, other, self); + } + + @Specialization + boolean equalsReverseDouble( + VirtualFrame frame, + TruffleObject self, + double other, + @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop, + @Shared("reverse") @Cached EqualsSimpleNode reverse) { + return reverse.execute(frame, other, self); + } + + @Specialization + boolean equalsReverseBigInt( + VirtualFrame frame, + TruffleObject self, + EnsoBigInteger other, + @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop, + @Shared("reverse") @Cached EqualsSimpleNode reverse) { + return reverse.execute(frame, other, self); + } + + @Specialization(guards = "isNotPrimitive(self, other, interop, warnings)") + boolean equalsComplex( + VirtualFrame frame, + Object self, + Object other, + @Cached EqualsComplexNode equalsComplex, + @Shared("isSameObjectNode") @Cached IsSameObjectNode isSameObjectNode, + @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary interop, + @CachedLibrary(limit = "5") WarningsLibrary warnings) { + return isSameObjectNode.execute(self, other) || equalsComplex.execute(frame, self, other); + } + + static boolean isNotPrimitive( + Object a, Object b, InteropLibrary interop, WarningsLibrary warnings) { + if (a instanceof AtomConstructor && b instanceof AtomConstructor) { + return false; + } + if (a instanceof Atom && b instanceof Atom) { + return false; + } + if (warnings.hasWarnings(a) || warnings.hasWarnings(b)) { + return true; + } + if (a instanceof EnsoMultiValue || b instanceof EnsoMultiValue) { + return true; + } + return !isPrimitive(a, interop) && !isPrimitive(b, interop); + } + + /** + * Return true iff object is a primitive value used in some specializations guard. By primitive + * value we mean any value that can be present in Enso, so, for example, not Integer, as that + * cannot be present in Enso. All the primitive types should be handled in their corresponding + * specializations. See {@link + * org.enso.interpreter.node.expression.builtin.interop.syntax.HostValueToEnsoNode}. + */ + static boolean isPrimitive(Object object, InteropLibrary interop) { + return isPrimitiveValue(object) + || object instanceof EnsoBigInteger + || object instanceof Text + || interop.isString(object) + || interop.isNumber(object) + || interop.isBoolean(object); + } + + static boolean isPrimitiveValue(Object object) { + return object instanceof Boolean || object instanceof Long || object instanceof Double; + } + + static boolean isBigInteger(InteropLibrary iop, Object v) { + if (v instanceof EnsoBigInteger) { + return true; + } else { + return !iop.fitsInDouble(v) && !iop.fitsInLong(v) && iop.fitsInBigInteger(v); + } + } + + BigInteger asBigInteger(InteropLibrary iop, Object v) { + if (v instanceof EnsoBigInteger big) { + return big.getValue(); + } else { + try { + return iop.asBigInteger(v); + } catch (UnsupportedMessageException ex) { + throw EnsoContext.get(this).raiseAssertionPanic(this, "Expecting BigInteger", ex); + } + } + } +} 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 1b4dd2192aa..159a5a0ea40 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 @@ -83,6 +83,10 @@ public abstract class HashCodeNode extends Node { return HashCodeNodeGen.create(); } + static HashCodeNode getUncached() { + return HashCodeNodeGen.getUncached(); + } + public abstract long execute(@AcceptsError Object object); /** Specializations for primitive values * */ diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/ordering/SortArrayNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/ordering/SortArrayNode.java index 1ed27879060..f34cd00e8a3 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/ordering/SortArrayNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/ordering/SortArrayNode.java @@ -1,5 +1,6 @@ package org.enso.interpreter.node.expression.builtin.ordering; +import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.Node; import org.enso.interpreter.dsl.AcceptsError; import org.enso.interpreter.dsl.BuiltinMethod; @@ -11,6 +12,7 @@ public final class SortArrayNode extends Node { @Child private SortVectorNode sortVectorNode = SortVectorNode.build(); public Object execute( + VirtualFrame frame, State state, @AcceptsError Object self, long ascending, @@ -20,7 +22,15 @@ public final class SortArrayNode extends Node { Object onFunc, long problemBehavior) { return sortVectorNode.execute( - state, self, ascending, comparators, compareFunctions, byFunc, onFunc, problemBehavior); + frame, + state, + self, + ascending, + comparators, + compareFunctions, + byFunc, + onFunc, + problemBehavior); } public static SortArrayNode build() { diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/ordering/SortVectorNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/ordering/SortVectorNode.java index 017aa53ae54..854e99eca2a 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/ordering/SortVectorNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/expression/builtin/ordering/SortVectorNode.java @@ -6,6 +6,8 @@ import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Cached.Shared; import com.oracle.truffle.api.dsl.GenerateUncached; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.MaterializedFrame; +import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.interop.InteropLibrary; import com.oracle.truffle.api.interop.InvalidArrayIndexException; import com.oracle.truffle.api.interop.UnsupportedMessageException; @@ -77,6 +79,7 @@ public abstract class SortVectorNode extends Node { * @return A new, sorted vector. */ public abstract Object execute( + VirtualFrame frame, State state, @AcceptsError Object self, long ascending, @@ -101,6 +104,7 @@ public abstract class SortVectorNode extends Node { "interop.isNull(onFunc)" }) Object sortPrimitives( + VirtualFrame frame, State state, Object self, long ascending, @@ -131,7 +135,14 @@ public abstract class SortVectorNode extends Node { } var javaComparator = createDefaultComparator( - lessThanNode, equalsNode, typeOfNode, toTextNode, ascending, problemBehavior, interop); + frame.materialize(), + lessThanNode, + equalsNode, + typeOfNode, + toTextNode, + ascending, + problemBehavior, + interop); try { return sortPrimitiveVector(elems, javaComparator); } catch (CompareException e) { @@ -142,6 +153,7 @@ public abstract class SortVectorNode extends Node { @TruffleBoundary private DefaultSortComparator createDefaultComparator( + MaterializedFrame frame, LessThanNode lessThanNode, EqualsNode equalsNode, TypeOfNode typeOfNode, @@ -150,6 +162,7 @@ public abstract class SortVectorNode extends Node { long problemBehaviorNum, InteropLibrary interop) { return new DefaultSortComparator( + frame, lessThanNode, equalsNode, typeOfNode, @@ -165,6 +178,7 @@ public abstract class SortVectorNode extends Node { "interop.hasArrayElements(self)", }) Object sortGeneric( + MaterializedFrame frame, State state, Object self, long ascending, @@ -206,6 +220,7 @@ public abstract class SortVectorNode extends Node { if (interop.isNull(byFunc) && interop.isNull(onFunc) && isPrimitiveGroup(group)) { javaComparator = new DefaultSortComparator( + frame, lessThanNode, equalsNode, typeOfNode, @@ -557,12 +572,14 @@ public abstract class SortVectorNode extends Node { */ final class DefaultSortComparator extends SortComparator { + private final MaterializedFrame frame; private final LessThanNode lessThanNode; private final EqualsNode equalsNode; private final TypeOfNode typeOfNode; private final boolean ascending; private DefaultSortComparator( + MaterializedFrame frame, LessThanNode lessThanNode, EqualsNode equalsNode, TypeOfNode typeOfNode, @@ -571,6 +588,7 @@ public abstract class SortVectorNode extends Node { ProblemBehavior problemBehavior, InteropLibrary interop) { super(toTextNode, problemBehavior, interop); + this.frame = frame; this.lessThanNode = lessThanNode; this.equalsNode = equalsNode; this.typeOfNode = typeOfNode; @@ -583,7 +601,7 @@ public abstract class SortVectorNode extends Node { } int compareValuesWithDefaultComparator(Object x, Object y) { - if (equalsNode.execute(x, y)) { + if (equalsNode.execute(frame, x, y)) { return 0; } else { // Check if x < y diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoMultiValue.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoMultiValue.java index 1b3550f2aaa..1eb2c679251 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoMultiValue.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoMultiValue.java @@ -369,7 +369,7 @@ public final class EnsoMultiValue implements EnsoObject { try { var members = iop.getMembers(values[i]); var len = iop.getArraySize(members); - for (var j = 0L; j < len; i++) { + for (var j = 0L; j < len; j++) { var name = iop.readArrayElement(members, j); names.add(iop.asString(name)); } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/EnsoHashMap.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/EnsoHashMap.java index c960b5aae9c..e2ae5ac8ed1 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/EnsoHashMap.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/EnsoHashMap.java @@ -3,6 +3,7 @@ package org.enso.interpreter.runtime.data.hash; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Cached.Shared; +import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.interop.InteropLibrary; import com.oracle.truffle.api.interop.UnknownKeyException; import com.oracle.truffle.api.interop.UnsupportedMessageException; @@ -55,11 +56,11 @@ public final class EnsoHashMap implements EnsoObject { } EnsoHashMapBuilder getMapBuilder( - boolean readOnly, HashCodeNode hashCodeNode, EqualsNode equalsNode) { + VirtualFrame frame, boolean readOnly, HashCodeNode hashCodeNode, EqualsNode equalsNode) { if (readOnly) { return mapBuilder; } else { - return mapBuilder.asModifiable(generation, hashCodeNode, equalsNode); + return mapBuilder.asModifiable(frame, generation, hashCodeNode, equalsNode); } } @@ -104,7 +105,7 @@ public final class EnsoHashMap implements EnsoObject { Object key, @Shared("hash") @Cached HashCodeNode hashCodeNode, @Shared("equals") @Cached EqualsNode equalsNode) { - var entry = mapBuilder.get(key, generation, hashCodeNode, equalsNode); + var entry = mapBuilder.get(null, key, generation, hashCodeNode, equalsNode); return entry != null; } @@ -122,7 +123,7 @@ public final class EnsoHashMap implements EnsoObject { @Shared("hash") @Cached HashCodeNode hashCodeNode, @Shared("equals") @Cached EqualsNode equalsNode) throws UnknownKeyException { - StorageEntry entry = mapBuilder.get(key, generation, hashCodeNode, equalsNode); + StorageEntry entry = mapBuilder.get(null, key, generation, hashCodeNode, equalsNode); if (entry != null) { return entry.value(); } else { diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/EnsoHashMapBuilder.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/EnsoHashMapBuilder.java index c1208f2a5fd..722aeb06914 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/EnsoHashMapBuilder.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/EnsoHashMapBuilder.java @@ -1,6 +1,7 @@ package org.enso.interpreter.runtime.data.hash; import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.VirtualFrame; import java.util.Arrays; import org.enso.interpreter.node.expression.builtin.meta.EqualsNode; import org.enso.interpreter.node.expression.builtin.meta.HashCodeNode; @@ -87,10 +88,10 @@ final class EnsoHashMapBuilder { * Otherwise it may return new builder suitable for additions. */ public EnsoHashMapBuilder asModifiable( - int atGeneration, HashCodeNode hashCodeNode, EqualsNode equalsNode) { + VirtualFrame frame, int atGeneration, HashCodeNode hashCodeNode, EqualsNode equalsNode) { if (atGeneration != generation || generation * 4 > byHash.length * 3) { var newSize = Math.max(actualSize * 2, byHash.length); - return rehash(newSize, atGeneration, hashCodeNode, equalsNode); + return rehash(frame, newSize, atGeneration, hashCodeNode, equalsNode); } else { return this; } @@ -102,7 +103,12 @@ final class EnsoHashMapBuilder { * equal key, it marks it as removed, if it hasn't been removed yet. Once it finds an empty slot, * it puts there a new entry with the next generation. */ - public void put(Object key, Object value, HashCodeNode hashCodeNode, EqualsNode equalsNode) { + public void put( + VirtualFrame frame, + Object key, + Object value, + HashCodeNode hashCodeNode, + EqualsNode equalsNode) { var at = findWhereToStart(key, hashCodeNode); var nextGeneration = ++generation; var replacingExistingKey = false; @@ -114,7 +120,7 @@ final class EnsoHashMapBuilder { byHash[at] = new StorageEntry(key, value, nextGeneration); return; } - if (compare(equalsNode, byHash[at].key(), key)) { + if (compare(frame, equalsNode, byHash[at].key(), key)) { var invalidatedEntry = byHash[at].markRemoved(nextGeneration); if (invalidatedEntry != byHash[at]) { byHash[at] = invalidatedEntry; @@ -133,14 +139,18 @@ final class EnsoHashMapBuilder { * given {@code generation}. */ public StorageEntry get( - Object key, int generation, HashCodeNode hashCodeNode, EqualsNode equalsNode) { + VirtualFrame frame, + Object key, + int generation, + HashCodeNode hashCodeNode, + EqualsNode equalsNode) { var at = findWhereToStart(key, hashCodeNode); for (var i = 0; i < byHash.length; i++) { if (byHash[at] == null) { return null; } if (byHash[at].isVisible(generation)) { - if (compare(equalsNode, key, byHash[at].key())) { + if (compare(frame, equalsNode, key, byHash[at].key())) { return byHash[at]; } } @@ -164,14 +174,15 @@ final class EnsoHashMapBuilder { * * @return true if the removal was successful false otherwise. */ - public boolean remove(Object key, HashCodeNode hashCodeNode, EqualsNode equalsNode) { + public boolean remove( + VirtualFrame frame, Object key, HashCodeNode hashCodeNode, EqualsNode equalsNode) { var at = findWhereToStart(key, hashCodeNode); var nextGeneration = ++generation; for (var i = 0; i < byHash.length; i++) { if (byHash[at] == null) { return false; } - if (compare(equalsNode, key, byHash[at].key())) { + if (compare(frame, equalsNode, key, byHash[at].key())) { var invalidatedEntry = byHash[at].markRemoved(nextGeneration); if (invalidatedEntry != byHash[at]) { byHash[at] = invalidatedEntry; @@ -191,12 +202,16 @@ final class EnsoHashMapBuilder { * atGeneration}. */ private EnsoHashMapBuilder rehash( - int size, int atGeneration, HashCodeNode hashCodeNode, EqualsNode equalsNode) { + VirtualFrame frame, + int size, + int atGeneration, + HashCodeNode hashCodeNode, + EqualsNode equalsNode) { var newBuilder = new EnsoHashMapBuilder(size); for (var i = 0; i < byHash.length; i++) { var entry = byHash[i]; if (entry != null && entry.isVisible(atGeneration)) { - newBuilder.put(entry.key(), entry.value(), hashCodeNode, equalsNode); + newBuilder.put(frame, entry.key(), entry.value(), hashCodeNode, equalsNode); } } return newBuilder; @@ -224,11 +239,11 @@ final class EnsoHashMapBuilder { + "}"; } - private static boolean compare(EqualsNode equalsNode, Object a, Object b) { + private static boolean compare(VirtualFrame frame, EqualsNode equalsNode, Object a, Object b) { if (a instanceof Double aDbl && b instanceof Double bDbl && aDbl.isNaN() && bDbl.isNaN()) { return true; } else { - return equalsNode.execute(a, b); + return equalsNode.execute(frame, a, b); } } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/HashMapInsertNode.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/HashMapInsertNode.java index 8ffc5d9e240..6f64ed98b2c 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/HashMapInsertNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/HashMapInsertNode.java @@ -4,6 +4,7 @@ import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Cached.Shared; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.interop.InteropLibrary; import com.oracle.truffle.api.interop.InvalidArrayIndexException; import com.oracle.truffle.api.interop.StopIterationException; @@ -30,17 +31,18 @@ public abstract class HashMapInsertNode extends Node { return HashMapInsertNodeGen.create(); } - public abstract EnsoHashMap execute(Object self, Object key, Object value); + public abstract EnsoHashMap execute(VirtualFrame frame, Object self, Object key, Object value); @Specialization EnsoHashMap doEnsoHashMap( + VirtualFrame frame, EnsoHashMap hashMap, Object key, Object value, @Shared("hash") @Cached HashCodeNode hashCodeNode, @Shared("equals") @Cached EqualsNode equalsNode) { - var mapBuilder = hashMap.getMapBuilder(false, hashCodeNode, equalsNode); - mapBuilder.put(key, value, hashCodeNode, equalsNode); + var mapBuilder = hashMap.getMapBuilder(frame, false, hashCodeNode, equalsNode); + mapBuilder.put(frame, key, value, hashCodeNode, equalsNode); var newMap = mapBuilder.build(); return newMap; } @@ -51,6 +53,7 @@ public abstract class HashMapInsertNode extends Node { */ @Specialization(guards = "mapInterop.hasHashEntries(foreignMap)", limit = "3") EnsoHashMap doForeign( + VirtualFrame frame, Object foreignMap, Object keyToInsert, Object valueToInsert, @@ -65,8 +68,9 @@ public abstract class HashMapInsertNode extends Node { Object keyValueArr = iteratorInterop.getIteratorNextElement(entriesIterator); Object key = iteratorInterop.readArrayElement(keyValueArr, 0); Object value = iteratorInterop.readArrayElement(keyValueArr, 1); - mapBuilder = mapBuilder.asModifiable(mapBuilder.generation(), hashCodeNode, equalsNode); - mapBuilder.put(key, value, hashCodeNode, equalsNode); + mapBuilder = + mapBuilder.asModifiable(frame, mapBuilder.generation(), hashCodeNode, equalsNode); + mapBuilder.put(frame, key, value, hashCodeNode, equalsNode); } } catch (UnsupportedMessageException | StopIterationException | InvalidArrayIndexException e) { CompilerDirectives.transferToInterpreter(); @@ -76,8 +80,8 @@ public abstract class HashMapInsertNode extends Node { + " has wrongly specified Interop API (hash entries iterator)"; throw new PanicException(Text.create(msg), this); } - mapBuilder = mapBuilder.asModifiable(mapBuilder.generation(), hashCodeNode, equalsNode); - mapBuilder.put(keyToInsert, valueToInsert, hashCodeNode, equalsNode); + mapBuilder = mapBuilder.asModifiable(frame, mapBuilder.generation(), hashCodeNode, equalsNode); + mapBuilder.put(frame, keyToInsert, valueToInsert, hashCodeNode, equalsNode); return EnsoHashMap.createWithBuilder(mapBuilder); } } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/HashMapRemoveNode.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/HashMapRemoveNode.java index b19f6e915b2..30a3541197e 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/HashMapRemoveNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/hash/HashMapRemoveNode.java @@ -5,6 +5,7 @@ import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Cached.Shared; import com.oracle.truffle.api.dsl.GenerateUncached; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.interop.InteropLibrary; import com.oracle.truffle.api.interop.InvalidArrayIndexException; import com.oracle.truffle.api.interop.StopIterationException; @@ -29,16 +30,17 @@ public abstract class HashMapRemoveNode extends Node { return HashMapRemoveNodeGen.create(); } - public abstract EnsoHashMap execute(Object self, Object key); + public abstract EnsoHashMap execute(VirtualFrame frame, Object self, Object key); @Specialization EnsoHashMap removeFromEnsoMap( + VirtualFrame frame, EnsoHashMap ensoMap, Object key, @Shared("hash") @Cached HashCodeNode hashCodeNode, @Shared("equals") @Cached EqualsNode equalsNode) { - var mapBuilder = ensoMap.getMapBuilder(false, hashCodeNode, equalsNode); - if (mapBuilder.remove(key, hashCodeNode, equalsNode)) { + var mapBuilder = ensoMap.getMapBuilder(frame, false, hashCodeNode, equalsNode); + if (mapBuilder.remove(frame, key, hashCodeNode, equalsNode)) { return mapBuilder.build(); } else { throw DataflowError.withoutTrace("No such key", null); @@ -47,6 +49,7 @@ public abstract class HashMapRemoveNode extends Node { @Specialization(guards = "interop.hasHashEntries(map)") EnsoHashMap removeFromInteropMap( + VirtualFrame frame, Object map, Object keyToRemove, @CachedLibrary(limit = "5") InteropLibrary interop, @@ -62,7 +65,7 @@ public abstract class HashMapRemoveNode extends Node { while (interop.hasIteratorNextElement(entriesIterator)) { Object keyValueArr = interop.getIteratorNextElement(entriesIterator); Object key = interop.readArrayElement(keyValueArr, 0); - if ((boolean) equalsNode.execute(keyToRemove, key)) { + if (equalsNode.execute(frame, keyToRemove, key)) { if (keyToRemoveFound) { CompilerDirectives.transferToInterpreter(); var ctx = EnsoContext.get(this); @@ -72,8 +75,9 @@ public abstract class HashMapRemoveNode extends Node { } } else { Object value = interop.readArrayElement(keyValueArr, 1); - mapBuilder = mapBuilder.asModifiable(mapBuilder.generation(), hashCodeNode, equalsNode); - mapBuilder.put(key, value, hashCodeNode, equalsNode); + mapBuilder = + mapBuilder.asModifiable(frame, mapBuilder.generation(), hashCodeNode, equalsNode); + mapBuilder.put(frame, key, value, hashCodeNode, equalsNode); } } } catch (UnsupportedMessageException | StopIterationException | InvalidArrayIndexException e) { diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/Warning.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/Warning.java index 0d530d724fb..9945a6d877d 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/Warning.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/Warning.java @@ -231,7 +231,7 @@ public final class Warning implements EnsoObject { public Warning reassign(Node location) { RootNode root = location.getRootNode(); SourceSection section = location.getEncapsulatingSourceSection(); - Reassignment reassignment = new Reassignment(root.getName(), section); + Reassignment reassignment = new Reassignment(root == null ? "" : root.getName(), section); return new Warning(value, origin, sequenceId, reassignments.prepend(reassignment)); } diff --git a/test/Base_Tests/src/Data/Numbers_Spec.enso b/test/Base_Tests/src/Data/Numbers_Spec.enso index 0c38843bf73..66adce3d621 100644 --- a/test/Base_Tests/src/Data/Numbers_Spec.enso +++ b/test/Base_Tests/src/Data/Numbers_Spec.enso @@ -27,19 +27,17 @@ type Complex < self (that:Complex) = Complex_Comparator.compare self that == Ordering.Less > self (that:Complex) = Complex_Comparator.compare self that == Ordering.Greater - == self (that:Complex) = Complex_Comparator.compare self that == Ordering.Equal - - pending_equality -> Text = "== with conversions isn't yet supported" - Complex.from (that:Number) = Complex.new that type Complex_Comparator compare x:Complex y:Complex = if x.re==y.re && x.im==y.im then Ordering.Equal else if x.im==0 && y.im==0 then Ordering.compare x.re y.re else Nothing - hash x:Complex = 7*x.re + 11*x.im + hash x:Complex = if x.im == 0 then Default_Comparator.hash x.re else + 7*x.re + 11*x.im Comparable.from (_:Complex) = Complex_Comparator +Comparable.from (_:Number) = Complex_Comparator add_specs suite_builder = @@ -591,21 +589,25 @@ add_specs suite_builder = v2 = (Complex.new 6 6) Ordering.compare v1 v2 . catch Incomparable_Values (_->42) . should_equal 42 - group_builder.specify "Equality of complex and number" pending=Complex.pending_equality <| + group_builder.specify "Equality of complex and number" <| v1 = (Complex.new 3) v2 = 3 + r1 = v1==v2 + r2 = v2==v1 + r1 . should_be_true + r2 . should_be_true v1 . should_equal v2 v2 . should_equal v1 - v1==v2 . should_be_true - v2==v1 . should_be_true - group_builder.specify "Equality of number and complex" pending=Complex.pending_equality <| + group_builder.specify "Equality of number and complex" <| v1 = 3 v2 = (Complex.new 3) + r1 = v1==v2 + r2 = v2==v1 + r1 . should_be_true + r2 . should_be_true v1 . should_equal v2 v2 . should_equal v1 - v1==v2 . should_be_true - v2==v1 . should_be_true group_builder.specify "Greater or equal of complex and complex" <| v1 = (Complex.new 3) diff --git a/test/Base_Tests/src/Semantic/Conversion_Spec.enso b/test/Base_Tests/src/Semantic/Conversion_Spec.enso index ab436844ea7..4e33503de7e 100644 --- a/test/Base_Tests/src/Semantic/Conversion_Spec.enso +++ b/test/Base_Tests/src/Semantic/Conversion_Spec.enso @@ -235,6 +235,8 @@ add_specs suite_builder = # no conversions needed when calling `exchange` methods nine . should_equal one + suite_builder.group "MultiValue Conversions" group_builder-> + group_builder.specify "Requesting Text & Foo" <| check a (n : Text & Foo) = case a of 0 -> n.foo