Convert Any.== to a builtin (#3956)

`Any.==` is a builtin method. The semantics is the same as it used to be, except that we no longer assume `x == y` iff `Meta.is_same_object x y`, which used to be the case and caused failures in table tests.

# Important Notes
Measurements from `EqualsBenchmarks` shows that the performance of `Any.==` for recursive atoms increased by roughly 20%, and the performance for primitive types stays roughly the same.
This commit is contained in:
Pavel Marek 2022-12-29 22:20:00 +01:00 committed by GitHub
parent 74742d3267
commit e6838bc90d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1214 additions and 365 deletions

View File

@ -492,6 +492,7 @@
- [Vector returns warnings of individual elements][3938]
- [Enso.getMetaObject, Type.isMetaInstance and Meta.is_a consolidation][3949]
- [Add executionContext/interrupt API command][3952]
- [Any.== is a builtin method][3956]
- [Simplify exception handling for polyglot exceptions][3981]
[3227]: https://github.com/enso-org/enso/pull/3227
@ -568,6 +569,7 @@
[3938]: https://github.com/enso-org/enso/pull/3938
[3949]: https://github.com/enso-org/enso/pull/3949
[3952]: https://github.com/enso-org/enso/pull/3952
[3956]: https://github.com/enso-org/enso/pull/3956
[3981]: https://github.com/enso-org/enso/pull/3981
# Enso 2.0.0-alpha.18 (2021-10-12)

View File

@ -74,11 +74,24 @@ type Any
implementing equality for your own types, keep in mind that it needs to
work with any Enso value as the `that` argument.
! Unicode Equality
The definition of equality includes Unicode canonicalization. I.e. two
texts are equal if they are identical after canonical decomposition. This
ensures that different ways of expressing the same character in the
underlying binary representation are considered equal.
? Generic Equality and Performance
While the generic equality provided here will work for _all_ values in
Enso, its performance may often be suboptimal. Many types can implement
their own equality operations that will be more efficient than these.
> Example
The string 'é' (i.e. the character U+00E9, LATIN SMALL LETTER E WITH ACUTE)
is canonically the same as the string 'e\u0301' (i.e. the letter `e`
followed by U+0301, COMBINING ACUTE ACCENT). Therefore:
('é' == 'e\u0301') == True
> Example
Checking if the variable `a` is equal to `147`.
@ -88,27 +101,7 @@ type Any
a = 7 * 21
a == 147
== : Any -> Boolean
== self that = if Meta.is_same_object self that then True else
self_meta = Meta.meta self
that_meta = Meta.meta that
case Pair.new self_meta that_meta of
Pair.Value (Meta.Atom.Value _) (Meta.Atom.Value _) ->
c_1 = self_meta.constructor.value ...
c_2 = that_meta.constructor.value ...
if Meta.is_same_object c_1 c_2 . not then False else
f_1 = self_meta.fields
f_2 = that_meta.fields
0.up_to f_1.length . all i-> (f_1.at i) == (f_2.at i)
Pair.Value (Meta.Error.Value _) (Meta.Error.Value _) -> self_meta.payload == that_meta.payload
Pair.Value (Meta.Polyglot.Value o_1) (Meta.Polyglot.Value o_2) ->
langs_match = (self_meta.get_language == Meta.Language.Java) && (that_meta.get_language == Meta.Language.Java)
if langs_match.not then False else o_1.equals o_2
Pair.Value (Meta.Unresolved_Symbol.Value _) (Meta.Unresolved_Symbol.Value _) ->
(self_meta.name == that_meta.name) && (self_meta.scope == that_meta.scope)
## Constructor comparison is covered by the identity equality.
Primitive objects should define their own equality.
Therefore, there are no more cases to handle in self method.
_ -> False
== self that = @Builtin_Method "Any.=="
## ALIAS Inequality

View File

@ -154,18 +154,3 @@ type Array
primitive array protocol.
to_array : Array
to_array self = @Builtin_Method "Array.to_array"
## Checks whether this array is equal to `that`.
Arguments:
- that: The array to compare this array against.
Two arrays are considered equal, when they have the same length and
their items are pairwise equal.
== : Array -> Boolean
== self that =
if Meta.is_same_object Array self then Meta.is_same_object Array that else
eq_at i = self.at i == that.at i
Panic.catch Any handler=(_ -> False)
if self.length == that.length then 0.up_to self.length . all eq_at else False

View File

@ -14,18 +14,6 @@ type Boolean
True
False
## Compares two booleans for equality.
Arguments:
- that: The boolean to compare this with.
> Example
Comparing True to False to get False.
True == False
== : Boolean -> Boolean
== self that = @Builtin_Method "Boolean.=="
## Computes the logical and (conjunction) of two booleans.
Arguments:

View File

@ -420,9 +420,3 @@ type Locale
example_to_text = Locale.default.to_text
to_text : Text | Nothing
to_text self = self.java_locale.toLanguageTag
## Compares two locales for equality.
== : Any -> Boolean
== self other = case other of
Locale.Value other_java_locale -> self.java_locale.equals other_java_locale
_ -> False

View File

@ -458,18 +458,6 @@ type Decimal
^ : Number -> Number
^ self that = @Builtin_Method "Decimal.^"
## Compares this and that for equality.
Arguments:
- that: The number to compare this against.
> Example
Comparing 7 and 2.1 for equality.
7 == 2.1
== : Number -> Boolean
== self that = @Builtin_Method "Decimal.=="
## Checks if this is greater than that.
Arguments:
@ -708,18 +696,6 @@ type Integer
^ : Number -> Number
^ self that = @Builtin_Method "Integer.^"
## Compares this and that for equality.
Arguments:
- that: The number to compare this against.
> Example
Comparing 7 and 2 for equality.
7 == 2
== : Number -> Boolean
== self that = @Builtin_Method "Integer.=="
## Checks if this is greater than that.
Arguments:

View File

@ -33,29 +33,6 @@ type Text
+ : Text -> Text
+ self that = @Builtin_Method "Text.+"
## Checks whether `self` is equal to `that`.
Arguments:
- that: The text to compare `self` for equality with.
! Unicode Equality
The definition of equality includes Unicode canonicalization. I.e. two
texts are equal if they are identical after canonical decomposition. This
ensures that different ways of expressing the same character in the
underlying binary representation are considered equal.
> Example
The string 'é' (i.e. the character U+00E9, LATIN SMALL LETTER E WITH ACUTE)
is canonically the same as the string 'e\u0301' (i.e. the letter `e`
followed by U+0301, COMBINING ACUTE ACCENT). Therefore:
('é' == 'e\u0301') == True
== : Any -> Boolean
== self that = if Meta.is_same_object self Text then Meta.is_same_object that Text else
case that of
_ : Text -> Text_Utils.equals self that
_ -> False
## Compare two texts to discover their ordering.
Arguments:

View File

@ -605,14 +605,6 @@ type Date
Ordering.from_sign sign
_ -> Error.throw (Type_Error.Error Date that "that")
## Compares two Dates for equality.
== : Date -> Boolean
== self that = case that of
Date -> Meta.is_same_object self Date
_ : Date ->
sign = Time_Utils.compare_to_localdate self that
0 == sign
_ -> False
## PRIVATE
week_days_between start end =

View File

@ -701,12 +701,3 @@ type Date_Time
sign = Time_Utils.compare_to_zoneddatetime self that
Ordering.from_sign sign
_ -> Error.throw (Type_Error.Error Date_Time that "that")
## Compares two Date_Time for equality.
== : Date_Time -> Boolean
== self that = case that of
Date_Time -> Meta.is_same_object self Date_Time
_ : Date_Time ->
sign = Time_Utils.compare_to_zoneddatetime self that
0 == sign
_ -> False

View File

@ -166,23 +166,6 @@ type Duration
Panic.catch ArithmeticException (self.minus_builtin that) err->
Error.throw (Time_Error.Error err.payload.getMessage)
## Check two durations for equality.
Arguments:
- that: The duration to compare against `self`.
> Examples
Check if 60 seconds and 1 minute are equal.
import Standard.Base.Data.Time.Duration
example_eq = (Duration.new seconds=60).total_minutes == (Duration.new minutes=1).total_minutes
== : Duration -> Boolean
== self that =
case that of
_ : Duration -> self.equals_builtin that
_ -> False
## Compares `self` to `that` to produce an ordering.
Arguments:

View File

@ -129,20 +129,6 @@ type Period
DateTimeException -> Error.throw Time_Error.Error "Period subtraction failed"
ArithmeticException -> Error.throw Illegal_Argument.Error "Arithmetic error"
## Check two periods for equality.
Note that two periods are equal if they have the exact same amount of
years, months, and days. So `(Period.new days=30)` and
`(Period.new months=1)` are not equal. Even `(Period.new years=1)` and
`(Period.new months=12)` are not equal.
Arguments:
- other_period: The period to compare against `self`.
== : Period -> Boolean
== self that =
ensure_period that <|
self.internal_period.equals that.internal_period
## Just throws `Incomparable_Values`, because periods cannot be
compared without additional context.

View File

@ -377,12 +377,3 @@ type Time_Of_Day
sign = Time_Utils.compare_to_localtime self that
Ordering.from_sign sign
_ -> Error.throw (Type_Error.Error Time_Of_Day that "that")
## Compares two Time_Of_Day for equality.
== : Date -> Boolean
== self that = case that of
Time_Of_Day -> Meta.is_same_object self Time_Of_Day
_ : Time_Of_Day ->
sign = Time_Utils.compare_to_localtime self that
0 == sign
_ -> False

View File

@ -169,10 +169,3 @@ type Time_Zone
type_pair = ["type", "Time_Zone"]
cons_pair = ["constructor", "new"]
JS_Object.from_pairs [type_pair, cons_pair, ["id", self.zone_id]]
## Compares two Zones for equality.
== : Time_Zone -> Boolean
== self that =
case that of
_ : Time_Zone -> Time_Utils.equals_zone self that
_ -> False

View File

@ -602,28 +602,6 @@ type Vector a
"and " + remaining_count.to_text + " more elements"
prefix.map .to_text . join ", " "[" " "+remaining_text+"]"
## Checks whether this vector is equal to `that`.
Arguments:
- that: The vector to compare this vector against.
Two vectors are considered equal, when they have the same length and
their items are pairwise equal.
> Example
Compare two vectors for equality (this case is false).
[1, 2, 3] == [2, 3, 4]
== : Vector -> Boolean
== self that = case that of
_ : Vector ->
eq_at i = self.at i == that.at i
if self.length == that.length then 0.up_to self.length . all eq_at else False
_ : Array ->
eq_at i = self.at i == that.at i
if self.length == that.length then 0.up_to self.length . all eq_at else False
_ -> False
## Concatenates two vectors, resulting in a new vector, containing all the
elements of `self`, followed by all the elements of `that`.

View File

@ -133,10 +133,3 @@ type Error
1.is_error
is_error : Boolean
is_error self = True
## PRIVATE
TODO this is a kludge until we have proper eigentypes and statics.
Allows to check equality of the `Error` type with itself.
== self that = if Meta.is_error self then self else
if Meta.is_error that then that else
Meta.is_same_object self that

View File

@ -208,14 +208,3 @@ type URI
type_pair = ["type", "URI"]
cons_pair = ["constructor", "parse"]
JS_Object.from_pairs [type_pair, cons_pair, ["uri", self.to_text]]
## Check if this URI is equal to another URI.
> Example
Check if two URIs are equal.
import Standard.Base.Network.URI.URI
example_eq = "https://example.com".to_uri == "http://example.org".to_uri
== : URI -> Boolean
== self that = self.internal_uri.equals that.internal_uri

View File

@ -427,17 +427,6 @@ type File
normalize : File
normalize self = @Builtin_Method "File.normalize"
## Checks if this file has the same `path` as `that`.
> Example
Check if two files are equivalent.
import Standard.Examples
example_eq = Examples.csv == Examples.scratch_file
== : File -> Boolean
== self that = @Builtin_Method "File.=="
## Deletes the file.
If the file is a directory, it must be empty, otherwise a `Panic` will

View File

@ -32,7 +32,7 @@ check_integrity entity1 entity2 =
- entity2: The entity to check against the first.
check_connection : (Table | Column) -> (Table | Column) -> Boolean
check_connection entity1 entity2 =
entity1.connection == entity2.connection
Meta.is_same_object entity1.connection entity2.connection
## PRIVATE

View File

@ -157,7 +157,7 @@ using GitHub API, similarly as Enso releases.
The primary purpose of the launcher is running various Enso components, namely
the REPL, running a project, Enso scripts or the language server.
The launcher automatically infers which Enso version to used, based on the
The launcher automatically infers which Enso version to use, based on the
parameters and configuration:
- When running a project or the language server, the version specified in

View File

@ -0,0 +1,400 @@
package org.enso.interpreter.bench.benchmarks.semantic;
import java.io.ByteArrayOutputStream;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.enso.polyglot.MethodNames.Module;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.BenchmarkParams;
import org.openjdk.jmh.infra.Blackhole;
/**
* Benchmarks for `Any.==` method. This benchmark takes two vectors as input, and compares each
* pair of elements with `==`.
*/
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 3)
@Measurement(iterations = 2)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class EqualsBenchmarks {
private static final int primitiveVectorSize = 4_000;
private static final int stringsVectorSize = 3_000;
/**
* Maximum length of randomly generated strings.
*/
private static final int maxStringSize = 20;
/**
* Maximum depth of a tree (Node type). Every Node can have up to 5 children.
*/
private static final int maxTreeDepth = 4;
/**
* Size of the vector of trees (Node type).
*/
private static final int treeVectorSize = 500;
private Value module;
private Value benchFunc;
@Setup
public void initializeBenchmark(BenchmarkParams params) throws Exception {
var random = new Random(42);
var ctx = Context.newBuilder()
.allowExperimentalOptions(true)
.logHandler(new ByteArrayOutputStream())
.allowIO(true)
.allowAllAccess(true)
.option(
"enso.languageHomeOverride",
Paths.get("../../distribution/component").toFile().getAbsolutePath()
).build();
var benchmarkName = params.getBenchmark().replaceFirst(".*\\.", "");
var codeBuilder = new StringBuilder("""
import Standard.Base.Data.Range.Extensions
type Node
C1 f1
C2 f1 f2
C3 f1 f2 f3
C4 f1 f2 f3 f4
C5 f1 f2 f3 f4 f5
Nil
Value value
eq_vec vec1 vec2 =
(0.up_to vec1.length).map idx->
(vec1.at idx) == (vec2.at idx)
eq x y = x == y
"""
);
// Indexes where `True` is expected. Inside the generated vectors, on a predefined indexes,
// we put "constant" values, such that when the elements at these indexes are compared,
// `True` is returned.
Set<Integer> trueExpectedAt;
switch (benchmarkName) {
case "equalsPrimitives" -> {
trueExpectedAt = Set.of(
primitiveVectorSize / 2,
primitiveVectorSize / 4,
primitiveVectorSize / 8,
primitiveVectorSize / 16,
primitiveVectorSize / 32,
primitiveVectorSize / 64
);
codeBuilder
.append(
generateVectorOfPrimitives(primitiveVectorSize, "vec1", 42, trueExpectedAt, random)
)
.append("\n")
.append(
generateVectorOfPrimitives(primitiveVectorSize, "vec2", 42, trueExpectedAt, random)
)
.append("\n");
}
case "equalsStrings" -> {
trueExpectedAt = Set.of(
treeVectorSize / 2,
treeVectorSize / 4,
treeVectorSize / 8
);
codeBuilder
.append(
generateVectorOfStrings(stringsVectorSize, "vec1", "AAA", trueExpectedAt, random)
)
.append("\n")
.append(
generateVectorOfStrings(stringsVectorSize, "vec2", "AAA", trueExpectedAt, random)
)
.append("\n");
}
case "equalsTrees" -> {
trueExpectedAt = Set.of(
treeVectorSize / 2,
treeVectorSize / 4,
treeVectorSize / 8,
treeVectorSize / 16
);
codeBuilder
.append(
generateVectorOfTrees(treeVectorSize, "vec1", maxTreeDepth, createNilNode(), trueExpectedAt, random)
)
.append("\n")
.append(
generateVectorOfTrees(treeVectorSize, "vec2", maxTreeDepth, createNilNode(), trueExpectedAt, random)
)
.append("\n");
}
default ->
throw new IllegalStateException("Unexpected benchmark: " + params.getBenchmark());
}
codeBuilder.append("""
bench x = eq_vec vec1 vec2
""");
module = ctx.eval(
SrcUtil.source(benchmarkName, codeBuilder.toString())
);
benchFunc = module.invokeMember(Module.EVAL_EXPRESSION, "bench");
// Sanity check - elements in `trueExpectedAt` should be the same
var eqFunc = module.invokeMember(Module.EVAL_EXPRESSION, "eq");
var vec1 = module.invokeMember(Module.EVAL_EXPRESSION, "vec1");
var vec2 = module.invokeMember(Module.EVAL_EXPRESSION, "vec2");
for (Integer idx : trueExpectedAt) {
var result = eqFunc.execute(vec1.getArrayElement(idx), vec2.getArrayElement(idx));
if (!result.asBoolean()) {
throw new AssertionError("Elements of input vectors at " + idx + " should be the same.");
}
}
}
/**
* Iterates over {@link #primitiveVectorSize} long vector of random generated primitive values - integers,
* doubles, and strings
* @param blackHole
*/
@Benchmark
public void equalsPrimitives(Blackhole blackHole) {
performBenchmark(blackHole);
}
@Benchmark
public void equalsStrings(Blackhole blackhole) {
performBenchmark(blackhole);
}
@Benchmark
public void equalsTrees(Blackhole blackhole) {
performBenchmark(blackhole);
}
private void performBenchmark(Blackhole blackhole) {
Object res = benchFunc.execute(0);
blackhole.consume(res);
}
/**
* Generates source code for a vector of primitive values. The vector will contain integers and
* doubles. Count of elements of these different value types is equally distributed,
* i.e., there is exact same amount of integers and doubles. Vector is
* shuffled, so that there should not be a long consecutive range of values of just one type.
* <p>
* Generates code of form {@code vecName = [...]}
*
* @param totalSize Total size of the generated vector.
* @param vecName Name of the generated vector.
* @param identityElem A primitive element considered an identity with respect to `==` operator,
* will be put in indexes denoted by {@code constantIdxs}
* @param constantIdxs Indexes where {@code identityElem} will be put.
* @param random Random number generator.
* @return Source of the generated vector
*/
private static String generateVectorOfPrimitives(int totalSize, String vecName, Object identityElem, Collection<Integer> constantIdxs, Random random) {
var partSize = totalSize / 2;
List<Object> primitiveValues = new ArrayList<>();
random.ints(partSize).forEach(primitiveValues::add);
random.doubles(partSize).forEach(primitiveValues::add);
Collections.shuffle(primitiveValues, random);
for (Integer constantIdx : constantIdxs) {
primitiveValues.set(constantIdx, identityElem);
}
var sb = new StringBuilder();
sb.append(vecName).append(" = [");
for (Object primitiveValue : primitiveValues) {
if (primitiveValue instanceof Double dbl) {
sb.append(String.format("%f", dbl)).append(",");
} else {
sb.append(primitiveValue).append(",");
}
}
// Replace last comma
sb.setCharAt(sb.length() - 1, ']');
return sb.toString();
}
private static String generateVectorOfStrings(int size, String vecName, String identityElem, Collection<Integer> identityIdxs, Random random) {
var sb = new StringBuilder();
sb.append(vecName).append(" = [");
for (int i = 0; i < size; i++) {
var stringSize = random.nextInt(maxStringSize);
var str = identityIdxs.contains(i) ? identityElem : randomString(stringSize, random);
sb.append("\"").append(str).append("\"").append(",");
}
// Replace last comma
sb.setCharAt(sb.length() - 1, ']');
return sb.toString();
}
private static Node createNilNode() {
return new NilNode(0, null);
}
/**
* Generates source code for a vector of trees (Node type), i.e., generates an expression
* {@code vecName = [...]}.
*
* @param size Total size of the generated vector.
* @param vecName How the vector should be named.
* @param maxDepth Maximum depth of the generated tree. Note that there is no lower bound, so
* the generated tree can have depth 1.
* @param identityNode A node that is considered an identity with respect to `==` operator.
* This node will be put on every indes of {@code constantIdxs}.
* @param constantIdxs Indexes in the vector where {@code identityNode} should be put.
* @param random Random number generator.
* @return Source code for the generated tree.
*/
private static String generateVectorOfTrees(int size, String vecName, int maxDepth, Node identityNode, Collection<Integer> constantIdxs, Random random) {
var trees = new ArrayList<Node>();
for (int i = 0; i < size; i++) {
trees.add(
generateTree(null, 0, random, maxDepth)
);
}
for (Integer constantIdx : constantIdxs) {
trees.set(constantIdx, identityNode);
}
var sb = new StringBuilder();
sb.append(vecName).append(" = [");
for (Node tree : trees) {
sb.append(tree.createSource()).append(",");
}
sb.setCharAt(sb.length() - 1, ']');
return sb.toString();
}
private static Node generateTree(Node parent, int currDepth, Random random, int maxDepth) {
Node node;
if (currDepth == maxDepth) {
node = new NilNode(currDepth, parent);
} else {
if (random.nextBoolean() && currDepth > 0) {
node = new ValueNode(
currDepth,
parent,
random.nextInt()
);
} else {
node = new NodeWithChildren(currDepth, parent);
// childCount is between 1..5
var childCount = Math.max(random.nextInt(5), 1);
for (int i = 0; i < childCount; i++) {
generateTree(node, currDepth + 1, random, maxDepth);
}
}
}
if (parent instanceof NodeWithChildren parentWithChildren) {
parentWithChildren.addChild(node);
} else if (parent != null){
throw new AssertionError("expected parent to be NodeWithChildren or null");
}
return node;
}
private static final String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQESRTUVWXYZ";
private static String randomString(int size, Random random) {
var sb = new StringBuilder(size);
random
.ints(size, 0, characters.length())
.mapToObj(characters::charAt)
.forEach(sb::append);
return sb.toString();
}
/**
* A simple hierarchy of Node classes that simplifies source code generation.
*/
abstract static class Node {
final int depth;
final Node parent;
Node(int depth, Node parent) {
this.depth = depth;
this.parent = parent;
}
abstract String createSource();
}
private static final class ValueNode extends Node {
final int value;
ValueNode(int depth, Node parent, int value) {
super(depth, parent);
this.value = value;
}
@Override
public String createSource() {
return "(Node.Value " + value + ")";
}
}
private static final class NilNode extends Node {
NilNode(int depth, Node parent) {
super(depth, parent);
}
@Override
String createSource() {
return "Node.Nil";
}
}
private static final class NodeWithChildren extends Node {
private List<Node> children = new ArrayList<>();
NodeWithChildren(int depth, Node parent) {
super(depth, parent);
}
void addChild(Node node) {
children.add(node);
}
@Override
public String createSource() {
String ctor = switch(children.size()) {
case 1 -> "(Node.C1 ";
case 2 -> "(Node.C2 ";
case 3 -> "(Node.C3 ";
case 4 -> "(Node.C4 ";
case 5 -> "(Node.C5 ";
default -> throw new AssertionError("Unexpected number of children: " + children.size());
};
var sb = new StringBuilder();
sb.append(ctor);
for (Node child : children) {
sb.append(child.createSource()).append(" ");
}
sb.append(")");
return sb.toString();
}
}
}

View File

@ -1,25 +0,0 @@
package org.enso.interpreter.node.expression.builtin.bool;
import com.oracle.truffle.api.dsl.Fallback;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.dsl.BuiltinMethod;
@BuiltinMethod(type = "Boolean", name = "==", description = "Computes the equality of two booleans")
public abstract class EqualsNode extends Node {
abstract boolean execute(Object self, Object that);
static EqualsNode build() {
return EqualsNodeGen.create();
}
@Specialization
boolean doBoolean(boolean self, boolean that) {
return self == that;
}
@Fallback
boolean doOther(Object self, Object that) {
return self == that;
}
}

View File

@ -0,0 +1,557 @@
package org.enso.interpreter.node.expression.builtin.meta;
import com.ibm.icu.text.Normalizer;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Fallback;
import com.oracle.truffle.api.dsl.GenerateUncached;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.interop.ArityException;
import com.oracle.truffle.api.interop.InteropLibrary;
import com.oracle.truffle.api.interop.InvalidArrayIndexException;
import com.oracle.truffle.api.interop.UnknownIdentifierException;
import com.oracle.truffle.api.interop.UnsupportedMessageException;
import com.oracle.truffle.api.interop.UnsupportedTypeException;
import com.oracle.truffle.api.library.CachedLibrary;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.profiles.ConditionProfile;
import com.oracle.truffle.api.profiles.LoopConditionProfile;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Map;
import org.enso.interpreter.dsl.AcceptsError;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.node.callable.ExecuteCallNode;
import org.enso.interpreter.node.expression.builtin.meta.EqualsAnyNodeGen.InvokeEqualsNodeGen;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.callable.UnresolvedConversion;
import org.enso.interpreter.runtime.callable.UnresolvedSymbol;
import org.enso.interpreter.runtime.callable.atom.Atom;
import org.enso.interpreter.runtime.callable.atom.AtomConstructor;
import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.data.Type;
import org.enso.interpreter.runtime.error.WarningsLibrary;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
import org.enso.interpreter.runtime.state.State;
import org.enso.polyglot.MethodNames;
@BuiltinMethod(
type = "Any",
name = "==",
description = "Implementation of Any.=="
)
@GenerateUncached
public abstract class EqualsAnyNode extends Node {
protected static String EQUALS_MEMBER_NAME = MethodNames.Function.EQUALS;
public static EqualsAnyNode build() {
return EqualsAnyNodeGen.create();
}
public abstract boolean execute(@AcceptsError Object self, @AcceptsError Object right);
/** Primitive values **/
@Specialization
boolean equalsBoolean(boolean self, boolean other) {
return self == other;
}
@Specialization
boolean equalsLong(long self, long other) {
return self == other;
}
@Specialization
boolean equalsDouble(double self, double other) {
return self == other;
}
@Specialization
boolean equalsLongDouble(long self, double other) {
return (double) self == other;
}
@Specialization
boolean equalsDoubleLong(double self, long other) {
return self == (double) other;
}
@Specialization
boolean equalsIntLong(int self, long other) {
return (long) self == other;
}
@Specialization
boolean equalsLongInt(long self, int other) {
return self == (long) other;
}
@Specialization
boolean equalsIntDouble(int self, double other) {
return (double) self == other;
}
@Specialization
boolean equalsDoubleInt(double self, int other) {
return self == (double) other;
}
@Specialization
@TruffleBoundary
boolean equalsBigInt(EnsoBigInteger self, EnsoBigInteger otherBigInt) {
return self.equals(otherBigInt);
}
@Specialization
@TruffleBoundary
boolean equalsBitIntDouble(EnsoBigInteger self, double other) {
return self.doubleValue() == other;
}
@Specialization
@TruffleBoundary
boolean equalsDoubleBigInt(double self, EnsoBigInteger other) {
return self == other.doubleValue();
}
/** Enso specific types **/
@Specialization
boolean equalsUnresolvedSymbols(UnresolvedSymbol self, UnresolvedSymbol otherSymbol,
@Cached EqualsAnyNode equalsNode) {
return self.getName().equals(otherSymbol.getName())
&& equalsNode.execute(self.getScope(), otherSymbol.getScope());
}
@Specialization
boolean equalsUnresolvedConversion(UnresolvedConversion selfConversion, UnresolvedConversion otherConversion,
@Cached EqualsAnyNode equalsNode) {
return equalsNode.execute(selfConversion.getScope(), otherConversion.getScope());
}
/**
* If one of the objects has warnings attached, just treat it as an object without any
* warnings.
*/
@Specialization(guards = {
"selfWarnLib.hasWarnings(selfWithWarnings) || otherWarnLib.hasWarnings(otherWithWarnings)"
}, limit = "3")
boolean equalsWithWarnings(Object selfWithWarnings, Object otherWithWarnings,
@CachedLibrary("selfWithWarnings") WarningsLibrary selfWarnLib,
@CachedLibrary("otherWithWarnings") WarningsLibrary otherWarnLib,
@Cached EqualsAnyNode equalsNode
) {
try {
Object self =
selfWarnLib.hasWarnings(selfWithWarnings) ? selfWarnLib.removeWarnings(selfWithWarnings)
: selfWithWarnings;
Object other =
otherWarnLib.hasWarnings(otherWithWarnings) ? otherWarnLib.removeWarnings(otherWithWarnings)
: otherWithWarnings;
return equalsNode.execute(self, other);
} catch (UnsupportedMessageException e) {
throw new IllegalStateException(e);
}
}
/** Interop libraries **/
@Specialization(guards = {
"selfInterop.isNull(selfNull)",
"otherInterop.isNull(otherNull)"
}, limit = "3")
boolean equalsNull(
Object selfNull, Object otherNull,
@CachedLibrary("selfNull") InteropLibrary selfInterop,
@CachedLibrary("otherNull") InteropLibrary otherInterop
) {
return true;
}
@Specialization(guards = {
"selfInterop.isBoolean(selfBoolean)",
"otherInterop.isBoolean(otherBoolean)"
}, limit = "3")
boolean equalsBooleanInterop(
Object selfBoolean,
Object otherBoolean,
@CachedLibrary("selfBoolean") InteropLibrary selfInterop,
@CachedLibrary("otherBoolean") InteropLibrary otherInterop
) {
try {
return selfInterop.asBoolean(selfBoolean) == otherInterop.asBoolean(otherBoolean);
} catch (UnsupportedMessageException e) {
throw new IllegalStateException(e);
}
}
@Specialization(guards = {
"!selfInterop.isDate(selfTimeZone)",
"!selfInterop.isTime(selfTimeZone)",
"selfInterop.isTimeZone(selfTimeZone)",
"!otherInterop.isDate(otherTimeZone)",
"!otherInterop.isTime(otherTimeZone)",
"otherInterop.isTimeZone(otherTimeZone)"
}, limit = "3")
boolean equalsTimeZones(Object selfTimeZone, Object otherTimeZone,
@CachedLibrary("selfTimeZone") InteropLibrary selfInterop,
@CachedLibrary("otherTimeZone") InteropLibrary otherInterop) {
try {
return selfInterop.asTimeZone(selfTimeZone).equals(
otherInterop.asTimeZone(otherTimeZone)
);
} catch (UnsupportedMessageException e) {
throw new IllegalStateException(e);
}
}
@TruffleBoundary
@Specialization(guards = {
"selfInterop.isDate(selfZonedDateTime)",
"selfInterop.isTime(selfZonedDateTime)",
"selfInterop.isTimeZone(selfZonedDateTime)",
"otherInterop.isDate(otherZonedDateTime)",
"otherInterop.isTime(otherZonedDateTime)",
"otherInterop.isTimeZone(otherZonedDateTime)"
}, limit = "3")
boolean equalsZonedDateTimes(Object selfZonedDateTime, Object otherZonedDateTime,
@CachedLibrary("selfZonedDateTime") InteropLibrary selfInterop,
@CachedLibrary("otherZonedDateTime") InteropLibrary otherInterop) {
try {
var self = ZonedDateTime.of(
selfInterop.asDate(selfZonedDateTime),
selfInterop.asTime(selfZonedDateTime),
selfInterop.asTimeZone(selfZonedDateTime)
);
var other = ZonedDateTime.of(
otherInterop.asDate(otherZonedDateTime),
otherInterop.asTime(otherZonedDateTime),
otherInterop.asTimeZone(otherZonedDateTime)
);
return self.isEqual(other);
} catch (UnsupportedMessageException e) {
throw new IllegalStateException(e);
}
}
@Specialization(guards = {
"selfInterop.isDate(selfDateTime)",
"selfInterop.isTime(selfDateTime)",
"!selfInterop.isTimeZone(selfDateTime)",
"otherInterop.isDate(otherDateTime)",
"otherInterop.isTime(otherDateTime)",
"!otherInterop.isTimeZone(otherDateTime)"
}, limit = "3")
boolean equalsDateTimes(Object selfDateTime, Object otherDateTime,
@CachedLibrary("selfDateTime") InteropLibrary selfInterop,
@CachedLibrary("otherDateTime") InteropLibrary otherInterop) {
try {
var self = LocalDateTime.of(
selfInterop.asDate(selfDateTime),
selfInterop.asTime(selfDateTime)
);
var other = LocalDateTime.of(
otherInterop.asDate(otherDateTime),
otherInterop.asTime(otherDateTime)
);
return self.isEqual(other);
} catch (UnsupportedMessageException e) {
throw new IllegalStateException(e);
}
}
@Specialization(guards = {
"selfInterop.isDate(selfDate)",
"!selfInterop.isTime(selfDate)",
"!selfInterop.isTimeZone(selfDate)",
"otherInterop.isDate(otherDate)",
"!otherInterop.isTime(otherDate)",
"!otherInterop.isTimeZone(otherDate)"
}, limit = "3")
boolean equalsDates(Object selfDate, Object otherDate,
@CachedLibrary("selfDate") InteropLibrary selfInterop,
@CachedLibrary("otherDate") InteropLibrary otherInterop) {
try {
return selfInterop.asDate(selfDate).isEqual(
otherInterop.asDate(otherDate)
);
} catch (UnsupportedMessageException e) {
throw new IllegalStateException(e);
}
}
@Specialization(guards = {
"!selfInterop.isDate(selfTime)",
"selfInterop.isTime(selfTime)",
"!selfInterop.isTimeZone(selfTime)",
"!otherInterop.isDate(otherTime)",
"otherInterop.isTime(otherTime)",
"!otherInterop.isTimeZone(otherTime)"
}, limit = "3")
boolean equalsTimes(Object selfTime, Object otherTime,
@CachedLibrary("selfTime") InteropLibrary selfInterop,
@CachedLibrary("otherTime") InteropLibrary otherInterop) {
try {
return selfInterop.asTime(selfTime).equals(
otherInterop.asTime(otherTime)
);
} catch (UnsupportedMessageException e) {
throw new IllegalStateException(e);
}
}
@Specialization(guards = {
"selfInterop.isDuration(selfDuration)",
"otherInterop.isDuration(otherDuration)"
}, limit = "3")
boolean equalsDuration(Object selfDuration, Object otherDuration,
@CachedLibrary("selfDuration") InteropLibrary selfInterop,
@CachedLibrary("otherDuration") InteropLibrary otherInterop) {
try {
return selfInterop.asDuration(selfDuration).equals(
otherInterop.asDuration(otherDuration)
);
} catch (UnsupportedMessageException e) {
throw new IllegalStateException(e);
}
}
/**
* Compares interop strings according to the lexicographical order, handling Unicode
* normalization. See {@code Text_Utils.compare_to}.
*/
@TruffleBoundary
@Specialization(guards = {
"selfInterop.isString(selfString)",
"otherInterop.isString(otherString)"
}, 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) {
throw new IllegalStateException(e);
}
return Normalizer.compare(
selfJavaString,
otherJavaString,
Normalizer.FOLD_CASE_DEFAULT
) == 0;
}
@Specialization(guards = {
"selfInterop.hasArrayElements(selfArray)",
"otherInterop.hasArrayElements(otherArray)"
}, limit = "3")
boolean equalsArrays(Object selfArray, Object otherArray,
@CachedLibrary("selfArray") InteropLibrary selfInterop,
@CachedLibrary("otherArray") InteropLibrary otherInterop,
@Cached EqualsAnyNode equalsNode
) {
try {
long selfSize = selfInterop.getArraySize(selfArray);
if (selfSize != otherInterop.getArraySize(otherArray)) {
return false;
}
for (long i = 0; i < selfSize; i++) {
Object selfElem = selfInterop.readArrayElement(selfArray, i);
Object otherElem = otherInterop.readArrayElement(otherArray, i);
if (!equalsNode.execute(selfElem, otherElem)) {
return false;
}
}
return true;
} catch (UnsupportedMessageException | InvalidArrayIndexException e) {
throw new IllegalStateException(e);
}
}
/** Equals for Atoms and AtomConstructors */
@Specialization
boolean equalsAtomConstructors(AtomConstructor selfConstructor, AtomConstructor otherConstructor) {
return selfConstructor == otherConstructor;
}
/**
* How many {@link EqualsAnyNode} should be created for fields in specialization for atoms.
*/
static final int equalsNodeCountForFields = 10;
static EqualsAnyNode[] createEqualsNodes(int size) {
EqualsAnyNode[] nodes = new EqualsAnyNode[size];
Arrays.fill(nodes, EqualsAnyNode.build());
return nodes;
}
@Specialization
boolean equalsAtoms(Atom self, Atom other,
@Cached LoopConditionProfile loopProfile,
@Cached(value = "createEqualsNodes(equalsNodeCountForFields)", allowUncached = true) EqualsAnyNode[] fieldEqualsNodes,
@Cached InvokeEqualsNode atomInvokeEqualsNode,
@Cached ConditionProfile enoughEqualNodesForFieldsProfile,
@Cached ConditionProfile constructorsNotEqualProfile) {
if (atomOverridesEquals(self)) {
return atomInvokeEqualsNode.execute(self, other);
}
if (constructorsNotEqualProfile.profile(
self.getConstructor() != other.getConstructor()
)) {
return false;
}
assert self.getFields().length == other.getFields().length;
int fieldsSize = self.getFields().length;
if (enoughEqualNodesForFieldsProfile.profile(fieldsSize <= equalsNodeCountForFields)) {
loopProfile.profileCounted(fieldsSize);
for (int i = 0; loopProfile.inject(i < fieldsSize); i++) {
if (!fieldEqualsNodes[i].execute(
self.getFields()[i],
other.getFields()[i]
)) {
return false;
}
}
} else {
return equalsAtomsFieldsUncached(self.getFields(), other.getFields());
}
return true;
}
@TruffleBoundary
private static boolean equalsAtomsFieldsUncached(Object[] selfFields, Object[] otherFields) {
assert selfFields.length == otherFields.length;
for (int i = 0; i < selfFields.length; i++) {
boolean areFieldsSame;
if (selfFields[i] instanceof Atom selfFieldAtom
&& otherFields[i] instanceof Atom otherFieldAtom
&& atomOverridesEquals(selfFieldAtom)) {
areFieldsSame = InvokeEqualsNode.getUncached().execute(selfFieldAtom, otherFieldAtom);
} else {
areFieldsSame = EqualsAnyNodeGen.getUncached().execute(selfFields[i], otherFields[i]);
}
if (!areFieldsSame) {
return false;
}
}
return true;
}
/**
* Helper node for invoking `==` method on atoms, that override this method.
*/
@GenerateUncached
static abstract class InvokeEqualsNode extends Node {
static InvokeEqualsNode getUncached() {
return InvokeEqualsNodeGen.getUncached();
}
static InvokeEqualsNode build() {
return InvokeEqualsNodeGen.create();
}
abstract boolean execute(Atom selfAtom, Atom otherAtom);
@Specialization(guards = "cachedSelfAtomCtor == selfAtom.getConstructor()")
boolean invokeEqualsCachedAtomCtor(Atom selfAtom, Atom otherAtom,
@Cached("selfAtom.getConstructor()") AtomConstructor cachedSelfAtomCtor,
@Cached("getEqualsMethod(cachedSelfAtomCtor)") Function equalsMethod,
@Cached ExecuteCallNode executeCallNode,
@CachedLibrary(limit = "3") InteropLibrary interop) {
assert atomOverridesEquals(selfAtom);
Object ret = executeCallNode.executeCall(
equalsMethod,
null,
State.create(EnsoContext.get(this)),
new Object[]{selfAtom, otherAtom}
);
try {
return interop.asBoolean(ret);
} catch (UnsupportedMessageException e) {
throw new IllegalStateException(e);
}
}
@TruffleBoundary
@Specialization(replaces = "invokeEqualsCachedAtomCtor")
boolean invokeEqualsUncached(Atom selfAtom, Atom otherAtom,
@Cached ExecuteCallNode executeCallNode) {
Function equalsMethod = getEqualsMethod(selfAtom.getConstructor());
Object ret = executeCallNode.executeCall(
equalsMethod,
null,
State.create(EnsoContext.get(this)),
new Object[]{selfAtom, otherAtom}
);
assert InteropLibrary.getUncached().isBoolean(ret);
try {
return InteropLibrary.getUncached().asBoolean(ret);
} catch (UnsupportedMessageException e) {
throw new IllegalStateException(e);
}
}
@TruffleBoundary
static Function getEqualsMethod(AtomConstructor atomConstructor) {
Type atomType = atomConstructor.getType();
Function equalsFunction = atomConstructor
.getDefinitionScope()
.getMethods()
.get(atomType)
.get("==");
assert equalsFunction != null;
return equalsFunction;
}
}
/**
* Returns true if the given atom overrides `==` operator.
*/
@TruffleBoundary
private static boolean atomOverridesEquals(Atom atom) {
var atomType = atom.getConstructor().getType();
Map<String, Function> methodsOnType = atom
.getConstructor()
.getDefinitionScope()
.getMethods()
.get(atomType);
if (methodsOnType != null) {
return methodsOnType.containsKey("==");
} else {
return false;
}
}
@Fallback
@TruffleBoundary
boolean equalsGeneric(Object left, Object right,
@CachedLibrary(limit = "5") InteropLibrary interop) {
EnsoContext ctx = EnsoContext.get(interop);
if (isHostObject(ctx, left) && isHostObject(ctx, right)) {
try {
return interop.asBoolean(
interop.invokeMember(left, "equals", right)
);
} catch (UnsupportedMessageException | ArityException | UnknownIdentifierException |
UnsupportedTypeException e) {
throw new IllegalStateException(e);
}
} else {
return left == right
|| left.equals(right)
|| interop.isIdentical(left, right, interop);
}
}
private static boolean isHostObject(EnsoContext context, Object object) {
return context.getEnvironment().isHostObject(object);
}
}

View File

@ -1,33 +0,0 @@
package org.enso.interpreter.node.expression.builtin.number.bigInteger;
import com.oracle.truffle.api.dsl.Fallback;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.node.expression.builtin.number.utils.BigIntegerOps;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
@BuiltinMethod(type = "Big_Integer", name = "==", description = "Big integer equality.")
public abstract class EqualsNode extends Node {
abstract boolean execute(Object self, Object that);
static EqualsNode build() {
return EqualsNodeGen.create();
}
@Specialization
boolean doBigInt(EnsoBigInteger self, EnsoBigInteger that) {
return BigIntegerOps.equals(self.getValue(), that.getValue());
}
@Specialization
boolean doDouble(EnsoBigInteger self, double that) {
return BigIntegerOps.toDouble(self.getValue()) == that;
}
@Fallback
boolean doOther(Object self, Object that) {
return self == that;
}
}

View File

@ -1,37 +0,0 @@
package org.enso.interpreter.node.expression.builtin.number.decimal;
import com.oracle.truffle.api.dsl.Fallback;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.node.expression.builtin.number.utils.BigIntegerOps;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
@BuiltinMethod(type = "Decimal", name = "==", description = "Equality on numbers.")
public abstract class EqualsNode extends Node {
abstract boolean execute(Object self, Object that);
static EqualsNode build() {
return EqualsNodeGen.create();
}
@Specialization
boolean doDouble(double self, double that) {
return self == that;
}
@Specialization
boolean doLong(double self, long that) {
return self == (double) that;
}
@Specialization
boolean doBigInteger(double self, EnsoBigInteger that) {
return self == BigIntegerOps.toDouble(that.getValue());
}
@Fallback
boolean doOther(Object self, Object that) {
return self == that;
}
}

View File

@ -1,31 +0,0 @@
package org.enso.interpreter.node.expression.builtin.number.smallInteger;
import com.oracle.truffle.api.dsl.Fallback;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.dsl.BuiltinMethod;
@BuiltinMethod(type = "Small_Integer", name = "==", description = "Equality on numbers.")
public abstract class EqualsNode extends Node {
abstract boolean execute(Object self, Object that);
static EqualsNode build() {
return EqualsNodeGen.create();
}
@Specialization
boolean doLong(long self, long that) {
return self == that;
}
@Specialization
boolean doDouble(long self, double that) {
return (double) self == that;
}
@Fallback
boolean doOther(Object self, Object that) {
return self == that;
}
}

View File

@ -120,7 +120,7 @@ public final class EnsoDate implements TruffleObject {
@CompilerDirectives.TruffleBoundary
@ExportMessage
public final Object toDisplayString(boolean allowSideEffects) {
public Object toDisplayString(boolean allowSideEffects) {
return DateTimeFormatter.ISO_LOCAL_DATE.format(date);
}
}

View File

@ -180,14 +180,6 @@ public final class EnsoDuration implements TruffleObject {
return duration.compareTo(interop.asDuration(durationObject));
}
@Builtin.Method(name = "equals_builtin")
@Builtin.Specialize
@Builtin.WrapException(from = UnsupportedMessageException.class)
public boolean equalsDuration(Object durationObject, InteropLibrary interop)
throws UnsupportedMessageException {
return duration.equals(interop.asDuration(durationObject));
}
@ExportMessage
public boolean isDuration() {
return true;

View File

@ -1,6 +1,7 @@
package org.enso.interpreter.runtime.data;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.TruffleFile;
import com.oracle.truffle.api.interop.InteropLibrary;
import com.oracle.truffle.api.interop.TruffleObject;
@ -180,13 +181,14 @@ public final class EnsoFile implements TruffleObject {
return this.truffleFile.getName();
}
@Builtin.Method(name = "==")
@CompilerDirectives.TruffleBoundary
public boolean isEqual(EnsoFile that) {
// It seems that fsContext is not equal in files coming from stacktraces.
// Once a solution to that is found replace it with a simple
// return this.truffleFile.equals(that.truffleFile);
return this.getPath().equals(that.getPath());
@TruffleBoundary
@Override
public boolean equals(Object obj) {
if (obj instanceof EnsoFile otherFile) {
return truffleFile.getPath().equals(otherFile.truffleFile.getPath());
} else {
return false;
}
}
@Builtin.Method

View File

@ -143,7 +143,7 @@ public final class EnsoTimeOfDay implements TruffleObject {
}
@ExportMessage
final boolean isDate() {
boolean isDate() {
return false;
}

View File

@ -103,12 +103,12 @@ public final class Vector implements TruffleObject {
* @return {@code true}
*/
@ExportMessage
public boolean hasArrayElements() {
boolean hasArrayElements() {
return true;
}
@ExportMessage
public long getArraySize(@CachedLibrary(limit = "3") InteropLibrary interop)
long getArraySize(@CachedLibrary(limit = "3") InteropLibrary interop)
throws UnsupportedMessageException {
return interop.getArraySize(storage);
}
@ -133,7 +133,6 @@ public final class Vector implements TruffleObject {
if (warnings.hasWarnings(v)) {
v = warnings.removeWarnings(v);
}
;
return new WithWarnings(toEnso.execute(v), extracted);
}
return toEnso.execute(v);

View File

@ -156,4 +156,13 @@ public final class EnsoBigInteger extends Number implements TruffleObject {
public double doubleValue() {
return value.doubleValue();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof EnsoBigInteger otherBigInt) {
return value.equals(otherBigInt.value);
} else {
return false;
}
}
}

View File

@ -728,7 +728,6 @@ object BindingsMap {
* @param name the name of the constructor.
* @param arity the number of fields in the constructor.
* @param allFieldsDefaulted whether all fields provide a default value.
* @param builtinType true if constructor is annotated with @Builtin_Type, false otherwise.
*/
case class Cons(name: String, arity: Int, allFieldsDefaulted: Boolean)
@ -736,6 +735,7 @@ object BindingsMap {
*
* @param name the type name
* @param members the member names
* @param builtinType true if constructor is annotated with @Builtin_Type, false otherwise.
*/
case class Type(
override val name: String,

View File

@ -34,6 +34,10 @@ spec =
arr = make_enso_array [1, 2, 3]
arr.method . should_equal 0
Test.specify "should handle ==" <|
(make_enso_array [1,2,3]).should_equal (make_enso_array [1,2,3])
(make_enso_array [1]).should_not_equal (make_enso_array [2])
Test.specify "should propagate dataflow errors" <|
err = Error.throw (Illegal_State.Error "Foo")
res = Array.new err

View File

@ -23,6 +23,13 @@ spec =
True.compare_to False . should_equal Ordering.Greater
False.compare_to True . should_equal Ordering.Less
Test.specify "should allow == operator" <|
True.should_equal True
False.should_equal False
True.should_not_equal False
False.should_not_equal True
(1 == 1).should_equal True
Test.specify "should allow for extending Bools in a local module" <|
test = 1 == 2
test.method . should_equal test

View File

@ -33,6 +33,12 @@ spec =
d3 . should_equal d4
d4 . should_equal d5
Test.specify "should provide equality operator" <|
(Date_Time.new 2022 zone=(Time_Zone.parse "CET")).should_not_equal (Date_Time.new 2022 zone=(Time_Zone.parse "UTC"))
(Date_Time.new 2022 zone=(Time_Zone.parse "CET")).should_equal (Date_Time.new 2022 zone=(Time_Zone.parse "CET"))
(Date_Time.new 2022 12 12).should_equal (Date_Time.new 2022 12 12)
(Date_Time.new 2022 12 12).should_not_equal (Date_Time.new 1996)
spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision=False =
Test.group name <|

View File

@ -12,6 +12,9 @@ spec =
Test.group "Zone" <|
Test.specify "should get system zone id" <|
Time_Zone.system
Test.specify "Different time zones should not equal" <|
(Time_Zone.parse "UTC").should_not_equal (Time_Zone.parse "CET")
(Time_Zone.parse "UTC").should_equal (Time_Zone.parse "UTC")
Test.specify "should parse UTC zone" <|
zone = "UTC"
id = Time_Zone.parse zone

View File

@ -12,6 +12,7 @@ import project.Semantic.Import_Loop.Spec as Import_Loop_Spec
import project.Semantic.Meta_Spec
import project.Semantic.Meta_Location_Spec
import project.Semantic.Names_Spec
import project.Semantic.Equals_Spec
import project.Semantic.Runtime_Spec
import project.Semantic.Self_Type_Spec
import project.Semantic.Warnings_Spec
@ -108,6 +109,7 @@ main = Test_Suite.run_main <|
Meta_Spec.spec
Meta_Location_Spec.spec
Names_Spec.spec
Equals_Spec.spec
Noise_Generator_Spec.spec
Noise_Spec.spec
Numbers_Spec.spec

View File

@ -31,4 +31,8 @@ spec =
Test.specify "should return Syntax_Error when parsing invalid URI" <|
URI.parse "a b c" . should_fail_with Syntax_Error.Error
Test.specify "should compare two URIs for equality" <|
(URI.parse "http://google.com").should_equal (URI.parse "http://google.com")
(URI.parse "http://google.com").should_not_equal (URI.parse "http://amazon.com")
main = Test_Suite.run_main spec

View File

@ -0,0 +1,186 @@
from Standard.Base import all
from Standard.Test import Test, Test_Suite
import Standard.Test.Extensions
polyglot java import java.util.HashMap
type CustomEqType
C1 f1
C2 f1 f2
sum self = case self of
CustomEqType.C1 f1 -> f1
CustomEqType.C2 f1 f2 -> f1 + f2
== self other = self.sum == other.sum
type Child
Value number
== : Any -> Boolean
== self other = case other of
_ : Child -> (self.number % 100) == (other.number % 100)
_ -> False
type Parent
Value child
== : Any -> Boolean
== self other = case other of
_ : Parent -> self.child == other.child
_ -> False
type GrandParent
Value parent
== : Any -> Boolean
== self other = case other of
_ : GrandParent -> self.parent == other.parent
_ -> False
type ManyFieldType
Value f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 f13 f14 f15
type FourFieldType
Value f1 f2 f3 f4
type Point
Value x y
type Rect
Value (a:Point) (b:Point)
type Node
C1 f1
C2 f1 f2
C3 f1 f2 f3
C4 f1 f2 f3 f4
C5 f1 f2 f3 f4 f5
Nil
Value value
## Deep copy the tree to ensure that we cannot do Meta.is_same_object shortcut.
deep_copy : Node
deep_copy self =
case self of
Node.Nil -> Node.Nil
Node.Value value -> Node.Value value
Node.C1 f1 -> Node.C1 (f1.deep_copy)
Node.C2 f1 f2 -> Node.C2 (f1.deep_copy) (f2.deep_copy)
Node.C3 f1 f2 f3 -> Node.C3 (f1.deep_copy) (f2.deep_copy) (f3.deep_copy)
Node.C4 f1 f2 f3 f4 -> Node.C4 (f1.deep_copy) (f2.deep_copy) (f3.deep_copy) (f4.deep_copy)
Node.C5 f1 f2 f3 f4 f5 -> Node.C5 (f1.deep_copy) (f2.deep_copy) (f3.deep_copy) (f4.deep_copy) (f5.deep_copy)
_ -> Error.throw ("Unexpected type " + self.to_text)
create_random_tree : Integer -> Java_Random -> Integer
create_random_tree max_depth rnd cur_depth=0 =
if cur_depth == max_depth then Node.Nil else
next_depth = cur_depth + 1
children_count = rnd.nextInt 6
children = (0.up_to children_count).map _-> @Tail_Call create_random_tree max_depth rnd next_depth
case children_count of
0 -> Node.Value 42
1 -> Node.C1 (children.at 0)
2 -> Node.C2 (children.at 0) (children.at 1)
3 -> Node.C3 (children.at 0) (children.at 1) (children.at 2)
4 -> Node.C4 (children.at 0) (children.at 1) (children.at 2) (children.at 3)
5 -> Node.C5 (children.at 0) (children.at 1) (children.at 2) (children.at 3) (children.at 4)
_ -> Error.throw ("Unexpected number of children: " + children_count.to_text)
foreign js js_false = """
return false
foreign js js_true = """
return true
foreign js js_text_foo = """
return 'foo'
spec =
Test.group "Operator ==" <|
Test.specify "should handle primitive values" <|
(2 == (2.0)).should_be_true
(2 == (2.1)).should_be_false
(2.0).should_equal 2
(js_true == True).should_be_true
(js_false == False).should_be_true
(js_true == False).should_be_false
(js_text_foo == "foo").should_be_true
Test.specify "should dispatch to overriden `==` on atoms" <|
child1 = Child.Value 11
parent1 = Parent.Value child1
grand_parent1 = GrandParent.Value parent1
child2 = Child.Value 111
parent2 = Parent.Value child2
grand_parent2 = GrandParent.Value parent2
(grand_parent1 == grand_parent2).should_be_true
Test.specify "should handle `==` on types with many fields" <|
many_fields1 = ManyFieldType.Value (Child.Value 1) (Child.Value 2) (Child.Value 3) (Child.Value 4) (Child.Value 5) (Child.Value 6) (Child.Value 7) (Child.Value 8) (Child.Value 9) (Child.Value 10) (Child.Value 11) (Child.Value 12) (Child.Value 13) (Child.Value 14) (Child.Value 15)
many_fields2 = ManyFieldType.Value (Child.Value 101) (Child.Value 102) (Child.Value 103) (Child.Value 104) (Child.Value 105) (Child.Value 106) (Child.Value 107) (Child.Value 108) (Child.Value 109) (Child.Value 110) (Child.Value 111) (Child.Value 112) (Child.Value 113) (Child.Value 114) (Child.Value 115)
(many_fields1 == many_fields2).should_be_true
Test.specify "should be able to compare atoms with different constructors" <|
((CustomEqType.C1 10) == (CustomEqType.C2 7 3)).should_be_true
((CustomEqType.C1 0) == (CustomEqType.C2 7 3)).should_be_false
Test.specify "should dispatch to equals on host values" <|
java_object1 = HashMap.new
java_object1.put "a" 1
java_object1.put "b" 2
java_object2 = HashMap.new
java_object2.put "b" 2
java_object2.put "a" 1
(java_object1 == java_object2).should_be_true
java_object2.put "c" 42
(java_object1 == java_object2).should_be_false
Test.specify "should return False for different Atoms with same fields" <|
p1 = Point.Value 1 2
p2 = Point.Value 3 4
rect = Rect.Value p1 p2
four_field = FourFieldType.Value 1 2 3 4
(rect == four_field).should_be_false
Test.specify "should handle `==` on types" pending="Blocked by https://www.pivotaltracker.com/story/show/184092284" <|
(Child == Child).should_be_true
(Child == Point).should_be_false
(Point == Child).should_be_false
(HashMap == Child).should_be_false
(Child == HashMap).should_be_false
(Boolean == Any).should_be_false
(Any == Boolean).should_be_false
(Any == Any).should_be_true
(Boolean == Boolean).should_be_true
Test.specify "should dispatch to overriden `==` in vectors" <|
([(Child.Value 1)] == [(Child.Value 101)]).should_be_true
([(Child.Value 1)] == [(Child.Value 2)]).should_be_false
Test.specify "should dispatch to overriden `==` in arrays" <|
((Array.new_1 (Child.Value 1)) == (Array.new_1 (Child.Value 101))).should_be_true
Test.specify "should handle recursive atoms without custom `==`" <|
rnd = (Random.new seed=42).java_random
trees = (0.up_to 5).map _->
create_random_tree 5 rnd
trees.each tree->
dupl_tree = tree.deep_copy
Test.with_clue "Seed sed to 42" (tree == dupl_tree).should_be_true
main = Test_Suite.run_main spec

View File

@ -44,6 +44,10 @@ spec =
f_2 = File.new "bar/../baz/../foo"
f_2.normalize.should_equal f_1
Test.specify "should handle `==` on files" <|
(File.new "foo").should_equal (File.new "foo")
(File.new "bar").should_not_equal (File.new "foo")
Test.specify "should allow reading a file byte by byte" <|
f = enso_project.data / "short.txt"
f.delete_if_exists