Add Comparator conversion for all types (#4067)

Add `Comparator` type class emulation for all types. Migrate all the types in stdlib to this new `Comparator` API. The main documentation is in `Ordering.enso`.

Fixes these pivotals:
- https://www.pivotaltracker.com/story/show/183945328
- https://www.pivotaltracker.com/story/show/183958734
- https://www.pivotaltracker.com/story/show/184380208

# Important Notes
- The new Comparator API forces users to specify both `equals` and `hash` methods on their custom comparators.
- All the `compare_to` overrides were replaced by definition of a custom _ordered_ comparator.
- All the call sites of `x.compare_to y` method were replaced with `Ordering.compare x y`.
- `Ordering.compare` is essentially a shortcut for `Comparable.from x . compare x y`.
- The default comparator for `Any` is `Default_Unordered_Comparator`, which just forwards to the builtin `EqualsNode` and `HashCodeNode` nodes.
- For `x`, one can get its hash with `Comparable.from x . hash x`.
- This makes `hash` as _hidden_ as possible. There are no other public methods to get a hash code of an object.
- Comparing `x` and `y` can be done either by `Ordering.compare x y` or `Comparable.from x . compare x y` instead of `x.compare_to y`.
This commit is contained in:
Pavel Marek 2023-02-10 10:22:11 +01:00 committed by GitHub
parent 4f70bcc5ff
commit 1f8511dab2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
93 changed files with 2009 additions and 832 deletions

View File

@ -556,6 +556,7 @@
- [Optimize Atom storage layouts][3862]
- [Make instance methods callable like statics for builtin types][4077]
- [Convert large longs to doubles, safely, for host calls][4099]
- [Consistent ordering with comparators](4067)
- [Profile engine startup][4110]
- [Report type of polyglot values][4111]
- [Engine can now recover from serialization failures][5591]
@ -649,6 +650,7 @@
[4056]: https://github.com/enso-org/enso/pull/4056
[4077]: https://github.com/enso-org/enso/pull/4077
[4099]: https://github.com/enso-org/enso/pull/4099
[4067]: https://github.com/enso-org/enso/pull/4067
[4110]: https://github.com/enso-org/enso/pull/4110
[4111]: https://github.com/enso-org/enso/pull/4111
[5591]: https://github.com/enso-org/enso/pull/5591

View File

@ -1,12 +1,16 @@
import project.Data.Ordering.Ordering
import project.Data.Pair.Pair
import project.Data.Range.Extensions
import project.Data.Text.Text
import project.Error.Error
import project.Error.Incomparable_Values.Incomparable_Values
import project.Error.Common.No_Such_Conversion
import project.Error.Common.Type_Error
import project.Nothing.Nothing
import project.Meta
import project.Panic.Panic
from project.Data.Boolean import Boolean, True, False
from project.Data.Ordering import all
## Any is the universal top-type, with all other types being subsumed by it.
@ -101,7 +105,21 @@ type Any
a = 7 * 21
a == 147
== : Any -> Boolean
== self that = @Builtin_Method "Any.=="
== self that =
# If there is No_Such_Conversion, then `self` and `that` are probably
# host or polyglot values, so we just compare them with the default comparator.
eq_self = Panic.catch No_Such_Conversion (Comparable.from self) _-> Default_Unordered_Comparator
eq_that = Panic.catch No_Such_Conversion (Comparable.from that) _-> Default_Unordered_Comparator
if Meta.is_same_object eq_self Incomparable then False else
similar_type = Meta.is_same_object eq_self eq_that
if similar_type.not then False else
case eq_self.is_ordered of
True ->
# Comparable.equals_builtin is a hack how to directly access EqualsNode from the
# engine, so that we don't end up in an infinite recursion here (which would happen
# if we would compare with `eq_self == eq_that`).
Comparable.equals_builtin (eq_self.compare self that) Ordering.Equal
False -> eq_self.equals self that
## ALIAS Inequality
@ -133,12 +151,8 @@ type Any
Arguments:
- that: The value to compare `self` against.
To have `>` defined, a type must define `compare_to`, returning an Ordering.
! Implementing Greater Than
Many types can admit a definition of greater than that is more efficient
than the generic one given here. When implementing this for your own types
please ensure that it is semantically equivalent to using `.compare_to`.
To have `>` properly defined, a type must have an associated ordered comparator.
See `Ordering.enso` for information how comparators work.
> Example
Checking if the variable `a` is greater than `147`.
@ -148,8 +162,12 @@ type Any
example_greater =
a = 7 * 28
a > 147
> : Any -> Boolean
> self that = self.compare_to that == Ordering.Greater
> : Any -> Boolean ! Incomparable_Values
> self that =
assert_ordered_comparators self that <|
case (Comparable.from self).compare self that of
Ordering.Greater -> True
_ -> False
## ALIAS Greater Than or Equal
@ -174,10 +192,13 @@ type Any
example_greater_eq =
a = 6 * 21
a >= 147
>= : Any -> Boolean
>= : Any -> Boolean ! Incomparable_Values
>= self that =
ordering = self.compare_to that
(ordering == Ordering.Greater) || (ordering == Ordering.Equal)
assert_ordered_comparators self that <|
case (Comparable.from self).compare self that of
Ordering.Less -> False
Ordering.Equal -> True
Ordering.Greater -> True
## ALIAS Less Than
@ -186,12 +207,8 @@ type Any
Arguments:
- that: The value to compare `self` against.
To have `<` defined, a type must define `compare_to`, returning an Ordering.
! Implementing Less Than
Many types can admit a definition of less than that is more efficient than
the generic one given here. When implementing this for your own types
please ensure that it is semantically equivalent to using `.compare_to`.
To have `<` properly defined, a type must have an associated ordered comparator.
See `Ordering.enso` for information how comparators work.
> Example
Checking if the variable `a` is less than `147`.
@ -201,8 +218,12 @@ type Any
example_less =
a = 7 * 21
a < 147
< : Any -> Boolean
< self that = self.compare_to that == Ordering.Less
< : Any -> Boolean ! Incomparable_Values
< self that =
assert_ordered_comparators self that <|
case (Comparable.from self).compare self that of
Ordering.Less -> True
_ -> False
## ALIAS Less Than or Equal
@ -227,10 +248,13 @@ type Any
example_less_eq =
a = 7 * 21
a < 147
<= : Any -> Boolean
<= : Any -> Boolean ! Incomparable_Values
<= self that =
ordering = self.compare_to that
(ordering == Ordering.Less) || (ordering == Ordering.Equal)
assert_ordered_comparators self that <|
case (Comparable.from self).compare self that of
Ordering.Less -> True
Ordering.Equal -> True
Ordering.Greater -> False
## Checks if the type is an instance of `Nothing`.
@ -396,3 +420,18 @@ type Any
(+1 >> *2) 2
>> : (Any -> Any) -> (Any -> Any) -> Any -> Any
>> self ~that = x -> that (self x)
## PRIVATE
Checks if both comparators of the given objects are both of same type and ordered.
If they are not of same type, a `Type_Error` is thrown.
If the comparators are either `Incomparable`, or unordered, `False` is returned.
assert_ordered_comparators : Any -> (Any -> Any) -> Any ! (Type_Error | Incomparable_Values)
assert_ordered_comparators this that ~action =
comp_this = Comparable.from this
comp_that = Comparable.from that
if (Meta.is_same_object comp_this comp_that).not then Error.throw (Type_Error.Error (Meta.type_of comp_this) comp_that "comp_that") else
if Meta.is_same_object comp_this Incomparable || comp_this.is_ordered.not then Error.throw Incomparable_Values else
action

View File

@ -146,7 +146,7 @@ type Array
[3,2,1].to_array.sort
sort : (Any -> Any -> Ordering) -> Array
sort self comparator=(_.compare_to _) =
sort self comparator=(Ordering.compare _ _) =
self.sort_builtin comparator
## Identity.

View File

@ -1,5 +1,7 @@
import project.Any.Any
import project.Data.Ordering.Ordering
import project.Data.Ordering.Comparable
import project.Data.Ordering.Default_Ordered_Comparator
import project.Nothing.Nothing
from project.Data.Boolean.Boolean import True, False
@ -55,19 +57,6 @@ type Boolean
not : Boolean
not self = @Builtin_Method "Boolean.not"
## Compares the two operands to determine the ordering of this with
respect to that.
Arguments:
- that: The operand to order this with respect to.
> Example
Computing the ordering of True and False
True.compare_to False
compare_to : Boolean -> Ordering
compare_to self that = @Builtin_Method "Boolean.compare_to"
## The if-then-else control flow operator that executes one of two branches
based on a conditional.
@ -100,3 +89,6 @@ type Boolean
if (27 % 3) == 0 then IO.println "Fizz"
if_then : Any -> Any | Nothing
if_then self ~on_true = @Builtin_Method "Boolean.if_then"
Comparable.from (_:Boolean) = Default_Ordered_Comparator

View File

@ -22,7 +22,8 @@ import project.Warning.Warning
from project.Metadata.Widget import Single_Choice
from project.Metadata.Choice import Option
import project.Metadata.Display
# We need to import conversion methods from Ordering, therefore, we import all
from project.Data.Ordering import all
from project.Data.Boolean import Boolean, True, False
## Methods for serializing from and to JSON.
@ -174,19 +175,6 @@ type JS_Object
to_json : Text
to_json self = self.to_text
## Checks if this JS_Object is equal to another JS_Object.
Arguments:
- that: The map to compare `self` to.
== : JS_Object -> Boolean
== self that = case that of
_ : JS_Object ->
self_keys = self.field_names
that_keys = that.field_names
self_keys.length == that_keys.length && self_keys.all key->
(self.get key == that.at key).catch No_Such_Key.Error _->False
_ -> False
## UNSTABLE
Transform the vector into text for displaying as part of its default
@ -195,6 +183,29 @@ type JS_Object
to_default_visualization_data self =
render self
## PRIVATE
type JS_Object_Comparator
is_ordered : Boolean
is_ordered = False
equals : JS_Object -> JS_Object -> Boolean
equals obj1 obj2 =
obj1_keys = obj1.field_names
obj2_keys = obj2.field_names
obj1_keys.length == obj2_keys.length && obj1_keys.all key->
(obj1.get key == obj2.at key).catch No_Such_Key.Error _->False
hash : JS_Object -> Integer
hash obj =
values_hashes = obj.field_names.map field_name->
val = obj.get field_name
Comparable.from val . hash val
# Return sum, as we don't care about ordering of field names
values_hashes.fold 0 (+)
Comparable.from (_:JS_Object) = JS_Object_Comparator
## PRIVATE
Render the JS_Object to Text with truncated depth.
render object depth=0 max_depth=5 max_length=100 = case object of

View File

@ -1,4 +1,6 @@
import project.Data.Ordering.Ordering
import project.Data.Ordering.Comparable
import project.Data.Ordering.Default_Ordered_Comparator
import project.Data.Text.Text
import project.Data.Locale.Locale
import project.Error.Common.Arithmetic_Error
@ -529,19 +531,6 @@ type Decimal
ceil : Integer
ceil self = @Builtin_Method "Decimal.ceil"
## Compares the two operands to determine the ordering of this with
respect to that.
Arguments:
- that: The operand to order this with respect to.
> Example
Computing the ordering of 1.732 and 4 (Less).
1.732.compare_to 4
compare_to : Number -> Ordering
compare_to self that = @Builtin_Method "Decimal.compare_to"
## Computes the nearest integer below this decimal.
This method provides a means of converting a Decimal to an Integer.
@ -768,19 +757,6 @@ type Integer
ceil : Integer
ceil self = @Builtin_Method "Integer.ceil"
## Compares the two operands to determine the ordering of this with
respect to that.
Arguments:
- that: The operand to order this with respect to.
> Example
Computing the ordering of 1 and 4 (Less).
1.compare_to 4
compare_to : Number -> Ordering
compare_to self that = @Builtin_Method "Integer.compare_to"
## Computes the integer division of this by that.
Arguments:
@ -964,6 +940,8 @@ type Integer
parse_builtin text radix = @Builtin_Method "Integer.parse"
Comparable.from (_:Number) = Default_Ordered_Comparator
## UNSTABLE
A syntax error when parsing a double.

View File

@ -1,14 +1,182 @@
import project.Any.Any
import project.Data.Numbers.Decimal
import project.Data.Numbers.Integer
import project.Data.Numbers.Number
import project.Error.Common.Type_Error
import project.Error.Error
import project.Error.Incomparable_Values.Incomparable_Values
import project.Error.Unimplemented.Unimplemented
import project.Nothing
import project.Meta
import project.Meta.Atom
from project.Data.Boolean import all
## Provides custom ordering, equality check and hash code for types that need it.
The Enso runtime system offers default implementation of _equality_
as well as capability to _compute hash code_ (for use in `Map`) automatically.
The default implementation is sufficient for most of the programming activities.
Especially when defining new type and its constructors, they get sensible
implementation of both functions.
Should there be a need to redefine the default implementation, here is a way:
Define conversion function `Comparable.from` for your `type` and return pointer to
another `type` that satisfies either of the following two definitions:
```
type Ordered_Comparator T
is_ordered = True
compare : T -> T -> Ordering
hash : T -> Integer
type Unordered_Comparator T
is_ordered = False
equals : T -> T -> Boolean
hash : T -> Integer
```
Or `Incomparable` in case that the type `T` should not be compared at all.
Note that there has to be `is_ordered` method defined which returns a Boolean
indicating that the comparator is ordered. This is currently needed as there is
no way to define interfaces in Enso.
An _unordered comparator_ has to implement both `equals` and `hash` to define
a _total_ custom equality. By _total_, we mean that every instance of the type
has to be either equal or not equal, which is represented by the type signature
of `equals` - just `Boolean` is returned without any errors thrown.
An _ordered comparator_ has `compare` method instead of `equals` method, that is
expected to return `Ordering` signaling that an instance of the type is either
less than, equal to, or greater than the other instance. This relation is also
_total_, meaning that all the instances of the type are comparable.
The runtime expects the following semantics for all the comparators:
- Hash consistency:
- If x == y then hash(x) == hash(y)
- If hash(x) != hash(y) then x != y
- Consistency: if x == y then x == y for all the subsequent invocations.
- Symmetry: if x == y then y == x
- Reflexivity: x == x
- Transitivity: if x < y and y < z then x < z
- Antisymmetry (?): if x > y then y < x
Users are responsible for the compliance to the aforementioned semantics.
Should the semantics be violated, an unexpected behavior may be encountered, e.g.,
`Array.sort` may return unexpected results or fail with `Incomparable_Values`.
> Example
Comparator for an unordered Pair `UPair`. In this example, we can see an
implementation of the `hash` method that delegates to hash methods of
fields.
```
type UPair
Value x y
type UPair_Comparator
is_ordered = False
equals pair1 pair2 =
if pair1.x == pair2.x && pair1.y == pair2.y then True else
if pair1.x == pair2.y && pair1.y == pair2.x then True else
False
hash upair =
x_comp = Comparable.from upair.x
y_comp = Comparable.from upair.y
(x_comp.hash upair.x) + (y_comp.hash upair.y)
Comparable.from (_ : UPair) = UPair_Comparator
```
> Example
Representation of _rational numbers_ as a pair of integers needs a
special equality. Here is a way to define it:
```
type Rational
Fraction (numerator:Integer) (denominator:Integer)
Comparable.from (_:Rational) = Rational_Ordering
type Rational_Ordering
is_ordered = True
compare self r1 r2 =
v1 = r1.numerator * r2.denominator
v2 = r2.numerator * r1.denominator
if v1 < v2 then Ordering.Less else
if v1 > v2 then Ordering.Greater else
Ordering.Equal
hash self r1 = 42 # or something better
```
By defining the `Rational_Ordering` and making it available via
`Comparable.from (_:Rational)` method, all parts of the Enso system will use
the custom comparator whenever equality or hash code is needed.
The equality check of two objects:
- verifies both objects share the same type of `comparator`
- consults their `compare`, or `equals` method, based on whether the comparator is
ordered or unordered.
- if the result is `Ordering.Equal` the system also checks that both instances have the same `hash`
- the `hash` code check may be done only from time to time to speed things up
@Builtin_Type
type Comparable
## PRIVATE
Called as a callback from internal engine code for an atom with a custom
comparator. It is assumed that the given atom has a custom comparator, that is
a comparator different than `Default_Unordered_Comparator`.
hash_callback : Atom -> Integer
hash_callback atom = (Comparable.from atom).hash atom
## PRIVATE
A custom comparator is any comparator that is different than the
default ones.
has_custom_comparator : Atom -> Boolean
has_custom_comparator atom =
comp = Comparable.from atom
(comp.is_a Default_Unordered_Comparator).not && (comp.is_a Default_Ordered_Comparator).not
## Singleton denoting that values of certain type are not comparable.
type Incomparable
Singleton
## Default implementation of unordered comparator.
@Builtin_Type
type Default_Unordered_Comparator
is_ordered = False
equals : Any -> Any -> Boolean
equals x y = Comparable.equals_builtin x y
hash : Any -> Integer
hash object = Comparable.hash_builtin object
## Default implementation of an ordered _comparator_. Handles only primitive types,
does not handle atoms, or vectors. Any type that requires an ordering, must
define its own ordered comparator.
@Builtin_Type
type Default_Ordered_Comparator
is_ordered = True
## Handles only primitive types, not atoms or vectors.
compare : Any -> Any -> Ordering
compare x y =
if Comparable.less_than_builtin x y then Ordering.Less else
if Comparable.equals_builtin x y then Ordering.Equal else
if Comparable.less_than_builtin y x then Ordering.Greater
hash : Number -> Integer
hash x = Comparable.hash_builtin x
Comparable.from (_:Any) = Default_Unordered_Comparator
## Types representing the ordering of values.
These are intended to be returned from the `compare_to` function, that has a
type as follows for a type `A`: `A.compare_to : A -> Ordering`.
The result should be returned in terms of how `self` orders in comparison to
`that`. So, if `self` is greater than `that`, you should return `Greater.`
@Builtin_Type
type Ordering
## A representation that the first value orders as less than the second.
@ -20,6 +188,14 @@ type Ordering
## A representation that the first value orders as greater than the second.
Greater
## Compares to values and returns an Ordering
compare : Any -> Any -> Ordering ! (Incomparable_Values | Type_Error)
compare x y =
if x < y then Ordering.Less else
if x == y then Ordering.Equal else
if x > y then Ordering.Greater else
Error.throw Incomparable_Values
## Converts the ordering to the signed notion of ordering based on integers.
> Example
@ -42,11 +218,6 @@ type Ordering
Ordering.Equal -> other
Ordering.Greater -> Ordering.Greater
compare_to : Ordering -> Ordering
compare_to self that = case that of
_ : Ordering -> self.to_sign.compare_to that.to_sign
_ -> Error.throw (Type_Error.Error Ordering that "that")
## Converts a sign-based representation of ordering to Enso's native ordering.
Arguments:
@ -59,3 +230,10 @@ type Ordering
from_sign : Integer -> Ordering
from_sign sign = if sign == 0 then Ordering.Equal else
if sign > 0 then Ordering.Greater else Ordering.Less
type Ordering_Comparator
is_ordered = True
compare x y = (Comparable.from x.to_sign).compare x.to_sign y.to_sign
hash x = x.to_sign
Comparable.from (_:Ordering) = Ordering_Comparator

View File

@ -1,6 +1,7 @@
import project.Any.Any
import project.Data.Ordering.Natural_Order
import project.Data.Ordering.Ordering
import project.Data.Ordering.Comparable
import project.Data.Text.Case_Sensitivity.Case_Sensitivity
import project.Data.Text.Text_Ordering.Text_Ordering
import project.Error.Incomparable_Values.Incomparable_Values
@ -11,18 +12,19 @@ from project.Data.Boolean import True, False
polyglot java import org.enso.base.ObjectComparator
## ADVANCED
Creates a Java Comparator object which can call Enso compare_to
Creates a Java Comparator object which can call back to Enso for comparison
of non-primitive types.
Arguments:
- custom_comparator:
If `Nothing` will get a singleton instance for `.compare_to`.
Otherwise can support a custom fallback comparator.
If `Nothing` will get an ordered comparator from each element.
Otherwise can support a custom fallback compare function.
new : Nothing | (Any -> Any -> Ordering) -> ObjectComparator
new custom_comparator=Nothing =
comparator_to_java cmp x y = Incomparable_Values.handle_errors (cmp x y . to_sign)
case custom_comparator of
Nothing -> ObjectComparator.getInstance (comparator_to_java .compare_to)
Nothing -> ObjectComparator.getInstance (comparator_to_java Ordering.compare)
_ -> ObjectComparator.new (comparator_to_java custom_comparator)
## ADVANCED

View File

@ -23,8 +23,8 @@ polyglot java import com.ibm.icu.text.BreakIterator
compare : Text -> Text -> Case_Sensitivity -> Ordering
compare text1 text2 case_sensitivity=Case_Sensitivity.Sensitive =
compare_text = case case_sensitivity of
Case_Sensitivity.Default -> _.compare_to _
Case_Sensitivity.Sensitive -> _.compare_to _
Case_Sensitivity.Default -> Ordering.compare _ _
Case_Sensitivity.Sensitive -> Ordering.compare _ _
Case_Sensitivity.Insensitive locale -> a -> b -> a.compare_to_ignore_case b locale
iter1 = BreakIterator.getCharacterInstance
@ -60,7 +60,7 @@ compare text1 text2 case_sensitivity=Case_Sensitivity.Sensitive =
[substring, decimal, pair.first, next_index]
## Loop to computer the ordering of text1 and text2.
## Loop to compute the ordering of text1 and text2.
Ordering: Nothing < Number < Text
prev1 - index to start of current character in text1.
next1 - index to start of next character (or -1 if finished) in text1.
@ -98,7 +98,7 @@ compare text1 text2 case_sensitivity=Case_Sensitivity.Sensitive =
num_text2 = parsed2.at 0
value2 = parsed2.at 1
value_comparison = value1.compare_to value2
value_comparison = Ordering.compare value1 value2
if value_comparison != Ordering.Equal then value_comparison else
text_comparison = compare_text num_text1 num_text2
if text_comparison != Ordering.Equal then text_comparison else

View File

@ -9,7 +9,7 @@ from project.Data.Boolean import True, False
Arguments:
- vector1: The first vector to compare.
- vector2: The second vector to compare.
- element_comparator (optional): A custom comparator defining the order
- element_comparator (optional): A custom compare function defining the order
between particular elements. Uses the default ordering by default.
Returns whether `vector1` is less, equal or greater than `vector2` according
@ -30,7 +30,7 @@ from project.Data.Boolean import True, False
Vector_Lexicographic_Order.compare [] [1] == Ordering.Less
Vector_Lexicographic_Order.compare [1] [1] == Ordering.Equal
compare : Vector -> Vector -> (Any -> Any -> Ordering) -> Ordering
compare vector1 vector2 (element_comparator = _.compare_to _) =
compare vector1 vector2 (element_comparator = Ordering.compare _ _) =
is_index_contained_in_both ix = ix<vector1.length && ix<vector2.length
go ix = case is_index_contained_in_both ix of
True ->
@ -48,5 +48,5 @@ compare vector1 vector2 (element_comparator = _.compare_to _) =
## At least one of the vectors ran out of elements. In that case,
the longer Vector is the greater one and if both have the same
length that means they must have been equal.
vector1.length . compare_to vector2.length
Ordering.compare vector1.length vector2.length
go 0

View File

@ -5,6 +5,7 @@ import project.Data.Numbers.Integer
import project.Data.Numbers.Number
import project.Data.Ordering.Comparator
import project.Data.Ordering.Ordering
import project.Data.Ordering.Comparable
import project.Data.Range.Extensions
import project.Data.Vector.Vector
import project.Error.Error
@ -341,12 +342,13 @@ compute_fold counter current value =
counter.increment
if counter.comparatorError.not then
value_comparator = Comparable.from value
if counter.minimum.is_nothing then counter.setMinimum value else
ordering = Incomparable_Values.handle_errors <| value.compare_to counter.minimum
ordering = Incomparable_Values.handle_errors <| value_comparator.compare value counter.minimum
if ordering.is_error then counter.failComparator else
if ordering == Ordering.Less then counter.setMinimum value
if counter.maximum.is_nothing then counter.setMaximum value else
ordering = Incomparable_Values.handle_errors <| value.compare_to counter.maximum
ordering = Incomparable_Values.handle_errors <| value_comparator.compare value counter.maximum
if ordering.is_error then counter.failComparator else
if ordering == Ordering.Greater then counter.setMaximum value

View File

@ -5,6 +5,7 @@ import project.Error.Common.Type_Error
import project.Error.Error
import project.Meta
from project.Data.Ordering import all
from project.Data.Boolean import Boolean, True, False
polyglot java import org.enso.base.Text_Utils
@ -33,23 +34,6 @@ type Text
+ : Text -> Text
+ self that = @Builtin_Method "Text.+"
## Compare two texts to discover their ordering.
Arguments:
- that: The text to order `self` with respect to.
> Example
Checking how "a" orders in relation to "b".
"a".compare_to "b"
compare_to : Text -> Ordering
compare_to self that = case that of
_ : Text ->
is_normalized = self.is_normalized && that.is_normalized
comparison_result = Text_Utils.compare self that is_normalized
Ordering.from_sign comparison_result
_ -> Error.throw (Type_Error.Error Text that "that")
## Checks whether `self` is equal to `that`, ignoring the case of the texts.
Arguments:
@ -134,3 +118,5 @@ type Text
Conversion to Text that overrides the default `to_text` behavior.
to_text : Text
to_text self = self
Comparable.from (_:Text) = Default_Ordered_Comparator

View File

@ -3,6 +3,8 @@ import project.Data.Json.JS_Object
import project.Data.Locale.Locale
import project.Data.Numbers.Integer
import project.Data.Ordering.Ordering
import project.Data.Ordering.Comparable
import project.Data.Ordering.Default_Ordered_Comparator
import project.Data.Text.Text
import project.Data.Time.Date_Period.Date_Period
import project.Data.Time.Date_Time.Date_Time
@ -589,22 +591,6 @@ type Date
format : Text -> Text
format self pattern = Time_Utils.local_date_format self pattern
## Compares `self` to `that` to produce an ordering.
Arguments:
- that: The other `Date` to compare against.
> Example
Compare two dates for their ordering.
(Date.new 2000).compare_to (Date.new 2001)
compare_to : Date -> Ordering
compare_to self that = case that of
_ : Date ->
sign = Time_Utils.compare_to_localdate self that
Ordering.from_sign sign
_ -> Error.throw (Type_Error.Error Date that "that")
## PRIVATE
week_days_between start end =
@ -641,3 +627,6 @@ is_weekend date =
## PRIVATE
fits_in_range start end date =
(start <= date) && (date < end)
Comparable.from (_:Date) = Default_Ordered_Comparator

View File

@ -3,6 +3,8 @@ import project.Data.Json.JS_Object
import project.Data.Locale.Locale
import project.Data.Numbers.Integer
import project.Data.Ordering.Ordering
import project.Data.Ordering.Comparable
import project.Data.Ordering.Default_Ordered_Comparator
import project.Data.Text.Text
import project.Data.Time.Date.Date
import project.Data.Time.Date_Period.Date_Period
@ -686,18 +688,5 @@ type Date_Time
format : Text -> Text
format self pattern = @Builtin_Method "Date_Time.format"
## Compares `self` to `that` to produce an ordering.
Arguments:
- that: The other `Date_Time` to compare against.
> Example
Compare two times for their ordering.
(Date_Time.new 2000).compare_to (Date_Time.new 2001)
compare_to : Date_Time -> Ordering
compare_to self that = case that of
_ : Date_Time ->
sign = Time_Utils.compare_to_zoneddatetime self that
Ordering.from_sign sign
_ -> Error.throw (Type_Error.Error Date_Time that "that")
Comparable.from (_:Date_Time) = Default_Ordered_Comparator

View File

@ -2,7 +2,8 @@ import project.Any.Any
import project.Data.Json.JS_Object
import project.Data.Numbers.Decimal
import project.Data.Numbers.Integer
import project.Data.Ordering.Ordering
import project.Data.Ordering.Comparable
import project.Data.Ordering.Default_Ordered_Comparator
import project.Data.Pair.Pair
import project.Data.Time.Date_Time.Date_Time
import project.Data.Time.Period.Period
@ -167,25 +168,6 @@ type Duration
Panic.catch ArithmeticException (self.minus_builtin that) err->
Error.throw (Time_Error.Error err.payload.getMessage)
## Compares `self` to `that` to produce an ordering.
Arguments:
- that: The other `Duration` to compare against.
> Example
Compare two durations for their ordering.
import Standard.Base.Data.Time.Duration
example_compare_to =
duration_1 = (Duration.new hour=1)
duration_2 = (Duration.new minutes=60) + (Duration.new minutes=5)
duration_1.compare_to duration_2
compare_to : Duration -> Ordering
compare_to self that = case that of
_ : Duration -> Ordering.from_sign (self.compare_to_builtin that)
_ -> Error.throw (Type_Error.Error Duration that "that")
## Get the portion of the duration expressed in nanoseconds.
> Example
@ -312,3 +294,5 @@ type Duration
example_is_empty = Duration.zero.is_empty
is_empty : Boolean
is_empty self = self.to_vector . all (==0)
Comparable.from (_:Duration) = Default_Ordered_Comparator

View File

@ -2,6 +2,8 @@ import project.Any.Any
import project.Data.Numbers.Integer
import project.Data.Time.Date.Date
import project.Data.Time.Duration.Duration
import project.Data.Ordering.Comparable
import project.Data.Ordering.Incomparable
import project.Data.Text.Text
import project.Error.Error
import project.Error.Illegal_Argument.Illegal_Argument
@ -128,11 +130,3 @@ type Period
case err of
DateTimeException -> Error.throw Time_Error.Error "Period subtraction failed"
ArithmeticException -> Error.throw Illegal_Argument.Error "Arithmetic error"
## Just throws `Incomparable_Values`, because periods cannot be
compared without additional context.
To compare two Periods, use something like:
`(start_date + period1) .compare_to (start_date + period2)`
compare_to : Period -> Nothing ! Incomparable_Values
compare_to self _ = Error.throw Incomparable_Values

View File

@ -2,7 +2,8 @@ import project.Any.Any
import project.Data.Json.JS_Object
import project.Data.Locale.Locale
import project.Data.Numbers.Integer
import project.Data.Ordering.Ordering
import project.Data.Ordering.Comparable
import project.Data.Ordering.Default_Ordered_Comparator
import project.Data.Text.Text
import project.Data.Time.Date.Date
import project.Data.Time.Date_Time.Date_Time
@ -357,23 +358,4 @@ type Time_Of_Day
format : Text -> Text
format self pattern = @Builtin_Method "Time_Of_Day.format"
## Compares `self` to `that` to produce an ordering.
Arguments:
- that: The other `Time_Of_Day` to compare against.
> Example
Compare two times for their ordering.
from Standard.Base import Time_Of_Day
example_compare_to =
time_1 = Time_Of_Day.new hour=2 minute=30
time_2 = Time_Of_Day.new minute=50
time_1.compare_to time_2
compare_to : Time_Of_Day -> Ordering
compare_to self that = case that of
_ : 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")
Comparable.from (_:Time_Of_Day) = Default_Ordered_Comparator

View File

@ -845,7 +845,8 @@ type Vector a
elements, returning an Ordering to compare them.
By default, elements are sorted in ascending order, using the comparator
`compare_to`. A custom comparator may be passed to the sort function.
acquired from each element. A custom compare function may be passed to
the sort method.
This is a stable sort, meaning that items that compare the same will not
have their order changed by the sorting process.
@ -877,7 +878,7 @@ type Vector a
[Pair 1 2, Pair -1 8].sort Sort_Direction.Descending (_.first)
sort : Sort_Direction -> (Any -> Any) -> (Any -> Any -> Ordering) -> Vector Any ! Incomparable_Values
sort self (order = Sort_Direction.Ascending) (on = x -> x) (by = (_.compare_to _)) =
sort self (order = Sort_Direction.Ascending) (on = x -> x) (by = (Ordering.compare _ _)) =
comp_ascending l r = by (on l) (on r)
comp_descending l r = by (on r) (on l)
compare = if order == Sort_Direction.Ascending then comp_ascending else

View File

@ -76,6 +76,9 @@ import project.Data.Maybe.Maybe
import project.Data.Noise
import project.Data.Ordering.Natural_Order
import project.Data.Ordering.Ordering
import project.Data.Ordering.Comparable
import project.Data.Ordering.Incomparable
import project.Data.Ordering.Default_Ordered_Comparator
import project.Data.Ordering.Sort_Direction.Sort_Direction
import project.Data.Pair.Pair
import project.Data.Range.Range
@ -127,6 +130,9 @@ export project.Data.Locale.Locale
export project.Data.Maybe.Maybe
export project.Data.Ordering.Natural_Order
export project.Data.Ordering.Ordering
export project.Data.Ordering.Comparable
export project.Data.Ordering.Incomparable
export project.Data.Ordering.Default_Ordered_Comparator
export project.Data.Ordering.Sort_Direction.Sort_Direction
export project.Data.Pair.Pair
export project.Data.Range.Range

View File

@ -289,7 +289,7 @@ type Table
## Returns a new table with the columns sorted by name according to the
specified sort method. By default, sorting will be according to
case-sensitive ascending order based on the `Text.compare_to` operator.
case-sensitive ascending order based on the normalized Unicode ordering.
Arguments:
- order: Whether sorting should be in ascending or descending order.

View File

@ -1172,7 +1172,7 @@ type Column
this rule, ignoring the ascending / descending setting.
- by: function taking two items in this column and returning an
ordering. If specified, it is used instead of the natural
(`.compare_to`) ordering.
ordering of the values.
> Example
Sorting a column in ascending order.
@ -1191,14 +1191,14 @@ type Column
Examples.integer_column.sort Sort_Direction.Descending missing_last=False
> Example
Sorting `column` in ascending order, using a custom comparator
Sorting `column` in ascending order, using a custom comparison
function.
import Standard.Examples
example_sort =
my_comparator a b = a.abs.compare_to b.abs
Examples.decimal_column.sort by=my_comparator
my_compare a b = Ordering.compare a.abs b.abs
Examples.decimal_column.sort by=my_compare
sort : Sort_Direction -> Boolean -> (Any -> Any -> Ordering) | Nothing -> Column
sort self order=Sort_Direction.Ascending missing_last=True by=Nothing =
order_bool = case order of

View File

@ -402,8 +402,7 @@ type Table
## Returns a new table with the columns sorted by name according to the
specified sort method. By default, sorting will be according to
case-sensitive ascending order based on the `compare_to` operator for
`Text`.
case-sensitive ascending order based on the normalized Unicode ordering.
Arguments:
- order: Whether sorting should be in ascending or descending order.

View File

@ -22,8 +22,12 @@ type Bits
Bits.Bits_32 -> 32
Bits.Bits_64 -> 64
## PRIVATE
compare_to self other = self.to_bits . compare_to other.to_bits
type Bits_Comparator
is_ordered = True
compare x y = Comparable.from x.to_bits . compare x.to_bits y.to_bits
hash x = Comparable.from x.to_bits . hash x.to_bits
Comparable.from (_:Bits) = Bits_Comparator
## Represents the different possible types of values within RDBMS columns.
type Value_Type

View File

@ -373,7 +373,7 @@ sort_columns internal_columns order text_ordering =
col -> col.name.to_case_insensitive_key locale=locale
comparator = case text_ordering.sort_digits_as_numbers of
True -> Natural_Order.compare
False -> .compare_to
False -> Ordering.compare
internal_columns.sort order=order on=mapper by=comparator
## PRIVATE

View File

@ -1,7 +1,6 @@
package org.enso.interpreter.epb.runtime;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Cached.Exclusive;
import com.oracle.truffle.api.exception.AbstractTruffleException;
import com.oracle.truffle.api.interop.ArityException;
import com.oracle.truffle.api.interop.ExceptionType;

View File

@ -0,0 +1,104 @@
package org.enso.interpreter.test;
import static org.junit.Assert.assertEquals;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
public class ConversionMethodTests extends TestBase {
private static Context ctx;
@BeforeClass
public static void initCtx() {
ctx = createDefaultContext();
}
@AfterClass
public static void disposeCtx() {
ctx.close();
}
@Test
public void testSimpleConversion() {
String src = """
type Foo
Mk_Foo foo
type Bar
Mk_Bar bar
type Baz
Mk_Baz baz
Foo.from (that:Bar) = Foo.Mk_Foo that.bar
Foo.from (that:Baz) = Foo.Mk_Foo that.baz
main = (Foo.from (Baz.Mk_Baz 10)).foo + (Foo.from (Bar.Mk_Bar 20)).foo
""";
Value res = evalModule(ctx, src);
assertEquals(30, res.asInt());
}
@Test
public void testDispatchOnHostMap() {
String src = """
polyglot java import java.util.Map as Java_Map
import Standard.Base.Data.Map.Map
type Foo
Mk_Foo data
Foo.from (that:Map) = Foo.Mk_Foo that
main =
jmap = Java_Map.of "A" 1 "B" 2 "C" 3
Foo.from jmap . data . size
""";
Value res = evalModule(ctx, src);
assertEquals(3, res.asInt());
}
@Test
public void testDispatchOnJSMap() {
String src = """
import Standard.Base.Data.Map.Map
foreign js js_map = '''
let m = new Map()
m.set("A", 1)
m.set("B", 2)
return m
type Foo
Mk_Foo data
Foo.from (that:Map) = Foo.Mk_Foo that
main =
Foo.from js_map . data . size
""";
Value res = evalModule(ctx, src);
assertEquals(2, res.asInt());
}
@Test
public void testDispatchOnJSDateTime() {
String src = """
import Standard.Base.Data.Time.Date_Time.Date_Time
foreign js js_date year month day hour minute second nanosecond = '''
return new Date(year, month - 1, day, hour, minute, second, nanosecond / 1000000);
type Foo
Mk_Foo data
Foo.from (that:Date_Time) = Foo.Mk_Foo that
main =
Foo.from (js_date 2023 2 7 23 59 0 10) . data . day
""";
Value res = evalModule(ctx, src);
assertEquals(7, res.asInt());
}
}

View File

@ -44,6 +44,8 @@ public class CurriedFunctionBenchmarks {
var benchmarkName = params.getBenchmark().replaceFirst(".*\\.", "");
var code = """
import Standard.Base.Any.Any
avg fn len =
sum acc i = if i == len then acc else
sum (acc + fn i) i+1

View File

@ -71,6 +71,7 @@ public class EqualsBenchmarks {
var benchmarkName = params.getBenchmark().replaceFirst(".*\\.", "");
var codeBuilder = new StringBuilder("""
import Standard.Base.Data.Range.Extensions
import Standard.Base.Any.Any
type Node
C1 f1

View File

@ -48,6 +48,7 @@ public class VectorBenchmarks {
var code = """
import Standard.Base.Data.Vector.Vector
import Standard.Base.Data.Array_Proxy.Array_Proxy
import Standard.Base.Any.Any
avg arr =
sum acc i = if i == arr.length then acc else

View File

@ -7,6 +7,7 @@ class AtomFixtures extends DefaultInterpreterRunner {
val millionElementList = eval(
s"""import Standard.Base.Data.List.List
|import Standard.Base.Any.Any
|
|main =
| generator fn acc i end = if i == end then acc else @Tail_Call generator fn (fn acc i) i+1 end
@ -16,6 +17,7 @@ class AtomFixtures extends DefaultInterpreterRunner {
val generateListCode =
"""import Standard.Base.Data.List.List
|import Standard.Base.Any.Any
|
|main = length ->
| generator = acc -> i -> if i == 0 then acc else @Tail_Call generator (List.Cons i acc) (i - 1)
@ -27,6 +29,7 @@ class AtomFixtures extends DefaultInterpreterRunner {
val generateListQualifiedCode =
"""import Standard.Base.Data.List.List
|import Standard.Base.Any.Any
|
|main = length ->
| generator = acc -> i -> if i == 0 then acc else @Tail_Call generator (List.Cons i acc) (i - 1)

View File

@ -8,6 +8,7 @@ class CallableFixtures extends DefaultInterpreterRunner {
val sumTCOfromCallCode =
"""
|from Standard.Base.Data.Numbers import all
|import Standard.Base.Any.Any
|
|type Foo
|
@ -22,7 +23,8 @@ class CallableFixtures extends DefaultInterpreterRunner {
val sumTCOmethodCallCode =
"""
"""import Standard.Base.Any.Any
|
|summator = acc -> current ->
| if current == 0 then acc else @Tail_Call summator (acc + current) (current - 1)
|
@ -33,7 +35,8 @@ class CallableFixtures extends DefaultInterpreterRunner {
val sumTCOmethodCall = getMain(sumTCOmethodCallCode)
val sumTCOmethodCallWithNamedArgumentsCode =
"""
"""import Standard.Base.Any.Any
|
|summator = acc -> current ->
| if current == 0 then acc else @Tail_Call summator (current = current - 1) (acc = acc + current)
|
@ -45,7 +48,8 @@ class CallableFixtures extends DefaultInterpreterRunner {
getMain(sumTCOmethodCallWithNamedArgumentsCode)
val sumTCOmethodCallWithDefaultedArgumentsCode =
"""
"""import Standard.Base.Any.Any
|
|summator = (acc = 0) -> current ->
| if current == 0 then acc else @Tail_Call summator (current = current - 1) (acc = acc + current)
|

View File

@ -6,7 +6,8 @@ class NamedDefaultedArgumentFixtures extends DefaultInterpreterRunner {
val hundredMillion: Long = 100000000
val sumTCOWithNamedArgumentsCode =
"""
"""import Standard.Base.Any.Any
|
|main = sumTo ->
| summator = acc -> current ->
| if current == 0 then acc else @Tail_Call summator (current = current - 1) (acc = acc + current)
@ -17,7 +18,8 @@ class NamedDefaultedArgumentFixtures extends DefaultInterpreterRunner {
val sumTCOWithNamedArguments = getMain(sumTCOWithNamedArgumentsCode)
val sumTCOWithDefaultedArgumentsCode =
"""
"""import Standard.Base.Any.Any
|
|main = sumTo ->
| summator = (acc = 0) -> current ->
| if current == 0 then acc else @Tail_Call summator (current = current - 1) (acc = acc + current)

View File

@ -9,7 +9,8 @@ class RecursionFixtures extends DefaultInterpreterRunner {
val hundred: Long = 100
val sumTCOCode =
"""
"""import Standard.Base.Any.Any
|
|main = sumTo ->
| summator = acc -> current ->
| if current == 0 then acc else @Tail_Call summator acc+current current-1
@ -20,7 +21,8 @@ class RecursionFixtures extends DefaultInterpreterRunner {
val sumTCO = getMain(sumTCOCode)
val sumTCOFoldLikeCode =
"""
"""import Standard.Base.Any.Any
|
|main = sumTo ->
| summator = acc -> i -> f ->
| if i == 0 then acc else @Tail_Call summator (f acc i) (i - 1) f
@ -30,7 +32,8 @@ class RecursionFixtures extends DefaultInterpreterRunner {
val sumTCOFoldLike = getMain(sumTCOFoldLikeCode)
val sumRecursiveCode =
"""
"""import Standard.Base.Any.Any
|
|main = sumTo ->
| summator = i -> if i == 0 then 0 else i + summator (i - 1)
| res = summator sumTo
@ -39,7 +42,8 @@ class RecursionFixtures extends DefaultInterpreterRunner {
val sumRecursive = getMain(sumRecursiveCode)
val oversaturatedRecursiveCallTCOCode =
"""
"""import Standard.Base.Any.Any
|
|main = sumTo ->
| summator = acc -> i -> f ->
| if i == 0 then acc else @Tail_Call summator (f acc i) (i - 1) f

View File

@ -63,6 +63,13 @@ public abstract class InvokeConversionNode extends BaseNode {
this.invokeFunctionNode.setTailStatus(tailStatus);
}
/**
* @param self A target of the conversion. Should be a {@link Type} on which the {@code from}
* method is defined. If it is not a {@link Type}, "Invalid conversion target" panic is
* thrown.
* @param that Source of the conversion. Can be arbitrary object, including polyglot values.
* @param arguments Additional arguments passed to the conversion function.
*/
public abstract Object execute(
VirtualFrame frame,
State state,
@ -196,6 +203,131 @@ public abstract class InvokeConversionNode extends BaseNode {
}
}
@Specialization(
guards = {
"!typesLib.hasType(that)",
"!typesLib.hasSpecialDispatch(that)",
"!interop.isTime(that)",
"interop.isDate(that)",
})
Object doConvertDate(
VirtualFrame frame,
State state,
UnresolvedConversion conversion,
Object self,
Object that,
Object[] arguments,
@CachedLibrary(limit = "10") InteropLibrary interop,
@CachedLibrary(limit = "10") TypesLibrary typesLib,
@Cached ConversionResolverNode conversionResolverNode) {
Function function =
conversionResolverNode.expectNonNull(
that, extractConstructor(self), EnsoContext.get(this).getBuiltins().date(), conversion);
return invokeFunctionNode.execute(function, frame, state, arguments);
}
@Specialization(
guards = {
"!typesLib.hasType(that)",
"!typesLib.hasSpecialDispatch(that)",
"interop.isTime(that)",
"!interop.isDate(that)",
})
Object doConvertTime(
VirtualFrame frame,
State state,
UnresolvedConversion conversion,
Object self,
Object that,
Object[] arguments,
@CachedLibrary(limit = "10") InteropLibrary interop,
@CachedLibrary(limit = "10") TypesLibrary typesLib,
@Cached ConversionResolverNode conversionResolverNode) {
Function function =
conversionResolverNode.expectNonNull(
that,
extractConstructor(self),
EnsoContext.get(this).getBuiltins().timeOfDay(),
conversion);
return invokeFunctionNode.execute(function, frame, state, arguments);
}
@Specialization(
guards = {
"!typesLib.hasType(that)",
"!typesLib.hasSpecialDispatch(that)",
"interop.isTime(that)",
"interop.isDate(that)",
})
Object doConvertDateTime(
VirtualFrame frame,
State state,
UnresolvedConversion conversion,
Object self,
Object that,
Object[] arguments,
@CachedLibrary(limit = "10") InteropLibrary interop,
@CachedLibrary(limit = "10") TypesLibrary typesLib,
@Cached ConversionResolverNode conversionResolverNode) {
Function function =
conversionResolverNode.expectNonNull(
that,
extractConstructor(self),
EnsoContext.get(this).getBuiltins().dateTime(),
conversion);
return invokeFunctionNode.execute(function, frame, state, arguments);
}
@Specialization(
guards = {
"!typesLib.hasType(that)",
"!typesLib.hasSpecialDispatch(that)",
"interop.isDuration(that)",
})
Object doConvertDuration(
VirtualFrame frame,
State state,
UnresolvedConversion conversion,
Object self,
Object that,
Object[] arguments,
@CachedLibrary(limit = "10") InteropLibrary interop,
@CachedLibrary(limit = "10") TypesLibrary typesLib,
@Cached ConversionResolverNode conversionResolverNode) {
Function function =
conversionResolverNode.expectNonNull(
that,
extractConstructor(self),
EnsoContext.get(this).getBuiltins().duration(),
conversion);
return invokeFunctionNode.execute(function, frame, state, arguments);
}
@Specialization(
guards = {
"!typesLib.hasType(thatMap)",
"!typesLib.hasSpecialDispatch(thatMap)",
"interop.hasHashEntries(thatMap)",
})
Object doConvertMap(
VirtualFrame frame,
State state,
UnresolvedConversion conversion,
Object self,
Object thatMap,
Object[] arguments,
@CachedLibrary(limit = "10") InteropLibrary interop,
@CachedLibrary(limit = "10") TypesLibrary typesLib,
@Cached ConversionResolverNode conversionResolverNode) {
Function function =
conversionResolverNode.expectNonNull(
thatMap,
extractConstructor(self),
EnsoContext.get(this).getBuiltins().map(),
conversion);
return invokeFunctionNode.execute(function, frame, state, arguments);
}
@Specialization(
guards = {
"!methods.hasType(that)",

View File

@ -29,7 +29,7 @@ public abstract class IndirectInvokeFunctionNode extends Node {
* Executes the {@link IndirectInvokeFunctionNode} to apply the function to given arguments.
*
* @param callable the function to call
* @param callerFrame the caller frame to pass to the function
* @param callerFrame the caller frame to pass to the function, may be null.
* @param state the state to pass to the function
* @param arguments the arguments being passed to {@code function}
* @param schema the names and ordering of arguments for this call site

View File

@ -1,38 +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;
import org.enso.interpreter.node.expression.builtin.ordering.Ordering;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.callable.atom.Atom;
import org.enso.interpreter.runtime.error.DataflowError;
@BuiltinMethod(type = "Boolean", name = "compare_to", description = "Comparison for Booleans.")
public abstract class CompareToNode extends Node {
static CompareToNode build() {
return CompareToNodeGen.create();
}
abstract Object execute(Boolean self, Object that);
@Specialization
Atom doBoolean(Boolean self, Boolean that) {
Ordering ordering = EnsoContext.get(this).getBuiltins().ordering();
if (self == that) {
return ordering.newEqual();
} else if (self) {
return ordering.newGreater();
} else {
return ordering.newLess();
}
}
@Fallback
DataflowError doOther(Boolean self, Object that) {
var builtins = EnsoContext.get(this).getBuiltins();
var typeError = builtins.error().makeTypeError(builtins.bool().getType(), that, "that");
return DataflowError.withoutTrace(typeError, this);
}
}

View File

@ -43,7 +43,7 @@ public abstract class PrintlnNode extends Node {
try {
print(ctx.getOut(), strings.asString(message));
} catch (UnsupportedMessageException e) {
throw new IllegalStateException("Impossible. self is guaranteed to be a string");
throw new IllegalStateException("Impossible. `message` is guaranteed to be a string");
}
return ctx.getNothing();
}

View File

@ -1,7 +1,6 @@
package org.enso.interpreter.node.expression.builtin.meta;
import com.ibm.icu.text.Normalizer;
import com.ibm.icu.text.Normalizer2;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Fallback;
@ -19,45 +18,59 @@ 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.node.callable.InvokeCallableNode.ArgumentsExecutionMode;
import org.enso.interpreter.node.callable.InvokeCallableNode.DefaultsExecutionMode;
import org.enso.interpreter.node.callable.dispatch.InvokeFunctionNode;
import org.enso.interpreter.node.expression.builtin.ordering.HasCustomComparatorNode;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.Module;
import org.enso.interpreter.runtime.callable.UnresolvedConversion;
import org.enso.interpreter.runtime.callable.UnresolvedSymbol;
import org.enso.interpreter.runtime.callable.argument.CallArgumentInfo;
import org.enso.interpreter.runtime.callable.atom.Atom;
import org.enso.interpreter.runtime.callable.atom.AtomConstructor;
import org.enso.interpreter.runtime.callable.atom.StructsLibrary;
import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.data.EnsoFile;
import org.enso.interpreter.runtime.data.Type;
import org.enso.interpreter.runtime.data.text.Text;
import org.enso.interpreter.runtime.error.WarningsLibrary;
import org.enso.interpreter.runtime.library.dispatch.TypesLibrary;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
import org.enso.interpreter.runtime.scope.ModuleScope;
import org.enso.interpreter.runtime.state.State;
import org.enso.polyglot.MethodNames;
@BuiltinMethod(
type = "Any",
name = "==",
description = "Implementation of Any.=="
type = "Comparable",
name = "equals_builtin",
description = """
Compares self with other object and returns True iff `self` is exactly the same as
the other object, including all its transitively accessible properties or fields.
Can handle arbitrary objects, including all foreign objects.
Does not throw exceptions.
Note that this is different than `Meta.is_same_object`, which checks whether two
references point to the same object on the heap.
"""
)
@GenerateUncached
public abstract class EqualsAnyNode extends Node {
public abstract class EqualsNode extends Node {
protected static String EQUALS_MEMBER_NAME = MethodNames.Function.EQUALS;
public static EqualsAnyNode build() {
return EqualsAnyNodeGen.create();
public static EqualsNode build() {
return EqualsNodeGen.create();
}
public abstract boolean execute(@AcceptsError Object self, @AcceptsError Object right);
public abstract boolean execute(@AcceptsError Object left, @AcceptsError Object right);
/**
* Primitive values
@ -69,6 +82,11 @@ public abstract class EqualsAnyNode extends Node {
return self == other;
}
@Specialization
boolean equalsBytes(byte self, byte other) {
return self == other;
}
@Specialization
boolean equalsLong(long self, long other) {
return self == other;
@ -133,17 +151,55 @@ public abstract class EqualsAnyNode extends Node {
@Specialization
boolean equalsUnresolvedSymbols(UnresolvedSymbol self, UnresolvedSymbol otherSymbol,
@Cached EqualsAnyNode equalsNode) {
@Cached EqualsNode equalsNode) {
return self.getName().equals(otherSymbol.getName())
&& equalsNode.execute(self.getScope(), otherSymbol.getScope());
}
@Specialization
boolean equalsUnresolvedConversion(UnresolvedConversion selfConversion, UnresolvedConversion otherConversion,
@Cached EqualsAnyNode equalsNode) {
@Cached EqualsNode equalsNode) {
return equalsNode.execute(selfConversion.getScope(), otherConversion.getScope());
}
@Specialization
boolean equalsModuleScopes(ModuleScope selfModuleScope, ModuleScope otherModuleScope,
@Cached EqualsNode equalsNode) {
return equalsNode.execute(selfModuleScope.getModule(), otherModuleScope.getModule());
}
@Specialization
@TruffleBoundary
boolean equalsModules(Module selfModule, Module otherModule,
@Cached EqualsNode equalsNode) {
return equalsNode.execute(selfModule.getName().toString(), otherModule.getName().toString());
}
@Specialization
boolean equalsFiles(EnsoFile selfFile, EnsoFile otherFile,
@CachedLibrary(limit = "5") InteropLibrary interop) {
return equalsStrings(selfFile.getPath(), otherFile.getPath(), interop, interop);
}
/**
* There is no specialization for {@link TypesLibrary#hasType(Object)}, because also
* primitive values would fall into that specialization and it would be too complicated
* to make that specialization disjunctive. So we rather specialize directly for
* {@link Type types}.
*/
@Specialization(guards = {
"typesLib.hasType(selfType)",
"typesLib.hasType(otherType)"
})
boolean equalsTypes(Type selfType, Type otherType,
@Cached EqualsNode equalsNode,
@CachedLibrary(limit = "5") TypesLibrary typesLib) {
return equalsNode.execute(
selfType.getQualifiedName().toString(),
otherType.getQualifiedName().toString()
);
}
/**
* If one of the objects has warnings attached, just treat it as an object without any
* warnings.
@ -154,7 +210,7 @@ public abstract class EqualsAnyNode extends Node {
boolean equalsWithWarnings(Object selfWithWarnings, Object otherWithWarnings,
@CachedLibrary("selfWithWarnings") WarningsLibrary selfWarnLib,
@CachedLibrary("otherWithWarnings") WarningsLibrary otherWarnLib,
@Cached EqualsAnyNode equalsNode
@Cached EqualsNode equalsNode
) {
try {
Object self =
@ -193,23 +249,6 @@ public abstract class EqualsAnyNode extends Node {
return selfInterop.isNull(selfNull) && otherInterop.isNull(otherNull);
}
@Specialization(guards = {
"isHostObject(selfHostObject)",
"isHostObject(otherHostObject)",
})
boolean equalsHostObjects(
Object selfHostObject, Object otherHostObject,
@CachedLibrary(limit = "5") InteropLibrary interop
) {
try {
return interop.asBoolean(
interop.invokeMember(selfHostObject, "equals", otherHostObject)
);
} catch (UnsupportedMessageException | ArityException | UnknownIdentifierException |
UnsupportedTypeException e) {
throw new IllegalStateException(e);
}
}
@Specialization(guards = {
"selfInterop.isBoolean(selfBoolean)",
@ -271,7 +310,8 @@ public abstract class EqualsAnyNode extends Node {
otherInterop.asTime(otherZonedDateTime),
otherInterop.asTimeZone(otherZonedDateTime)
);
return self.isEqual(other);
// We cannot use self.isEqual(other), because that does not include timezone.
return self.compareTo(other) == 0;
} catch (UnsupportedMessageException e) {
throw new IllegalStateException(e);
}
@ -388,12 +428,16 @@ public abstract class EqualsAnyNode extends Node {
@Specialization(guards = {
"selfInterop.hasArrayElements(selfArray)",
"otherInterop.hasArrayElements(otherArray)"
"otherInterop.hasArrayElements(otherArray)",
"!selfInterop.hasHashEntries(selfArray)",
"!otherInterop.hasHashEntries(otherArray)",
}, limit = "3")
boolean equalsArrays(Object selfArray, Object otherArray,
@CachedLibrary("selfArray") InteropLibrary selfInterop,
@CachedLibrary("otherArray") InteropLibrary otherInterop,
@Cached EqualsAnyNode equalsNode
@Cached EqualsNode equalsNode,
@Cached HasCustomComparatorNode hasCustomComparatorNode,
@Cached InvokeAnyEqualsNode invokeAnyEqualsNode
) {
try {
long selfSize = selfInterop.getArraySize(selfArray);
@ -403,7 +447,15 @@ public abstract class EqualsAnyNode extends Node {
for (long i = 0; i < selfSize; i++) {
Object selfElem = selfInterop.readArrayElement(selfArray, i);
Object otherElem = otherInterop.readArrayElement(otherArray, i);
if (!equalsNode.execute(selfElem, otherElem)) {
boolean elemsAreEqual;
if (selfElem instanceof Atom selfAtomElem
&& otherElem instanceof Atom otherAtomElem
&& hasCustomComparatorNode.execute(selfAtomElem)) {
elemsAreEqual = invokeAnyEqualsNode.execute(selfAtomElem, otherAtomElem);
} else {
elemsAreEqual = equalsNode.execute(selfElem, otherElem);
}
if (!elemsAreEqual) {
return false;
}
}
@ -415,13 +467,15 @@ public abstract class EqualsAnyNode extends Node {
@Specialization(guards = {
"selfInterop.hasHashEntries(selfHashMap)",
"otherInterop.hasHashEntries(otherHashMap)"
"otherInterop.hasHashEntries(otherHashMap)",
"!selfInterop.hasArrayElements(selfHashMap)",
"!otherInterop.hasArrayElements(otherHashMap)"
}, limit = "3")
boolean equalsHashMaps(Object selfHashMap, Object otherHashMap,
@CachedLibrary("selfHashMap") InteropLibrary selfInterop,
@CachedLibrary("otherHashMap") InteropLibrary otherInterop,
@CachedLibrary(limit = "5") InteropLibrary entriesInterop,
@Cached EqualsAnyNode equalsNode) {
@Cached EqualsNode equalsNode) {
try {
int selfHashSize = (int) selfInterop.getHashSize(selfHashMap);
int otherHashSize = (int) otherInterop.getHashSize(otherHashMap);
@ -450,6 +504,68 @@ public abstract class EqualsAnyNode extends Node {
}
}
@Specialization(guards = {
"!isAtom(selfObject)",
"!isAtom(otherObject)",
"!isHostObject(selfObject)",
"!isHostObject(otherObject)",
"interop.hasMembers(selfObject)",
"interop.hasMembers(otherObject)",
"!interop.isDate(selfObject)",
"!interop.isDate(otherObject)",
"!interop.isTime(selfObject)",
"!interop.isTime(otherObject)",
// Objects with types are handled in `equalsTypes` specialization, so we have to
// negate the guards of that specialization here - to make the specializations
// disjunctive.
"!typesLib.hasType(selfObject)",
"!typesLib.hasType(otherObject)",
})
boolean equalsInteropObjectWithMembers(Object selfObject, Object otherObject,
@CachedLibrary(limit = "10") InteropLibrary interop,
@CachedLibrary(limit = "5") TypesLibrary typesLib,
@Cached EqualsNode equalsNode) {
try {
Object selfMembers = interop.getMembers(selfObject);
Object otherMembers = interop.getMembers(otherObject);
assert interop.getArraySize(selfMembers) < Integer.MAX_VALUE : "Long array sizes not supported";
int membersSize = (int) interop.getArraySize(selfMembers);
if (interop.getArraySize(otherMembers) != membersSize) {
return false;
}
// Check member names
String[] memberNames = new String[membersSize];
for (int i = 0; i < membersSize; i++) {
String selfMemberName = interop.asString(interop.readArrayElement(selfMembers, i));
String otherMemberName = interop.asString(interop.readArrayElement(otherMembers, i));
if (!equalsNode.execute(selfMemberName, otherMemberName)) {
return false;
}
memberNames[i] = selfMemberName;
}
// Check member values
for (int i = 0; i < membersSize; i++) {
if (interop.isMemberReadable(selfObject, memberNames[i]) &&
interop.isMemberReadable(otherObject, memberNames[i])) {
Object selfMember = interop.readMember(selfObject, memberNames[i]);
Object otherMember = interop.readMember(otherObject, memberNames[i]);
if (!equalsNode.execute(selfMember, otherMember)) {
return false;
}
}
}
return true;
} catch (UnsupportedMessageException | InvalidArrayIndexException | UnknownIdentifierException e) {
throw new IllegalStateException(
String.format("One of the interop objects has probably wrongly specified interop API "
+ "for members. selfObject = %s ; otherObject = %s", selfObject, otherObject),
e
);
}
}
/** Equals for Atoms and AtomConstructors */
@Specialization
@ -458,13 +574,13 @@ public abstract class EqualsAnyNode extends Node {
}
/**
* How many {@link EqualsAnyNode} should be created for fields in specialization for atoms.
* How many {@link EqualsNode} 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());
static EqualsNode[] createEqualsNodes(int size) {
EqualsNode[] nodes = new EqualsNode[size];
Arrays.fill(nodes, EqualsNode.build());
return nodes;
}
@ -473,17 +589,14 @@ public abstract class EqualsAnyNode extends Node {
Atom self,
Atom other,
@Cached LoopConditionProfile loopProfile,
@Cached(value = "createEqualsNodes(equalsNodeCountForFields)", allowUncached = true) EqualsAnyNode[] fieldEqualsNodes,
@Cached InvokeEqualsNode atomInvokeEqualsNode,
@Cached(value = "createEqualsNodes(equalsNodeCountForFields)", allowUncached = true) EqualsNode[] fieldEqualsNodes,
@Cached ConditionProfile enoughEqualNodesForFieldsProfile,
@Cached ConditionProfile constructorsNotEqualProfile,
@CachedLibrary(limit = "3") StructsLibrary selfStructs,
@CachedLibrary(limit = "3") StructsLibrary otherStructs
@CachedLibrary(limit = "3") StructsLibrary otherStructs,
@Cached HasCustomComparatorNode hasCustomComparatorNode,
@Cached InvokeAnyEqualsNode invokeAnyEqualsNode
) {
if (atomOverridesEquals(self)) {
return atomInvokeEqualsNode.execute(self, other);
}
if (constructorsNotEqualProfile.profile(
self.getConstructor() != other.getConstructor()
)) {
@ -497,10 +610,19 @@ public abstract class EqualsAnyNode extends Node {
if (enoughEqualNodesForFieldsProfile.profile(fieldsSize <= equalsNodeCountForFields)) {
loopProfile.profileCounted(fieldsSize);
for (int i = 0; loopProfile.inject(i < fieldsSize); i++) {
if (!fieldEqualsNodes[i].execute(
selfFields[i],
otherFields[i]
)) {
boolean fieldsAreEqual;
// We don't check whether `other` has the same type of comparator, that is checked in
// `Any.==` that we invoke here anyway.
if (selfFields[i] instanceof Atom selfAtomField
&& otherFields[i] instanceof Atom otherAtomField
&& hasCustomComparatorNode.execute(selfAtomField)) {
// If selfFields[i] has a custom comparator, we delegate to `Any.==` that deals with
// custom comparators. EqualsNode cannot deal with custom comparators.
fieldsAreEqual = invokeAnyEqualsNode.execute(selfAtomField, otherAtomField);
} else {
fieldsAreEqual = fieldEqualsNodes[i].execute(selfFields[i], otherFields[i]);
}
if (!fieldsAreEqual) {
return false;
}
}
@ -517,10 +639,10 @@ public abstract class EqualsAnyNode extends Node {
boolean areFieldsSame;
if (selfFields[i] instanceof Atom selfFieldAtom
&& otherFields[i] instanceof Atom otherFieldAtom
&& atomOverridesEquals(selfFieldAtom)) {
areFieldsSame = InvokeEqualsNode.getUncached().execute(selfFieldAtom, otherFieldAtom);
&& HasCustomComparatorNode.getUncached().execute(selfFieldAtom)) {
areFieldsSame = InvokeAnyEqualsNode.getUncached().execute(selfFieldAtom, otherFieldAtom);
} else {
areFieldsSame = EqualsAnyNodeGen.getUncached().execute(selfFields[i], otherFields[i]);
areFieldsSame = EqualsNodeGen.getUncached().execute(selfFields[i], otherFields[i]);
}
if (!areFieldsSame) {
return false;
@ -529,102 +651,119 @@ public abstract class EqualsAnyNode extends Node {
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}
@Specialization(guards = {
"isHostObject(selfHostObject)",
"isHostObject(otherHostObject)"
})
boolean equalsHostObjects(
Object selfHostObject, Object otherHostObject,
@CachedLibrary(limit = "5") InteropLibrary interop
) {
try {
return interop.asBoolean(
interop.invokeMember(selfHostObject, "equals", otherHostObject)
);
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;
} catch (UnsupportedMessageException | ArityException | UnknownIdentifierException |
UnsupportedTypeException e) {
throw new IllegalStateException(e);
}
}
/**
* 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;
}
// HostFunction is identified by a qualified name, it is not a lambda.
// It has well-defined equality based on the qualified name.
@Specialization(guards = {
"isHostFunction(selfHostFunc)",
"isHostFunction(otherHostFunc)"
})
boolean equalsHostFunctions(Object selfHostFunc, Object otherHostFunc,
@CachedLibrary(limit = "5") InteropLibrary interop,
@Cached EqualsNode equalsNode) {
Object selfFuncStrRepr = interop.toDisplayString(selfHostFunc);
Object otherFuncStrRepr = interop.toDisplayString(otherHostFunc);
return equalsNode.execute(selfFuncStrRepr, otherFuncStrRepr);
}
@Fallback
@TruffleBoundary
boolean equalsGeneric(Object left, Object right,
@CachedLibrary(limit = "5") InteropLibrary interop) {
@CachedLibrary(limit = "5") InteropLibrary interop,
@CachedLibrary(limit = "5") TypesLibrary typesLib) {
return left == right
|| interop.isIdentical(left, right, interop)
|| left.equals(right);
|| left.equals(right)
|| (isNullOrNothing(left, typesLib, interop) && isNullOrNothing(right, typesLib, interop));
}
private boolean isNullOrNothing(Object object, TypesLibrary typesLib, InteropLibrary interop) {
if (typesLib.hasType(object)) {
return typesLib.getType(object) == EnsoContext.get(this).getNothing();
} else if (interop.isNull(object)) {
return true;
} else {
return object == null;
}
}
static boolean isAtom(Object object) {
return object instanceof Atom;
}
@TruffleBoundary
boolean isHostObject(Object object) {
return EnsoContext.get(this).getEnvironment().isHostObject(object);
}
@TruffleBoundary
boolean isHostFunction(Object object) {
return EnsoContext.get(this).getEnvironment().isHostFunction(object);
}
/**
* Helper node for invoking `Any.==` method.
*/
@GenerateUncached
static abstract class InvokeAnyEqualsNode extends Node {
static InvokeAnyEqualsNode getUncached() {
return EqualsNodeGen.InvokeAnyEqualsNodeGen.getUncached();
}
abstract boolean execute(Atom selfAtom, Atom otherAtom);
@Specialization
boolean invokeEqualsCachedAtomCtor(Atom selfAtom, Atom thatAtom,
@Cached(value = "getAnyEqualsMethod()", allowUncached = true) Function anyEqualsFunc,
@Cached(value = "buildInvokeFuncNodeForAnyEquals()", allowUncached = true) InvokeFunctionNode invokeAnyEqualsNode,
@CachedLibrary(limit = "3") InteropLibrary interop) {
// TODO: Shouldn't Comparable type be the very first argument? (synthetic self)?
Object ret = invokeAnyEqualsNode.execute(
anyEqualsFunc,
null,
State.create(EnsoContext.get(this)),
new Object[]{selfAtom, thatAtom}
);
try {
return interop.asBoolean(ret);
} catch (UnsupportedMessageException e) {
throw new IllegalStateException("Return value from Any== should be Boolean", e);
}
}
@TruffleBoundary
Function getAnyEqualsMethod() {
var anyType = EnsoContext.get(this).getBuiltins().any();
Function anyEqualsFunc =
anyType.getDefinitionScope().getMethods().get(anyType).get("==");
assert anyEqualsFunc != null : "Any.== method must exist";
return anyEqualsFunc;
}
InvokeFunctionNode buildInvokeFuncNodeForAnyEquals() {
return InvokeFunctionNode.build(
new CallArgumentInfo[]{new CallArgumentInfo("self"), new CallArgumentInfo("that")},
DefaultsExecutionMode.EXECUTE,
ArgumentsExecutionMode.EXECUTE
);
}
}
}

View File

@ -1,5 +1,6 @@
package org.enso.interpreter.node.expression.builtin.meta;
import com.google.common.base.Objects;
import com.ibm.icu.text.Normalizer2;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Cached;
@ -23,14 +24,24 @@ import java.time.ZonedDateTime;
import java.util.Arrays;
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.expression.builtin.ordering.HasCustomComparatorNode;
import org.enso.interpreter.node.expression.builtin.ordering.HashCallbackNode;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.Module;
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.atom.StructsLibrary;
import org.enso.interpreter.runtime.data.EnsoFile;
import org.enso.interpreter.runtime.data.Type;
import org.enso.interpreter.runtime.data.text.Text;
import org.enso.interpreter.runtime.error.WarningsLibrary;
import org.enso.interpreter.runtime.library.dispatch.TypesLibrary;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
import org.enso.interpreter.runtime.scope.ModuleScope;
/**
* Implements {@code hash_code} functionality.
@ -40,7 +51,7 @@ import org.enso.interpreter.runtime.number.EnsoBigInteger;
* <h3>Hashing contract:</h3>
*
* <ul>
* <li>Whenever two objects are equal ({@code EqualsAnyNode} returns {@code true}), their hashcode
* <li>Whenever two objects are equal ({@code EqualsNode} returns {@code true}), their hashcode
* should equal. More formally: {@code For all objects o1, o2: if o1 == o2 then hash(o1) ==
* hash(o2)}
* <li>Whenever two hash codes are different, their associated objects are different: {@code For all objects
@ -48,13 +59,20 @@ import org.enso.interpreter.runtime.number.EnsoBigInteger;
* </ul>
*/
@GenerateUncached
public abstract class HashCodeAnyNode extends Node {
@BuiltinMethod(
type = "Comparable",
name = "hash_builtin",
description = """
Returns hash code of this atom. Use only for overriding default Comparator.
"""
)
public abstract class HashCodeNode extends Node {
public static HashCodeAnyNode build() {
return HashCodeAnyNodeGen.create();
public static HashCodeNode build() {
return HashCodeNodeGen.create();
}
public abstract long execute(@AcceptsError Object self);
public abstract long execute(@AcceptsError Object object);
/** Specializations for primitive values * */
@Specialization
@ -69,12 +87,13 @@ public abstract class HashCodeAnyNode extends Node {
@Specialization
long hashCodeForLong(long l) {
return Long.hashCode(l);
// By casting long to double, we lose some precision on purpose
return hashCodeForDouble((double) l);
}
@Specialization
long hashCodeForInt(int i) {
return i;
return hashCodeForLong(i);
}
@Specialization
@ -83,20 +102,20 @@ public abstract class HashCodeAnyNode extends Node {
}
@Specialization
@TruffleBoundary
long hashCodeForDouble(double d) {
if (d % 1.0 != 0.0) {
if (d % 1.0 != 0 || BigIntegerOps.fitsInLong(d)) {
return Double.hashCode(d);
} else {
if (BigIntegerOps.fitsInLong(d)) {
return hashCodeForLong(Double.valueOf(d).longValue());
} else {
try {
return BigDecimal.valueOf(d).toBigIntegerExact().hashCode();
} catch (ArithmeticException e) {
throw new IllegalStateException(e);
}
}
return bigDoubleHash(d);
}
}
@TruffleBoundary
private static long bigDoubleHash(double d) {
try {
return BigDecimal.valueOf(d).toBigIntegerExact().hashCode();
} catch (ArithmeticException e) {
throw new IllegalStateException(e);
}
}
@ -111,12 +130,63 @@ public abstract class HashCodeAnyNode extends Node {
return System.identityHashCode(atomConstructor);
}
/** How many {@link HashCodeAnyNode} nodes should be created for fields in atoms. */
@Specialization
@TruffleBoundary
long hashCodeForUnresolvedSymbol(UnresolvedSymbol unresolvedSymbol,
@Cached HashCodeNode hashCodeNode) {
long nameHash = hashCodeNode.execute(unresolvedSymbol.getName());
long scopeHash = hashCodeNode.execute(unresolvedSymbol.getScope());
return Objects.hashCode(nameHash, scopeHash);
}
@Specialization
long hashCodeForUnresolvedConversion(UnresolvedConversion unresolvedConversion,
@CachedLibrary(limit = "5") InteropLibrary interop) {
return hashCodeForModuleScope(unresolvedConversion.getScope(), interop);
}
@Specialization
long hashCodeForModuleScope(ModuleScope moduleScope,
@CachedLibrary(limit = "5") InteropLibrary interop) {
return hashCodeForModule(moduleScope.getModule(), interop);
}
@Specialization
@TruffleBoundary
long hashCodeForModule(Module module,
@CachedLibrary(limit = "5") InteropLibrary interop) {
return hashCodeForString(module.toString(), interop);
}
@Specialization
long hashCodeForFile(EnsoFile file,
@CachedLibrary(limit = "5") InteropLibrary interop) {
return hashCodeForString(file.getPath(), interop);
}
/**
* There is no specialization for {@link TypesLibrary#hasType(Object)}, because also
* primitive values would fall into that specialization and it would be too complicated
* to make that specialization disjunctive. So we rather specialize directly for
* {@link Type}.
*/
@Specialization
long hashCodeForType(Type type,
@Cached HashCodeNode hashCodeNode) {
if (EnsoContext.get(this).getNothing() == type) {
// Nothing should be equal to `null`
return 0;
} else {
return hashCodeNode.execute(type.getQualifiedName().toString());
}
}
/** How many {@link HashCodeNode} nodes should be created for fields in atoms. */
static final int hashCodeNodeCountForFields = 10;
static HashCodeAnyNode[] createHashCodeNodes(int size) {
HashCodeAnyNode[] nodes = new HashCodeAnyNode[size];
Arrays.fill(nodes, HashCodeAnyNode.build());
static HashCodeNode[] createHashCodeNodes(int size) {
HashCodeNode[] nodes = new HashCodeNode[size];
Arrays.fill(nodes, HashCodeNode.build());
return nodes;
}
@ -124,16 +194,17 @@ public abstract class HashCodeAnyNode extends Node {
long hashCodeForAtom(
Atom atom,
@Cached(value = "createHashCodeNodes(hashCodeNodeCountForFields)", allowUncached = true)
HashCodeAnyNode[] fieldHashCodeNodes,
HashCodeNode[] fieldHashCodeNodes,
@Cached ConditionProfile isHashCodeCached,
@Cached ConditionProfile enoughHashCodeNodesForFields,
@Cached LoopConditionProfile loopProfile,
@CachedLibrary(limit = "10") StructsLibrary structs) {
@CachedLibrary(limit = "10") StructsLibrary structs,
@Cached HasCustomComparatorNode hasCustomComparatorNode,
@Cached HashCallbackNode hashCallbackNode) {
if (isHashCodeCached.profile(atom.getHashCode() != null)) {
return atom.getHashCode();
}
// TODO[PM]: If atom overrides hash_code, call that method (Will be done in a follow-up PR for
// https://www.pivotaltracker.com/story/show/183945328)
Object[] fields = structs.getFields(atom);
int fieldsCount = fields.length;
@ -142,7 +213,11 @@ public abstract class HashCodeAnyNode extends Node {
if (enoughHashCodeNodesForFields.profile(fieldsCount <= hashCodeNodeCountForFields)) {
loopProfile.profileCounted(fieldsCount);
for (int i = 0; loopProfile.inject(i < fieldsCount); i++) {
hashes[i] = (int) fieldHashCodeNodes[i].execute(fields[i]);
if (fields[i] instanceof Atom atomField && hasCustomComparatorNode.execute(atomField)) {
hashes[i] = (int) hashCallbackNode.execute(atomField);
} else {
hashes[i] = (int) fieldHashCodeNodes[i].execute(fields[i]);
}
}
} else {
hashCodeForAtomFieldsUncached(fields, hashes);
@ -159,7 +234,12 @@ public abstract class HashCodeAnyNode extends Node {
@TruffleBoundary
private void hashCodeForAtomFieldsUncached(Object[] fields, int[] fieldHashes) {
for (int i = 0; i < fields.length; i++) {
fieldHashes[i] = (int) HashCodeAnyNodeGen.getUncached().execute(fields[i]);
if (fields[i] instanceof Atom atomField
&& HasCustomComparatorNode.getUncached().execute(atomField)) {
fieldHashes[i] = (int) HashCallbackNode.getUncached().execute(atomField);
} else {
fieldHashes[i] = (int) HashCodeNodeGen.getUncached().execute(fields[i]);
}
}
}
@ -169,7 +249,7 @@ public abstract class HashCodeAnyNode extends Node {
long hashCodeForWarning(
Object selfWithWarning,
@CachedLibrary("selfWithWarning") WarningsLibrary warnLib,
@Cached HashCodeAnyNode hashCodeNode) {
@Cached HashCodeNode hashCodeNode) {
try {
return hashCodeNode.execute(warnLib.removeWarnings(selfWithWarning));
} catch (UnsupportedMessageException e) {
@ -318,20 +398,30 @@ public abstract class HashCodeAnyNode extends Node {
}
@Specialization(
guards = {"interop.hasArrayElements(selfArray)"},
guards = {
"interop.hasArrayElements(selfArray)",
"!interop.hasHashEntries(selfArray)"
},
limit = "3")
long hashCodeForArray(
Object selfArray,
@CachedLibrary("selfArray") InteropLibrary interop,
@Cached HashCodeAnyNode hashCodeNode,
@Cached("createCountingProfile()") LoopConditionProfile loopProfile) {
@Cached HashCodeNode hashCodeNode,
@Cached("createCountingProfile()") LoopConditionProfile loopProfile,
@Cached HashCallbackNode hashCallbackNode,
@Cached HasCustomComparatorNode hasCustomComparatorNode) {
try {
long arraySize = interop.getArraySize(selfArray);
loopProfile.profileCounted(arraySize);
int[] elemHashCodes = new int[(int) arraySize];
for (int i = 0; loopProfile.inject(i < arraySize); i++) {
if (interop.isArrayElementReadable(selfArray, i)) {
elemHashCodes[i] = (int) hashCodeNode.execute(interop.readArrayElement(selfArray, i));
Object elem = interop.readArrayElement(selfArray, i);
if (elem instanceof Atom atomElem && hasCustomComparatorNode.execute(atomElem)) {
elemHashCodes[i] = (int) hashCallbackNode.execute(atomElem);
} else {
elemHashCodes[i] = (int) hashCodeNode.execute(elem);
}
}
}
return Arrays.hashCode(elemHashCodes);
@ -348,7 +438,7 @@ public abstract class HashCodeAnyNode extends Node {
long hashCodeForMap(
Object selfMap,
@CachedLibrary(limit = "5") InteropLibrary interop,
@Cached HashCodeAnyNode hashCodeNode) {
@Cached HashCodeNode hashCodeNode) {
int mapSize;
long keysHashCode = 0;
long valuesHashCode = 0;
@ -369,6 +459,47 @@ public abstract class HashCodeAnyNode extends Node {
return Arrays.hashCode(new long[] {keysHashCode, valuesHashCode, mapSize});
}
@Specialization(guards = {
"!isAtom(objectWithMembers)",
"!isHostObject(objectWithMembers)",
"interop.hasMembers(objectWithMembers)",
"!interop.hasArrayElements(objectWithMembers)",
"!interop.isTime(objectWithMembers)",
"!interop.isDate(objectWithMembers)",
"!interop.isTimeZone(objectWithMembers)",
"!typesLib.hasType(objectWithMembers)",
})
long hashCodeForInteropObjectWithMembers(Object objectWithMembers,
@CachedLibrary(limit = "10") InteropLibrary interop,
@CachedLibrary(limit = "5") TypesLibrary typesLib,
@Cached HashCodeNode hashCodeNode) {
try {
Object members = interop.getMembers(objectWithMembers);
assert interop.getArraySize(members) < Integer.MAX_VALUE : "long array size not supported";
int size = (int) interop.getArraySize(members);
// Final hash code will be put together from member names and member values.
long[] hashCodes = new long[size * 2];
int hashCodesIdx = 0;
for (int i = 0; i < size; i++) {
String memberName = interop.asString(interop.readArrayElement(members, i));
hashCodes[hashCodesIdx++] = hashCodeNode.execute(memberName);
if (interop.isMemberReadable(objectWithMembers, memberName)) {
Object member = interop.readMember(objectWithMembers, memberName);
hashCodes[hashCodesIdx++] = hashCodeNode.execute(member);
} else {
hashCodes[hashCodesIdx++] = 0;
}
}
return Arrays.hashCode(hashCodes);
} catch (UnsupportedMessageException | InvalidArrayIndexException | UnknownIdentifierException e) {
throw new IllegalStateException(
String.format("An interop object (%s) has probably wrongly specified interop API"
+ " for members.", objectWithMembers),
e
);
}
}
@Specialization(
guards = {"interop.isNull(selfNull)"},
limit = "3")
@ -391,8 +522,29 @@ public abstract class HashCodeAnyNode extends Node {
}
}
/**
* Every host function has a unique fully qualified name, it is not a lambda.
* We get the hashcode from the qualified name.
*/
@TruffleBoundary
@Specialization(guards = "isHostFunction(hostFunction)")
long hashCodeForHostFunction(Object hostFunction,
@CachedLibrary(limit = "3") InteropLibrary interop,
@Cached HashCodeNode hashCodeNode) {
return hashCodeNode.execute(interop.toDisplayString(hostFunction));
}
static boolean isAtom(Object object) {
return object instanceof Atom;
}
@TruffleBoundary
boolean isHostObject(Object object) {
return EnsoContext.get(this).getEnvironment().isHostObject(object);
}
@TruffleBoundary
boolean isHostFunction(Object object) {
return EnsoContext.get(this).getEnvironment().isHostFunction(object);
}
}

View File

@ -1,51 +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.node.expression.builtin.ordering.Ordering;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.callable.atom.Atom;
import org.enso.interpreter.runtime.error.DataflowError;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
@BuiltinMethod(
type = "Big_Integer",
name = "compare_to",
description = "Comparison for big integers.")
public abstract class CompareToNode extends Node {
static CompareToNode build() {
return CompareToNodeGen.create();
}
abstract Object execute(EnsoBigInteger self, Object that);
@Specialization
Atom doLong(EnsoBigInteger self, long that) {
return getOrdering().fromJava(BigIntegerOps.compareTo(self.getValue(), that));
}
@Specialization
Atom doBigInt(EnsoBigInteger self, EnsoBigInteger that) {
return getOrdering().fromJava(BigIntegerOps.compareTo(self.getValue(), that.getValue()));
}
@Specialization
Atom doDecimal(EnsoBigInteger self, double that) {
return getOrdering().fromJava(BigIntegerOps.compareTo(self.getValue(), that));
}
@Fallback
DataflowError doOther(EnsoBigInteger self, Object that) {
var builtins = EnsoContext.get(this).getBuiltins();
var typeError = builtins.error().makeTypeError(builtins.number().getNumber(), that, "that");
return DataflowError.withoutTrace(typeError, this);
}
Ordering getOrdering() {
return EnsoContext.get(this).getBuiltins().ordering();
}
}

View File

@ -1,60 +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.node.expression.builtin.ordering.Ordering;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.callable.atom.Atom;
import org.enso.interpreter.runtime.error.DataflowError;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
@BuiltinMethod(type = "Decimal", name = "compare_to", description = "Comparison for decimals.")
public abstract class CompareToNode extends Node {
static CompareToNode build() {
return CompareToNodeGen.create();
}
abstract Object execute(double self, Object that);
@Specialization
Atom doLong(double self, long that) {
if (self == that) {
return getOrdering().newEqual();
} else if (self > that) {
return getOrdering().newGreater();
} else {
return getOrdering().newLess();
}
}
@Specialization
Atom doBigInt(double self, EnsoBigInteger that) {
return getOrdering().fromJava(BigIntegerOps.compareTo(self, that.getValue()));
}
@Specialization
Atom doDecimal(double self, double that) {
if (self == that) {
return getOrdering().newEqual();
} else if (self > that) {
return getOrdering().newGreater();
} else {
return getOrdering().newLess();
}
}
@Fallback
DataflowError doOther(double self, Object that) {
var builtins = EnsoContext.get(this).getBuiltins();
var typeError = builtins.error().makeTypeError(builtins.number().getNumber(), that, "that");
return DataflowError.withoutTrace(typeError, this);
}
Ordering getOrdering() {
return EnsoContext.get(this).getBuiltins().ordering();
}
}

View File

@ -1,63 +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;
import org.enso.interpreter.node.expression.builtin.number.utils.BigIntegerOps;
import org.enso.interpreter.node.expression.builtin.ordering.Ordering;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.callable.atom.Atom;
import org.enso.interpreter.runtime.error.DataflowError;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
@BuiltinMethod(
type = "Small_Integer",
name = "compare_to",
description = "Comparison for small integers.")
public abstract class CompareToNode extends Node {
static CompareToNode build() {
return CompareToNodeGen.create();
}
abstract Object execute(long self, Object that);
@Specialization
Atom doLong(long self, long that) {
if (self == that) {
return getOrdering().newEqual();
} else if (self > that) {
return getOrdering().newGreater();
} else {
return getOrdering().newLess();
}
}
@Specialization
Atom doBigInt(long self, EnsoBigInteger that) {
return getOrdering().fromJava(BigIntegerOps.compareTo(self, that.getValue()));
}
@Specialization
Atom doDecimal(long self, double that) {
if (self == that) {
return getOrdering().newEqual();
} else if (self > that) {
return getOrdering().newGreater();
} else {
return getOrdering().newLess();
}
}
@Fallback
DataflowError doOther(long self, Object that) {
var builtins = EnsoContext.get(this).getBuiltins();
var typeError = builtins.error().makeTypeError(builtins.number().getNumber(), that, "that");
return DataflowError.withoutTrace(typeError, this);
}
Ordering getOrdering() {
return EnsoContext.get(this).getBuiltins().ordering();
}
}

View File

@ -0,0 +1,8 @@
package org.enso.interpreter.node.expression.builtin.ordering;
import org.enso.interpreter.dsl.BuiltinType;
import org.enso.interpreter.node.expression.builtin.Builtin;
/** A hidden builtin. Only conversions with target type of Comparable are visible. */
@BuiltinType
public class Comparable extends Builtin {}

View File

@ -0,0 +1,7 @@
package org.enso.interpreter.node.expression.builtin.ordering;
import org.enso.interpreter.dsl.BuiltinType;
import org.enso.interpreter.node.expression.builtin.Builtin;
@BuiltinType
public class DefaultOrderedComparator extends Builtin {}

View File

@ -0,0 +1,7 @@
package org.enso.interpreter.node.expression.builtin.ordering;
import org.enso.interpreter.dsl.BuiltinType;
import org.enso.interpreter.node.expression.builtin.Builtin;
@BuiltinType
public class DefaultUnorderedComparator extends Builtin {}

View File

@ -0,0 +1,86 @@
package org.enso.interpreter.node.expression.builtin.ordering;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.GenerateUncached;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.interop.InteropLibrary;
import com.oracle.truffle.api.interop.UnsupportedMessageException;
import com.oracle.truffle.api.library.CachedLibrary;
import com.oracle.truffle.api.nodes.Node;
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.callable.argument.CallArgumentInfo;
import org.enso.interpreter.runtime.callable.atom.Atom;
import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.state.State;
/**
* Helper node for invocation of {@code Comparable.has_custom_comparator atom}. Note that emulating
* the semantics of that function in Java code would be too complicated, so we rather implemented it
* in Enso and just call it from this node.
*/
@GenerateUncached
public abstract class HasCustomComparatorNode extends Node {
public static HasCustomComparatorNode getUncached() {
return HasCustomComparatorNodeGen.getUncached();
}
/**
* Returns true if the given atom has a custom comparator, that is a comparator that is different
* than the default (internal) ones. The default comparators are {@code
* Default_Unordered_Comparator} and {@code Default_Ordered_Comparator}.
*
* @param atom Atom for which we check whether it has custom comparator
* @return true iff the given atom has a custom comparator
*/
public abstract boolean execute(Atom atom);
@Specialization
boolean hasCustomComparatorCached(
Atom atom,
@Cached(value = "getHasCustomComparatorFunction()", allowUncached = true)
Function hasCustomComparatorFunc,
@Cached(value = "buildInvokeNodeWithAtomArgument()", allowUncached = true)
InvokeFunctionNode hasCustomComparatorInvokeNode,
@CachedLibrary(limit = "5") InteropLibrary interop) {
var ctx = EnsoContext.get(this);
var comparableType = ctx.getBuiltins().comparable().getType();
Object res =
hasCustomComparatorInvokeNode.execute(
hasCustomComparatorFunc, null, State.create(ctx), new Object[] {comparableType, atom});
assert interop.isBoolean(res);
try {
return interop.asBoolean(res);
} catch (UnsupportedMessageException e) {
throw new IllegalStateException(
"Return type from Comparable.has_custom_comparator should be Boolean", e);
}
}
/**
* Builds an {@link InvokeFunctionNode} for a method with just one argument named {@code atom}.
*/
static InvokeFunctionNode buildInvokeNodeWithAtomArgument() {
return InvokeFunctionNode.build(
new CallArgumentInfo[] {new CallArgumentInfo("self"), new CallArgumentInfo("atom")},
DefaultsExecutionMode.EXECUTE,
ArgumentsExecutionMode.EXECUTE);
}
@TruffleBoundary
Function getHasCustomComparatorFunction() {
var comparableType = EnsoContext.get(this).getBuiltins().comparable().getType();
Function hasCustomComparatorFunc =
comparableType
.getDefinitionScope()
.getMethods()
.get(comparableType)
.get("has_custom_comparator");
assert hasCustomComparatorFunc != null : "Comparable.has_custom_comparator function must exist";
return hasCustomComparatorFunc;
}
}

View File

@ -0,0 +1,83 @@
package org.enso.interpreter.node.expression.builtin.ordering;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.GenerateUncached;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.interop.InteropLibrary;
import com.oracle.truffle.api.interop.UnsupportedMessageException;
import com.oracle.truffle.api.library.CachedLibrary;
import com.oracle.truffle.api.nodes.Node;
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.callable.argument.CallArgumentInfo;
import org.enso.interpreter.runtime.callable.atom.Atom;
import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.state.State;
/**
* Helper node for invocation of {@code Comparable.hash_callback atom}. Note that emulating the
* semantics of that function in Java code would be too complicated, so we rather implemented it in
* Enso and just call it from this node.
*/
@GenerateUncached
public abstract class HashCallbackNode extends Node {
public static HashCallbackNode getUncached() {
return HashCallbackNodeGen.getUncached();
}
/**
* Dispatches to the appropriate comparator for the given atom and calls {@code hash} method on
* it. Returns the value from that method.
*
* <p>Note that the given atom should have a custom comparator, otherwise it could be handled by
* {@link org.enso.interpreter.node.expression.builtin.meta.HashCodeNode}.
*
* @param atom Atom, preferably with a custom comparator, for which we get the custom comparator
* and call {@code hash} method on the comparator.
* @return Hash code for the atom, as returned by the custom comparator.
*/
public abstract long execute(Atom atom);
@Specialization
long hashCallbackCached(
Atom atom,
@Cached(value = "getHashCallbackFunction()", allowUncached = true) Function hashCallbackFunc,
@Cached(value = "buildInvokeNodeWithAtomArgument()", allowUncached = true)
InvokeFunctionNode hashCallbackInvokeNode,
@CachedLibrary(limit = "5") InteropLibrary interop) {
var ctx = EnsoContext.get(this);
var comparableType = ctx.getBuiltins().comparable().getType();
Object res =
hashCallbackInvokeNode.execute(
hashCallbackFunc, null, State.create(ctx), new Object[] {comparableType, atom});
try {
return interop.asLong(res);
} catch (UnsupportedMessageException e) {
throw new IllegalStateException(
"Return type from Comparable.hash_callback should be Long", e);
}
}
/**
* Builds an {@link InvokeFunctionNode} for a method with just one argument named {@code atom}.
*/
static InvokeFunctionNode buildInvokeNodeWithAtomArgument() {
return InvokeFunctionNode.build(
new CallArgumentInfo[] {new CallArgumentInfo("self"), new CallArgumentInfo("atom")},
DefaultsExecutionMode.EXECUTE,
ArgumentsExecutionMode.EXECUTE);
}
@TruffleBoundary
Function getHashCallbackFunction() {
var comparableType = EnsoContext.get(this).getBuiltins().comparable().getType();
Function hashCallback =
comparableType.getDefinitionScope().getMethods().get(comparableType).get("hash_callback");
assert hashCallback != null : "Comparable.hash_callback function must exist";
return hashCallback;
}
}

View File

@ -0,0 +1,305 @@
package org.enso.interpreter.node.expression.builtin.ordering;
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.InteropLibrary;
import com.oracle.truffle.api.interop.UnsupportedMessageException;
import com.oracle.truffle.api.library.CachedLibrary;
import com.oracle.truffle.api.nodes.Node;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import org.enso.interpreter.dsl.AcceptsError;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.data.text.Text;
import org.enso.interpreter.runtime.error.DataflowError;
import org.enso.interpreter.runtime.error.WarningsLibrary;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
@BuiltinMethod(
type = "Comparable",
name = "less_than_builtin",
description = """
Returns true if self is less than `other`. Or throw an error if the values are
not comparable.
"""
)
@GenerateUncached
public abstract class LessThanNode extends Node {
public static LessThanNode build() {
return LessThanNodeGen.create();
}
public abstract Object execute(@AcceptsError Object left, @AcceptsError Object other);
@Specialization
boolean lessIntegers(int i, int j) {
return i < j;
}
@Specialization
boolean lessBools(boolean b1, boolean b2) {
return !b1 && b2;
}
@Specialization
boolean lessLongs(long l1, long l2) {
return l1 < l2;
}
@Specialization
boolean lessDoubles(double self, double other) {
return self < other;
}
@Specialization
boolean lessLongDouble(long self, double other) {
return (double) self < other;
}
@Specialization
boolean lessDoubleLong(double self, long other) {
return self < (double) other;
}
@Specialization
boolean lessIntLong(int self, long other) {
return (long) self < other;
}
@Specialization
boolean lessLongInt(long self, int other) {
return self < (long) other;
}
@Specialization
boolean lessIntDouble(int self, double other) {
return (double) self < other;
}
@Specialization
boolean lessDoubleInt(double self, int other) {
return self < (double) other;
}
@Specialization
@TruffleBoundary
boolean lessBigInt(EnsoBigInteger self, EnsoBigInteger other) {
return self.getValue().compareTo(other.getValue()) < 0;
}
@Specialization
@TruffleBoundary
boolean lessBitIntDouble(EnsoBigInteger self, double other) {
return self.doubleValue() < other;
}
@Specialization
@TruffleBoundary
boolean lessDoubleBigInt(double self, EnsoBigInteger other) {
return self < other.doubleValue();
}
/**
* 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")
Object lessWithWarnings(Object selfWithWarnings, Object otherWithWarnings,
@CachedLibrary("selfWithWarnings") WarningsLibrary selfWarnLib,
@CachedLibrary("otherWithWarnings") WarningsLibrary otherWarnLib,
@Cached LessThanNode lessThanNode
) {
try {
Object self =
selfWarnLib.hasWarnings(selfWithWarnings) ? selfWarnLib.removeWarnings(selfWithWarnings)
: selfWithWarnings;
Object other =
otherWarnLib.hasWarnings(otherWithWarnings) ? otherWarnLib.removeWarnings(otherWithWarnings)
: otherWithWarnings;
return lessThanNode.execute(self, other);
} catch (UnsupportedMessageException e) {
throw new IllegalStateException(e);
}
}
@Specialization(limit = "3")
boolean lessTexts(Text selfText, Text otherText,
@CachedLibrary("selfText") InteropLibrary selfInterop,
@CachedLibrary("otherText") InteropLibrary otherInterop) {
if (selfText.is_normalized() && otherText.is_normalized()) {
return selfText.toString().compareTo(otherText.toString()) < 0;
} else {
return lessInteropStrings(selfText, otherText, selfInterop, otherInterop);
}
}
@Specialization(
guards = {
"selfInterop.isString(selfStr)",
"otherInterop.isString(otherStr)"
},
limit = "5"
)
@TruffleBoundary
boolean lessInteropStrings(Object selfStr, Object otherStr,
@CachedLibrary("selfStr") InteropLibrary selfInterop,
@CachedLibrary("otherStr") InteropLibrary otherInterop) {
String selfJavaString;
String otherJavaString;
try {
selfJavaString = selfInterop.asString(selfStr);
otherJavaString = otherInterop.asString(otherStr);
} catch (UnsupportedMessageException e) {
throw new IllegalStateException(e);
}
return Normalizer.compare(
selfJavaString,
otherJavaString,
Normalizer.FOLD_CASE_DEFAULT
) < 0;
}
@Specialization(guards = {
"selfInterop.isBoolean(selfBoolean)",
"otherInterop.isBoolean(otherBoolean)"
}, limit = "3")
boolean lessInteropBoolean(
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);
}
}
@TruffleBoundary
@Specialization(guards = {
"selfInterop.isDate(selfZonedDateTime)",
"selfInterop.isTime(selfZonedDateTime)",
"selfInterop.isTimeZone(selfZonedDateTime)",
"otherInterop.isDate(otherZonedDateTime)",
"otherInterop.isTime(otherZonedDateTime)",
"otherInterop.isTimeZone(otherZonedDateTime)"
}, limit = "3")
boolean lessInteropZonedDateTimes(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.compareTo(other) < 0;
} 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 lessInteropDateTimes(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.isBefore(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 lessInteropDates(Object selfDate, Object otherDate,
@CachedLibrary("selfDate") InteropLibrary selfInterop,
@CachedLibrary("otherDate") InteropLibrary otherInterop) {
try {
return selfInterop.asDate(selfDate).isBefore(
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 lessInteropTimes(Object selfTime, Object otherTime,
@CachedLibrary("selfTime") InteropLibrary selfInterop,
@CachedLibrary("otherTime") InteropLibrary otherInterop) {
try {
return selfInterop.asTime(selfTime).isBefore(
otherInterop.asTime(otherTime)
);
} catch (UnsupportedMessageException e) {
throw new IllegalStateException(e);
}
}
@Specialization(guards = {
"selfInterop.isDuration(selfDuration)",
"otherInterop.isDuration(otherDuration)"
}, limit = "3")
boolean lessInteropDuration(Object selfDuration, Object otherDuration,
@CachedLibrary("selfDuration") InteropLibrary selfInterop,
@CachedLibrary("otherDuration") InteropLibrary otherInterop) {
try {
Duration selfDur = selfInterop.asDuration(selfDuration);
Duration otherDur = otherInterop.asDuration(otherDuration);
return selfDur.compareTo(otherDur) < 0;
} catch (UnsupportedMessageException e) {
throw new IllegalStateException(e);
}
}
@Fallback
Object fallback(Object left, Object right) {
var typeError = EnsoContext.get(this).getBuiltins().error().makeTypeError(left, right, "right");
return DataflowError.withoutTrace(typeError, this);
}
}

View File

@ -29,6 +29,9 @@ import org.enso.interpreter.node.expression.builtin.meta.ProjectDescription;
import org.enso.interpreter.node.expression.builtin.mutable.Array;
import org.enso.interpreter.node.expression.builtin.mutable.Ref;
import org.enso.interpreter.node.expression.builtin.immutable.Vector;
import org.enso.interpreter.node.expression.builtin.ordering.Comparable;
import org.enso.interpreter.node.expression.builtin.ordering.DefaultOrderedComparator;
import org.enso.interpreter.node.expression.builtin.ordering.DefaultUnorderedComparator;
import org.enso.interpreter.node.expression.builtin.ordering.Ordering;
import org.enso.interpreter.node.expression.builtin.resource.ManagedResource;
import org.enso.interpreter.node.expression.builtin.text.Text;
@ -82,6 +85,9 @@ public class Builtins {
private final Number number;
private final Boolean bool;
private final Ordering ordering;
private final Comparable comparable;
private final DefaultOrderedComparator defaultOrderedComparator;
private final DefaultUnorderedComparator defaultUnorderedComparator;
private final System system;
private final Special special;
@ -128,6 +134,9 @@ public class Builtins {
error = new Error(this, context);
ordering = getBuiltinType(Ordering.class);
comparable = getBuiltinType(Comparable.class);
defaultUnorderedComparator = getBuiltinType(DefaultUnorderedComparator.class);
defaultOrderedComparator = getBuiltinType(DefaultOrderedComparator.class);
system = new System(this);
number = new Number(this);
bool = this.getBuiltinType(Boolean.class);
@ -585,6 +594,18 @@ public class Builtins {
return ordering;
}
public Comparable comparable() {
return comparable;
}
public DefaultOrderedComparator defaultOrderedComparator() {
return defaultOrderedComparator;
}
public DefaultUnorderedComparator defaultUnorderedComparator() {
return defaultUnorderedComparator;
}
/** @return the container for the dataflow error-related builtins */
public Type dataflowError() {
return dataflowError.getType();

View File

@ -40,7 +40,6 @@ public final class UnresolvedConversion implements TruffleObject {
* is returned. This is useful for certain subtyping relations, such as "any constructor is a
* subtype of Any" or "Nat is a subtype of Int, is a subtype of Number".
*
* @param constructors the constructors hierarchy for which this symbol should be resolved
* @return the resolved function definition, or null if not found
*/
public Function resolveFor(Type into, Type from) {

View File

@ -172,14 +172,6 @@ public final class EnsoDuration implements TruffleObject {
return new EnsoDuration(duration.minus(interop.asDuration(durationObject)));
}
@Builtin.Method(name = "compare_to_builtin", description = "Compares to other duration")
@Builtin.Specialize
@Builtin.WrapException(from = UnsupportedMessageException.class)
public long compareTo(Object durationObject, InteropLibrary interop)
throws UnsupportedMessageException {
return duration.compareTo(interop.asDuration(durationObject));
}
@ExportMessage
public boolean isDuration() {
return true;

View File

@ -202,7 +202,7 @@ public final class Type implements TruffleObject {
@ExportMessage
Object getMetaParents(@CachedLibrary("this") InteropLibrary lib)
throws UnsupportedMessageException {
if (isNothing(lib)) {
if (isNothing(lib) || !hasMetaParents()) {
throw UnsupportedMessageException.create();
}
assert supertype != null;

View File

@ -11,8 +11,8 @@ import com.oracle.truffle.api.library.ExportLibrary;
import com.oracle.truffle.api.library.ExportMessage;
import com.oracle.truffle.api.profiles.ConditionProfile;
import org.enso.interpreter.dsl.Builtin;
import org.enso.interpreter.node.expression.builtin.meta.EqualsAnyNode;
import org.enso.interpreter.node.expression.builtin.meta.HashCodeAnyNode;
import org.enso.interpreter.node.expression.builtin.meta.EqualsNode;
import org.enso.interpreter.node.expression.builtin.meta.HashCodeNode;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.data.Type;
import org.enso.interpreter.runtime.data.Vector;
@ -58,8 +58,8 @@ public final class EnsoHashMap implements TruffleObject {
return new EnsoHashMap(mapBuilder, snapshotSize);
}
static EnsoHashMap createEmpty(HashCodeAnyNode hashCodeAnyNode, EqualsAnyNode equalsNode) {
return new EnsoHashMap(EnsoHashMapBuilder.create(hashCodeAnyNode, equalsNode), 0);
static EnsoHashMap createEmpty(HashCodeNode hashCodeNode, EqualsNode equalsNode) {
return new EnsoHashMap(EnsoHashMapBuilder.create(hashCodeNode, equalsNode), 0);
}
EnsoHashMapBuilder getMapBuilder() {
@ -100,7 +100,7 @@ public final class EnsoHashMap implements TruffleObject {
@Builtin.Method
@Builtin.Specialize
public static EnsoHashMap empty(
@Cached HashCodeAnyNode hashCodeNode, @Cached EqualsAnyNode equalsNode) {
@Cached HashCodeNode hashCodeNode, @Cached EqualsNode equalsNode) {
return createEmpty(hashCodeNode, equalsNode);
}

View File

@ -3,8 +3,8 @@ package org.enso.interpreter.runtime.data.hash;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import java.util.ArrayList;
import java.util.List;
import org.enso.interpreter.node.expression.builtin.meta.EqualsAnyNode;
import org.enso.interpreter.node.expression.builtin.meta.HashCodeAnyNode;
import org.enso.interpreter.node.expression.builtin.meta.EqualsNode;
import org.enso.interpreter.node.expression.builtin.meta.HashCodeNode;
import org.graalvm.collections.EconomicMap;
import org.graalvm.collections.Equivalence;
@ -18,14 +18,14 @@ public final class EnsoHashMapBuilder {
/** All entries stored by their sequential index. */
private final List<StorageEntry> sequentialEntries;
private final HashCodeAnyNode hashCodeNode;
private final EqualsAnyNode equalsNode;
private final HashCodeNode hashCodeNode;
private final EqualsNode equalsNode;
private int size;
private EnsoHashMapBuilder(HashCodeAnyNode hashCodeAnyNode, EqualsAnyNode equalsNode) {
this.storage = EconomicMap.create(new StorageStrategy(equalsNode, hashCodeAnyNode));
private EnsoHashMapBuilder(HashCodeNode hashCodeNode, EqualsNode equalsNode) {
this.storage = EconomicMap.create(new StorageStrategy(equalsNode, hashCodeNode));
this.sequentialEntries = new ArrayList<>();
this.hashCodeNode = hashCodeAnyNode;
this.hashCodeNode = hashCodeNode;
this.equalsNode = equalsNode;
}
@ -56,7 +56,7 @@ public final class EnsoHashMapBuilder {
* @param hashCodeNode Node that will be stored in the storage for invoking `hash_code` on keys.
* @param equalsNode Node that will be stored in the storage for invoking `==` on keys.
*/
public static EnsoHashMapBuilder create(HashCodeAnyNode hashCodeNode, EqualsAnyNode equalsNode) {
public static EnsoHashMapBuilder create(HashCodeNode hashCodeNode, EqualsNode equalsNode) {
return new EnsoHashMapBuilder(hashCodeNode, equalsNode);
}
@ -164,13 +164,13 @@ public final class EnsoHashMapBuilder {
/**
* Custom {@link Equivalence} used for the {@link EconomicMap} that delegates {@code equals} to
* {@link EqualsAnyNode} and {@code hash_code} to {@link HashCodeAnyNode}.
* {@link EqualsNode} and {@code hash_code} to {@link HashCodeNode}.
*/
private static final class StorageStrategy extends Equivalence {
private final EqualsAnyNode equalsNode;
private final HashCodeAnyNode hashCodeNode;
private final EqualsNode equalsNode;
private final HashCodeNode hashCodeNode;
private StorageStrategy(EqualsAnyNode equalsNode, HashCodeAnyNode hashCodeNode) {
private StorageStrategy(EqualsNode equalsNode, HashCodeNode hashCodeNode) {
this.equalsNode = equalsNode;
this.hashCodeNode = hashCodeNode;
}

View File

@ -10,8 +10,8 @@ import com.oracle.truffle.api.interop.UnsupportedMessageException;
import com.oracle.truffle.api.library.CachedLibrary;
import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.node.expression.builtin.meta.EqualsAnyNode;
import org.enso.interpreter.node.expression.builtin.meta.HashCodeAnyNode;
import org.enso.interpreter.node.expression.builtin.meta.EqualsNode;
import org.enso.interpreter.node.expression.builtin.meta.HashCodeNode;
@BuiltinMethod(
type = "Map",
@ -61,8 +61,8 @@ public abstract class HashMapInsertNode extends Node {
EnsoHashMap doForeign(Object foreignMap, Object keyToInsert, Object valueToInsert,
@CachedLibrary("foreignMap") InteropLibrary mapInterop,
@CachedLibrary(limit = "3") InteropLibrary iteratorInterop,
@Cached HashCodeAnyNode hashCodeNode,
@Cached EqualsAnyNode equalsNode) {
@Cached HashCodeNode hashCodeNode,
@Cached EqualsNode equalsNode) {
var mapBuilder = EnsoHashMapBuilder.create(hashCodeNode, equalsNode);
try {
Object entriesIterator = mapInterop.getHashEntriesIterator(foreignMap);

View File

@ -11,8 +11,8 @@ import com.oracle.truffle.api.interop.UnsupportedMessageException;
import com.oracle.truffle.api.library.CachedLibrary;
import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.node.expression.builtin.meta.EqualsAnyNode;
import org.enso.interpreter.node.expression.builtin.meta.HashCodeAnyNode;
import org.enso.interpreter.node.expression.builtin.meta.EqualsNode;
import org.enso.interpreter.node.expression.builtin.meta.HashCodeNode;
import org.enso.interpreter.runtime.error.DataflowError;
@BuiltinMethod(
@ -49,11 +49,11 @@ public abstract class HashMapRemoveNode extends Node {
)
EnsoHashMap removeFromInteropMap(Object map, Object keyToRemove,
@CachedLibrary(limit = "5") InteropLibrary interop,
@Cached HashCodeAnyNode hashCodeNode,
@Cached EqualsAnyNode equalsNode) {
@Cached HashCodeNode hashCodeNode,
@Cached EqualsNode equalsNode) {
// We cannot simply call interop.isHashEntryExisting, because it would, most likely
// use the default `hashCode` and `equals` Java methods. But we need to use our
// EqualsAnyNode, so we do the check for non-existing key inside the while loop.
// EqualsNode, so we do the check for non-existing key inside the while loop.
boolean keyToRemoveFound = false;
var mapBuilder = EnsoHashMapBuilder.create(hashCodeNode, equalsNode);
try {

View File

@ -1,16 +1,23 @@
package org.enso.interpreter.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.enso.interpreter.node.expression.builtin.meta.EqualsAnyNode;
import org.enso.interpreter.node.expression.builtin.meta.EqualsNode;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
@ -19,22 +26,18 @@ import org.junit.runner.RunWith;
@RunWith(Theories.class)
public class EqualsTest extends TestBase {
private static Context context;
private EqualsAnyNode equalsNode;
private TestRootNode testRootNode;
private static EqualsNode equalsNode;
private static TestRootNode testRootNode;
@BeforeClass
public static void initContextAndData() {
context = createDefaultContext();
unwrappedValues = fetchAllUnwrappedValues();
}
@Before
public void initNodes() {
executeInContext(
context,
() -> {
testRootNode = new TestRootNode();
equalsNode = EqualsAnyNode.build();
equalsNode = EqualsNode.build();
testRootNode.insertChildren(equalsNode);
return null;
});
@ -101,4 +104,75 @@ public class EqualsTest extends TestBase {
return null;
});
}
/** Test for some specific values, for which we know that they are equal. */
@Test
public void testDateEquality() {
Object ensoDate =
unwrapValue(
context,
createValue(
context, "(Date.new 1999 3 23)", "import Standard.Base.Data.Time.Date.Date"));
Object javaDate = unwrapValue(context, context.asValue(LocalDate.of(1999, 3, 23)));
executeInContext(
context,
() -> {
assertTrue(equalsNode.execute(ensoDate, javaDate));
return null;
});
}
@Test
public void testTimeEquality() {
Object ensoTime =
unwrapValue(
context,
createValue(
context,
"Time_Of_Day.new 23 59",
"import Standard.Base.Data.Time.Time_Of_Day.Time_Of_Day"));
Object javaDate = unwrapValue(context, context.asValue(LocalTime.of(23, 59)));
executeInContext(
context,
() -> {
assertTrue(equalsNode.execute(ensoTime, javaDate));
return null;
});
}
@Test
public void testDateTimeEquality() {
Object ensoDateTime =
unwrapValue(
context,
createValue(
context,
"(Date_Time.new 1999 3 1 23 59)",
"import Standard.Base.Data.Time.Date_Time.Date_Time"));
Object javaDateTime =
unwrapValue(
context,
context.asValue(
ZonedDateTime.of(
LocalDate.of(1999, 3, 1), LocalTime.of(23, 59), ZoneId.systemDefault())));
executeInContext(
context,
() -> {
assertTrue(equalsNode.execute(ensoDateTime, javaDateTime));
return null;
});
}
@Test
public void testVectorsEquality() {
Object ensoVector =
unwrapValue(context, createValue(context, "[1,2,3]", "from Standard.Base.import all"));
Object javaVector = unwrapValue(context, context.asValue(List.of(1, 2, 3)));
executeInContext(
context,
() -> {
assertTrue(equalsNode.execute(ensoVector, javaVector));
return null;
});
}
}

View File

@ -7,8 +7,8 @@ import com.oracle.truffle.api.interop.InteropLibrary;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.enso.interpreter.node.expression.builtin.meta.EqualsAnyNode;
import org.enso.interpreter.node.expression.builtin.meta.HashCodeAnyNode;
import org.enso.interpreter.node.expression.builtin.meta.EqualsNode;
import org.enso.interpreter.node.expression.builtin.meta.HashCodeNode;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.junit.AfterClass;
@ -24,22 +24,18 @@ public class HashCodeTest extends TestBase {
private static Context context;
private static final InteropLibrary interop = InteropLibrary.getUncached();
private HashCodeAnyNode hashCodeNode;
private EqualsAnyNode equalsNode;
private TestRootNode testRootNode;
private static HashCodeNode hashCodeNode;
private static EqualsNode equalsNode;
private static TestRootNode testRootNode;
@BeforeClass
public static void initContextAndData() {
context = createDefaultContext();
// Initialize datapoints here, to make sure that it is initialized just once.
unwrappedValues = fetchAllUnwrappedValues();
}
@Before
public void initNodes() {
executeInContext(context, () -> {
hashCodeNode = HashCodeAnyNode.build();
equalsNode = EqualsAnyNode.build();
hashCodeNode = HashCodeNode.build();
equalsNode = EqualsNode.build();
testRootNode = new TestRootNode();
testRootNode.insertChildren(hashCodeNode, equalsNode);
return null;

View File

@ -14,6 +14,7 @@ import java.nio.file.Paths;
import java.util.Map;
import java.util.concurrent.Callable;
import org.enso.interpreter.EnsoLanguage;
import org.enso.polyglot.MethodNames.Module;
import org.enso.polyglot.RuntimeOptions;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Language;
@ -24,7 +25,7 @@ import org.graalvm.polyglot.proxy.ProxyExecutable;
public abstract class TestBase {
protected static Context createDefaultContext() {
var context =
Context.newBuilder("enso")
Context.newBuilder()
.allowExperimentalOptions(true)
.allowIO(true)
.allowAllAccess(true)
@ -63,7 +64,9 @@ public abstract class TestBase {
/**
* Unwraps the `receiver` field from the Value. This is a hack to allow us to test execute methods
* of artificially created ASTs, e.g., single nodes.
* of artificially created ASTs, e.g., single nodes. More specifically, only unwrapped values are
* eligible to be passed to node's execute methods, we cannot pass {@link Value} directly to the
* node's execute methods.
*
* <p>Does something similar to what {@link
* com.oracle.truffle.tck.DebuggerTester#getSourceImpl(Source)} does, but uses a different hack
@ -77,6 +80,42 @@ public abstract class TestBase {
return unwrapper.args[0];
}
/**
* Creates an Enso value from the given source.
*
* @param src One-line assignment into a variable
* @param imports Imports, may be empty.
*/
protected static Value createValue(Context ctx, String src, String imports) {
if (src.lines().count() > 1 || imports == null) {
throw new IllegalArgumentException("src should have one line, imports must not be null");
}
var sb = new StringBuilder();
sb.append(imports);
sb.append(System.lineSeparator());
sb.append("my_var = ").append(src);
sb.append(System.lineSeparator());
Value tmpModule = ctx.eval("enso", sb.toString());
return tmpModule.invokeMember(Module.EVAL_EXPRESSION, "my_var");
}
protected static Value createValue(Context ctx, String src) {
return createValue(ctx, src, "");
}
/**
* Evaluates the given source as if it was in an unnamed module.
*
* @param src The source code of the module
* @return The value returned from the main method of the unnamed module.
*/
protected static Value evalModule(Context ctx, String src) {
Value module = ctx.eval(Source.create("enso", src));
Value assocType = module.invokeMember(Module.GET_ASSOCIATED_TYPE);
Value mainMethod = module.invokeMember(Module.GET_METHOD, assocType, "main");
return mainMethod.execute();
}
/**
* An artificial RootNode. Used for tests of nodes that need to be adopted. Just create this root
* node inside a context, all the other nodes, and insert them via {@link

View File

@ -10,6 +10,7 @@ import java.time.LocalTime;
import java.time.Period;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
@ -324,8 +325,9 @@ class ValuesGenerator {
var collect = new ArrayList<Value>();
if (languages.contains(Language.ENSO)) {
collect.add(v(null, "import Standard.Base.Data.Time.Date.Date", "Date.now").type());
collect.add(v(null, "import Standard.Base.Data.Time.Date.Date", "Date.new 1999 3 23").type());
collect.add(v(null, "import Standard.Base.Data.Time.Date_Time.Date_Time", "Date_Time.now").type());
collect.add(v(null, "import Standard.Base.Data.Time.Time_Zone.Time_Zone", "Time_Zone.new").type());
collect.add(v(null, "import Standard.Base.Data.Time.Date_Time.Date_Time", "Date_Time.parse '2021-01-01T00:30:12.7102[UTC]'").type());
collect.add(v(null, "import Standard.Base.Data.Time.Time_Of_Day.Time_Of_Day", "Time_Of_Day.now").type());
collect.add(v(null, "import Standard.Base.Data.Time.Duration.Duration", "Duration.new").type());
for (var v : collect) {
@ -338,7 +340,9 @@ class ValuesGenerator {
if (languages.contains(Language.JAVA)) {
collect.add(ctx.asValue(LocalDate.of(2022, 12, 10)));
collect.add(ctx.asValue(LocalDate.of(1999, 3, 23)));
collect.add(ctx.asValue(LocalTime.of(12, 35)));
collect.add(ctx.asValue(ZonedDateTime.of(2021, 1, 1, 0, 30, 12, 710200000, ZoneId.of("Z"))));
}
return collect;

View File

@ -55,11 +55,12 @@ class CodeLocationsTest extends InterpreterTest {
withLocationsInstrumenter { instrumenter =>
val code =
"""import Standard.Base.Data.List.List
|import Standard.Base.Any.Any
|
|main = (2-2 == 0).if_then_else (List.Cons 5 6) 0
|""".stripMargin.linesIterator.mkString("\n")
instrumenter.assertNodeExists(44, 41, classOf[ApplicationNode])
instrumenter.assertNodeExists(69, 13, classOf[ApplicationNode])
instrumenter.assertNodeExists(73, 41, classOf[ApplicationNode])
instrumenter.assertNodeExists(98, 13, classOf[ApplicationNode])
eval(code)
()
}

View File

@ -29,6 +29,7 @@ class ConstructorsTest extends InterpreterTest {
"work with recursion" in {
val testCode =
"""import Standard.Base.Data.List.List
|import Standard.Base.Any.Any
|
|main =
| genList = i -> if i == 0 then List.Nil else List.Cons i (genList (i - 1))
@ -86,6 +87,7 @@ class ConstructorsTest extends InterpreterTest {
"be usable in code, with arbitrary definition order" in {
val testCode =
"""import Standard.Base.Nothing.Nothing
|import Standard.Base.Any.Any
|
|type C2
| Cons2 a b

View File

@ -1,29 +0,0 @@
package org.enso.interpreter.test.semantic
import org.enso.interpreter.test.{InterpreterContext, InterpreterTest}
class ConversionMethodsTest extends InterpreterTest {
override def subject: String = "Methods"
override def specify(implicit
interpreterContext: InterpreterContext
): Unit = {
"be defined in the global scope and dispatched to" in {
val code =
"""
|type Foo
| Mk_Foo foo
|type Bar
| Mk_Bar bar
|type Baz
| Mk_Baz baz
|
|Foo.from (that:Bar) = Foo.Mk_Foo that.bar
|Foo.from (that:Baz) = Foo.Mk_Foo that.baz
|
|main = (Foo.from (Baz.Mk_Baz 10)).foo + (Foo.from (Bar.Mk_Bar 20)).foo
|""".stripMargin
eval(code) shouldEqual 30
}
}
}

View File

@ -70,6 +70,7 @@ class EvalTest extends InterpreterTest {
"work in a recursive setting" in {
val code =
"""import Standard.Base.Runtime.Debug
|import Standard.Base.Any.Any
|
|main =
| fn = sumTo ->
@ -84,6 +85,7 @@ class EvalTest extends InterpreterTest {
"work inside a thunk passed to another function" in {
val code =
"""import Standard.Base.Runtime.Debug
|import Standard.Base.Any.Any
|
|main =
| fn = sumTo ->

View File

@ -59,6 +59,7 @@ class GlobalScopeTest extends InterpreterTest {
"be able to mutually recurse in the global scope" in {
val code =
"""import Standard.Base.Nothing
|import Standard.Base.Any.Any
|
|Nothing.decrementCall = number ->
| res = number - 1

View File

@ -32,6 +32,8 @@ class GroupingTest extends InterpreterTest {
"work with forced terms and lazy arguments" in {
val code =
"""
|import Standard.Base.Any.Any
|
|main =
| ifTest = c -> (~ifT) -> ~ifF -> if c == 0 then ifT else ifF
| sum = c -> acc -> ifTest c acc (@Tail_Call sum c-1 acc+c)

View File

@ -51,6 +51,7 @@ class LambdaShorthandArgsTest extends InterpreterTest {
"work with mixfix functions" in {
val code =
"""from Standard.Base.Data.Numbers import all
|import Standard.Base.Any.Any
|
|Number.if_then_else self = ~t -> ~f -> if self == 0 then t else f
|

View File

@ -43,6 +43,7 @@ class LambdaTest extends InterpreterTest {
"work with recursion" in {
val code =
"""
|import Standard.Base.Any.Any
|main =
| sumTo = x -> if x == 0 then 0 else x + (sumTo (x-1))
| sumTo 10
@ -105,6 +106,7 @@ class LambdaTest extends InterpreterTest {
"recurse with closing over lexical scope" in {
val code =
"""
|import Standard.Base.Any.Any
|main =
| summator = current ->
| if current == 0 then 0 else (x -> summator (current - 1)) 0
@ -135,6 +137,7 @@ class LambdaTest extends InterpreterTest {
"call fully saturated lambdas returned with TCO" in {
val code =
"""from Standard.Base.Data.Numbers import Number
|import Standard.Base.Any.Any
|
|Number.if_then_else self = ~t -> ~f -> if self == 0 then t else f
|

View File

@ -128,6 +128,7 @@ class NamedArgumentsTest extends InterpreterTest {
"work in a recursive context" in {
val code =
"""import Standard.Base.Nothing
|import Standard.Base.Any.Any
|
|Nothing.summer = sumTo ->
| summator = (acc = 0) -> current ->
@ -194,6 +195,8 @@ class NamedArgumentsTest extends InterpreterTest {
"be usable with constructors" in {
val code =
"""
|import Standard.Base.Any.Any
|
|type C2
| Cons2 head rest
|type Nil2
@ -214,6 +217,8 @@ class NamedArgumentsTest extends InterpreterTest {
"be usable and overridable in constructors" in {
val code =
"""
|import Standard.Base.Any.Any
|
|type Nil2
|type C2
| Cons2 head (rest = Nil2)

View File

@ -24,6 +24,7 @@ class SuspendedArgumentsTest extends InterpreterTest {
"not get executed upfront" in {
val code =
"""import Standard.Base.IO
|import Standard.Base.Any.Any
|
|main =
| foo = i -> ~x -> ~y -> if i == 0 then x else y
@ -36,6 +37,8 @@ class SuspendedArgumentsTest extends InterpreterTest {
"work well with tail recursion" in {
val code =
"""
|import Standard.Base.Any.Any
|
|main =
| ifTest = c -> ~ifT -> ~ifF -> if c == 0 then ifT else ifF
| sum = c -> acc -> ifTest c acc (@Tail_Call sum c-1 acc+c)
@ -78,6 +81,7 @@ class SuspendedArgumentsTest extends InterpreterTest {
"work properly with oversaturated arguments" in {
val code =
"""import Standard.Base.IO
|import Standard.Base.Any.Any
|
|main =
| ifTest = c -> ~ifT -> ~ifF -> if c == 0 then ifT else ifF

View File

@ -26,6 +26,7 @@ class BooleanTest extends InterpreterTest {
val code =
"""from Standard.Base.Data.Boolean import all
|import Standard.Base.IO
|import Standard.Base.Any.Any
|
|Boolean.isTrue self = self
|

View File

@ -11,6 +11,7 @@ class NumberTest extends InterpreterTest {
"support equality comparisons" in {
val code =
"""import Standard.Base.IO
|import Standard.Base.Any.Any
|
|main =
| IO.println 7==5
@ -23,6 +24,8 @@ class NumberTest extends InterpreterTest {
"support a recursive sum case" in {
val code =
"""
|import Standard.Base.Any.Any
|
|main = sumTo ->
| summator = acc -> current ->
| if current == 0 then acc else summator acc+current current-1

View File

@ -7,8 +7,18 @@ from Standard.Test import Bench
type My
Data x
compare_to self other =
other.x . compare_to self.x
type My_Comparator
is_ordered = True
compare my_1 my_2 =
comparator = Comparable.from my_2.x
comparator.compare my_2.x my_1.x
hash my =
comparator = Comparable.from my.x
comparator.hash my.x
Comparable.from (_:My) = My_Comparator
vector_size = 100000
iter_size = 20

View File

@ -9,7 +9,7 @@ compare_all_adjacent text_vector =
res
bench =
## The `Text.compare_to` benchmarks check both scenarios where the Texts are
## The `Text` compare benchmarks check both scenarios where the Texts are
short and very long - both checking the case where the difference appears
early or late in the long string. This is to see well any overheads
related to preprocessing the whole string and any possible early

View File

@ -57,7 +57,7 @@ bench =
partially_sorted_vec = make_partially_sorted_vec vector_size
random_vec = Utils.make_random_vec vector_size
projection = x -> x % 10
comparator = l -> r -> r.compare_to l
comparator = l -> r -> Ordering.compare l r
Bench.measure (sorted_vec.sort) "Already Sorted" iter_size num_iterations
Bench.measure (sorted_vec.sort Sort_Direction.Descending) "Sorted in Opposite Order" iter_size num_iterations
@ -66,6 +66,6 @@ bench =
Bench.measure (random_vec.sort) "Random Elements Ascending" iter_size num_iterations
Bench.measure (random_vec.sort Sort_Direction.Descending) "Random Elements Descending" iter_size num_iterations
Bench.measure (random_vec.sort on=projection) "Sorting with a Custom Projection" iter_size num_iterations
Bench.measure (random_vec.sort by=comparator) "Sorting with a Custom Comparison" iter_size num_iterations
Bench.measure (random_vec.sort by=comparator) "Sorting with the Default_Ordered_Comparator" iter_size num_iterations
main = bench

View File

@ -15,12 +15,16 @@ from project.Common_Table_Operations.Util import expect_column_names, run_defaul
type My_Type
Value x y
compare_to self other = case other of
My_Type.Value ox oy ->
self.x+self.y . compare_to ox+oy
_ -> Ordering.Less
type My_Type_Comparator
is_ordered = True
compare my_1 my_2 =
comp = Comparable.from my_1.x
comp.compare (my_1.x + my_1.y) (my_2.x + my_2.y)
hash my_type = Comparable.from my_type.x . hash (my_type.x + my_type.y)
== self other = self.compare_to other == Ordering.Equal
Comparable.from (_:My_Type) = My_Type_Comparator
main = run_default_backend spec

View File

@ -26,15 +26,23 @@ polyglot java import org.enso.table.data.table.Table as Java_Table
type My
Data x y
== self that = case that of
My.Data x1 y1 -> (self.x + self.y) == (x1 + y1)
_ -> False
compare_to self that = self.x+self.y . compare_to that.x+that.y
frobnicate self = case self of
My.Data x1 y1 -> My.Data y1 x1
type My_Comparator
is_ordered = True
compare left right =
left_sum = left.x + left.y
right_sum = right.x + right.y
(Comparable.from left_sum) . compare left_sum right_sum
hash my =
sum = my.x + my.y
Comparable.from sum . hash sum
Comparable.from (_:My) = My_Comparator
spec =
make_varied_type_table =
strs = ["strs", ["a", "b", "c", Nothing]]
@ -462,7 +470,7 @@ spec =
Test.specify 'should allow passing a custom comparator' <|
c = Column.from_vector 'foo' [My.Data 1 2, My.Data 2 5, My.Data 3 4, My.Data 6 3, Nothing, My.Data 1 0]
cmp a b = (a.x-a.y).abs . compare_to (b.x-b.y).abs
cmp a b = Ordering.compare (a.x-a.y).abs (b.x-b.y).abs
r = c.sort by=cmp
r.to_vector.should_equal [My.Data 1 2, My.Data 3 4, My.Data 1 0, My.Data 2 5, My.Data 6 3, Nothing]

View File

@ -18,10 +18,10 @@ spec =
False.to_text . should_equal "False"
Test.specify "should allow for comparing Bools" <|
True.compare_to True . should_equal Ordering.Equal
False.compare_to False . should_equal Ordering.Equal
True.compare_to False . should_equal Ordering.Greater
False.compare_to True . should_equal Ordering.Less
(True == True) . should_be_true
(False == False) . should_be_true
(True > False) . should_be_true
(False < True) . should_be_true
Test.specify "should allow == operator" <|
True.should_equal True

View File

@ -3,6 +3,7 @@ import Standard.Base.Error.Illegal_Argument.Illegal_Argument
import Standard.Base.Error.No_Such_Key.No_Such_Key
import Standard.Base.Data.Time.Date_Time.Date_Time
from Standard.Base.Data.Map import Map
from Standard.Base.Data.Ordering import Default_Unordered_Comparator
from Standard.Test import Test, Test_Suite, Problems
import Standard.Test.Extensions
@ -75,6 +76,11 @@ spec =
empty_map.is_empty . should_be_true
non_empty.is_empty . should_be_false
Test.specify "should get default unordered comparator for polyglot maps" <|
Comparable.from (Map.empty) . should_equal Default_Unordered_Comparator
Comparable.from (js_empty_dict) . should_equal Default_Unordered_Comparator
Comparable.from (JavaMap.of "A" 1 "B" 2) . should_equal Default_Unordered_Comparator
Test.specify "should compare two hash maps" <|
(Map.singleton "a" 1).should_equal (Map.singleton "a" 1)
(Map.singleton "b" 2).should_not_equal (Map.singleton "a" 1)
@ -463,7 +469,7 @@ spec =
map.to_vector.should_equal [["A", 1], ["B", 2], ["C", 3]]
Test.specify "should treat Java Map as Enso map" <|
sort_by_keys vec = vec.sort by=x-> y-> x.first.compare_to y.first
sort_by_keys vec = vec.sort by=x-> y-> Ordering.compare x.first y.first
jmap = JavaMap.of "A" 1 "B" 2
(sort_by_keys jmap.to_vector) . should_equal [["A", 1], ["B", 2]]
(sort_by_keys (jmap.insert "C" 3 . to_vector)) . should_equal [["A", 1], ["B", 2], ["C", 3]]

View File

@ -385,29 +385,29 @@ spec =
(very_negative >= hundred_factorial).should_be_false
(very_negative >= Nothing).should_fail_with Type_Error.Error
Test.specify "should support compare_to" <|
(1.compare_to 2).should_equal Ordering.Less
(1.compare_to 1).should_equal Ordering.Equal
(1.compare_to 0).should_equal Ordering.Greater
(1.compare_to 1.01).should_equal Ordering.Less
(1.compare_to 0.99).should_equal Ordering.Greater
(3.compare_to hundred_factorial).should_equal Ordering.Less
(3.compare_to very_negative).should_equal Ordering.Greater
(3.compare_to Nothing).should_fail_with Type_Error.Error
(1.01.compare_to 0.99).should_equal Ordering.Greater
(1.01.compare_to 1.02).should_equal Ordering.Less
(1.01.compare_to 1).should_equal Ordering.Greater
(1.01.compare_to 2).should_equal Ordering.Less
(3.14.compare_to hundred_factorial).should_equal Ordering.Less
(3.14.compare_to very_negative).should_equal Ordering.Greater
(1.5.compare_to Nothing).should_fail_with Type_Error.Error
(hundred_factorial.compare_to 1).should_equal Ordering.Greater
(hundred_factorial.compare_to 1.5).should_equal Ordering.Greater
(very_negative.compare_to 1).should_equal Ordering.Less
(very_negative.compare_to 1.5).should_equal Ordering.Less
(hundred_factorial.compare_to very_negative).should_equal Ordering.Greater
(very_negative.compare_to hundred_factorial).should_equal Ordering.Less
(very_negative.compare_to Nothing).should_fail_with Type_Error.Error
Test.specify "should be ordered by Default_Ordered_Comparator" <|
Ordering.compare 1 2 . should_equal Ordering.Less
Ordering.compare 1 1 . should_equal Ordering.Equal
Ordering.compare 1 0 . should_equal Ordering.Greater
Ordering.compare 1 1.01 . should_equal Ordering.Less
Ordering.compare 1 0.99 . should_equal Ordering.Greater
Ordering.compare 3 hundred_factorial . should_equal Ordering.Less
Ordering.compare 3 very_negative . should_equal Ordering.Greater
Ordering.compare 3 Nothing . should_fail_with Type_Error.Error
Ordering.compare 1.01 0.99 . should_equal Ordering.Greater
Ordering.compare 1.01 1.02 . should_equal Ordering.Less
Ordering.compare 1.01 1 . should_equal Ordering.Greater
Ordering.compare 1.01 2 . should_equal Ordering.Less
Ordering.compare 3.14 hundred_factorial . should_equal Ordering.Less
Ordering.compare 3.14 very_negative . should_equal Ordering.Greater
Ordering.compare 1.5 Nothing . should_fail_with Type_Error.Error
Ordering.compare hundred_factorial 1 . should_equal Ordering.Greater
Ordering.compare hundred_factorial 1.5 . should_equal Ordering.Greater
Ordering.compare very_negative 1 . should_equal Ordering.Less
Ordering.compare very_negative 1.5 . should_equal Ordering.Less
Ordering.compare hundred_factorial very_negative . should_equal Ordering.Greater
Ordering.compare very_negative hundred_factorial . should_equal Ordering.Less
Ordering.compare very_negative Nothing . should_fail_with Type_Error.Error
Test.specify "should expose exponentiation operations" <|
(3.14 ^ 2.71).should_equal 22.216689546 epsilon=eps

View File

@ -13,12 +13,19 @@ import Standard.Test.Extensions
type Ord
Value number
compare_to : Ord -> Ordering
compare_to self that = that.number.compare_to self.number
# The comparison is reverted, i.e., `x < y` gives result for `y.number < x.number`.
type Ord_Comparator
is_ordered = True
compare x y = (Comparable.from y.number) . compare y.number x.number
hash x = (Comparable.from x.number) . hash x.number
Comparable.from (_:Ord) = Ord_Comparator
type No_Ord
Value number
Comparable.from (_:No_Ord) = Incomparable
# Tests
spec = Test.group "Object Comparator" <|

View File

@ -37,7 +37,7 @@ spec = Test.group "Natural Order" <|
Natural_Order.compare "100-200.300" "1.2.3" . should_equal Ordering.Greater
Natural_Order.compare "1.2.3" "4.5.6" . should_equal Ordering.Less
".".compare_to "-" . should_equal Ordering.Greater
Ordering.compare "." "-" . should_equal Ordering.Greater
Natural_Order.compare "4-5-6" "4.5.6" . should_equal Ordering.Less
Natural_Order.compare "4-5-6" "100-200.300" . should_equal Ordering.Less

View File

@ -16,7 +16,7 @@ spec = Test.group "Lexicographic Order on Vectors" <|
Vector_Lexicographic_Order.compare [1] [1] . should_equal Ordering.Equal
Test.specify "should work correctly with a custom comparator" <|
comparator = x-> y-> x.a.compare_to y.a
comparator = x-> y-> Ordering.compare x.a y.a
Vector_Lexicographic_Order.compare [My_Type.Value "a" 1, My_Type.Value "b" 1, My_Type.Value "c" 1] [My_Type.Value "b" 1, My_Type.Value "a" 1, My_Type.Value "c" 1] element_comparator=comparator . should_equal Ordering.Less
Vector_Lexicographic_Order.compare [My_Type.Value "a" 1, My_Type.Value "b" 1, My_Type.Value "c" 1] [My_Type.Value "a" 100, My_Type.Value "b" 2, My_Type.Value "c" 3] element_comparator=comparator . should_equal Ordering.Equal

View File

@ -1,5 +1,6 @@
from Standard.Base import all
import Standard.Base.Error.Common.Type_Error
import Standard.Base.Error.Incomparable_Values.Incomparable_Values
from Standard.Test import Test, Test_Suite
import Standard.Test.Extensions
@ -9,30 +10,56 @@ import Standard.Test.Extensions
type Ord
Value number
compare_to : Ord -> Ordering
compare_to self that = if self.number == that.number then Ordering.Equal else
if self.number > that.number then Ordering.Greater else Ordering.Less
type Ord_Comparator
is_ordered = True
compare x y = (Comparable.from x.number) . compare x.number y.number
hash x = (Comparable.from x.number) . hash x.number
Comparable.from (_:Ord) = Ord_Comparator
## Unordered pair
type UPair
Value x y
type UPair_Comparator
is_ordered = False
equals pair1 pair2 =
if pair1.x == pair2.x && pair1.y == pair2.y then True else
if pair1.x == pair2.y && pair1.y == pair2.x then True else
False
hash upair =
x_comp = Comparable.from upair.x
y_comp = Comparable.from upair.y
(x_comp.hash upair.x) + (y_comp.hash upair.y)
Comparable.from (_ : UPair) = UPair_Comparator
type Parent
Value child
# === The Tests ===
spec =
Test.group "Default ordered comparator" <|
Test.specify "should support custom ordered comparator" <|
Ordering.compare (Ord.Value 1) (Ord.Value 2) . should_equal Ordering.Less
Ordering.compare (Ord.Value 1) (Ord.Value 1) . should_equal Ordering.Equal
Ordering.compare (Ord.Value 20) (Ord.Value 1) . should_equal Ordering.Greater
Test.specify "should support equality for custom ordered comparators in atom field" <|
((Parent.Value (Ord.Value 1)) == (Parent.Value (Ord.Value 1))) . should_be_true
((Parent.Value (Ord.Value 1)) == (Parent.Value (Ord.Value 22))) . should_be_false
Test.specify "should throw Incomparable_Values when comparing types with unordered comparator" <|
Ordering.compare (UPair.Value 1 2) (UPair.Value 2 1) . should_fail_with Incomparable_Values
Test.specify "should throw Type_Error when comparing different types" <|
Ordering.compare (UPair.Value 1 2) (Ord.Value 2) . should_fail_with Type_Error.Error
Ordering.compare 1 Nothing . should_fail_with Type_Error.Error
Test.group "Ordering" <|
Test.specify "should allow comparing Less" <|
left = Ord.Value 1032
right = Ord.Value 101111
left.compare_to right . should_equal Ordering.Less
Test.specify "should allow comparing Equal" <|
left = Ord.Value 1032
right = Ord.Value 1032
left.compare_to right . should_equal Ordering.Equal
Test.specify "should allow comparing Greater" <|
left = Ord.Value 1032
right = Ord.Value -1
left.compare_to right . should_equal Ordering.Greater
Test.specify "should allow conversion to sign representation" <|
Ordering.Less.to_sign . should_equal -1
Ordering.Equal.to_sign . should_equal 0
@ -44,15 +71,15 @@ spec =
Ordering.from_sign 1 . should_equal Ordering.Greater
Test.specify "should be ordered itself" <|
Ordering.Less.compare_to Ordering.Less . should_equal Ordering.Equal
Ordering.Less.compare_to Ordering.Equal . should_equal Ordering.Less
Ordering.Less.compare_to Ordering.Greater . should_equal Ordering.Less
Ordering.Equal.compare_to Ordering.Less . should_equal Ordering.Greater
Ordering.Equal.compare_to Ordering.Equal . should_equal Ordering.Equal
Ordering.Equal.compare_to Ordering.Greater . should_equal Ordering.Less
Ordering.Greater.compare_to Ordering.Less . should_equal Ordering.Greater
Ordering.Greater.compare_to Ordering.Equal . should_equal Ordering.Greater
Ordering.Greater.compare_to Ordering.Greater . should_equal Ordering.Equal
Ordering.compare Ordering.Less Ordering.Less . should_equal Ordering.Equal
Ordering.compare Ordering.Less Ordering.Equal . should_equal Ordering.Less
Ordering.compare Ordering.Less Ordering.Greater . should_equal Ordering.Less
Ordering.compare Ordering.Equal Ordering.Less . should_equal Ordering.Greater
Ordering.compare Ordering.Equal Ordering.Equal . should_equal Ordering.Equal
Ordering.compare Ordering.Equal Ordering.Greater . should_equal Ordering.Less
Ordering.compare Ordering.Greater Ordering.Less . should_equal Ordering.Greater
Ordering.compare Ordering.Greater Ordering.Equal . should_equal Ordering.Greater
Ordering.compare Ordering.Greater Ordering.Greater . should_equal Ordering.Equal
Test.specify "should allow lexicographical composition" <|
Ordering.Less.and_then Ordering.Less . should_equal Ordering.Less
@ -66,8 +93,8 @@ spec =
Ordering.Greater.and_then Ordering.Greater . should_equal Ordering.Greater
Test.specify "should fail with Type_Error for wrong type of that" <|
Ordering.Less.compare_to 1 . should_fail_with Type_Error.Error
Ordering.Less.compare_to Nothing . should_fail_with Type_Error.Error
Ordering.Less.compare_to "Hello" . should_fail_with Type_Error.Error
Ordering.compare Ordering.Less 1 . should_fail_with Type_Error.Error
Ordering.compare Ordering.Less Nothing . should_fail_with Type_Error.Error
Ordering.compare Ordering.Less "Hello" . should_fail_with Type_Error.Error
main = Test_Suite.run_main spec

View File

@ -11,12 +11,18 @@ import Standard.Test.Extensions
type Ord
Value number
Ord.compare_to : Ord -> Ordering
Ord.compare_to self that = that.number.compare_to self.number
type Ord_Comparator
is_ordered = True
compare x y = (Comparable.from x.number) . compare x.number y.number
hash x = (Comparable.from x.number) . hash x.number
Comparable.from (_:Ord) = Ord_Comparator
type No_Ord
Value number
Comparable.from (_:No_Ord) = Incomparable
# Tests
spec =
@ -155,12 +161,12 @@ spec =
text_set.compute Statistic.Kurtosis . should_fail_with Illegal_Argument.Error
text_set.running Statistic.Sum . should_fail_with Illegal_Argument.Error
Test.specify "should be able to do Count, Minimum and Maximum on custom type with compare_to" <|
Test.specify "should be able to do Count, Minimum and Maximum on custom type with custom ordered comparator" <|
ord_set.compute . should_equal 3
ord_set.compute Statistic.Minimum . should_equal (Ord.Value 10)
ord_set.compute Statistic.Maximum . should_equal (Ord.Value 2)
ord_set.compute Statistic.Minimum . should_equal (Ord.Value 2)
ord_set.compute Statistic.Maximum . should_equal (Ord.Value 10)
Test.specify "should fail with Incomparable_Values on custom type without compare_to" <|
Test.specify "should fail with Incomparable_Values on custom incomparable type" <|
no_ord_set.compute . should_equal 3
no_ord_set.compute Statistic.Minimum . should_fail_with Incomparable_Values
no_ord_set.compute Statistic.Maximum . should_fail_with Incomparable_Values
@ -189,7 +195,7 @@ spec =
values = ["G", "AA", "B", "G", "D"]
Statistic.rank_data values . should_equal [1.5, 5, 4, 1.5, 3]
Test.specify "should fail with Incomparable_Values on custom type without compare_to" <|
Test.specify "should fail with Incomparable_Values on custom incomparable type" <|
values = [No_Ord.Value 10, No_Ord.Value 2, No_Ord.Value 9]
Statistic.rank_data values . should_fail_with Incomparable_Values

View File

@ -91,39 +91,39 @@ spec =
complex_letter_1 . should_equal complex_letter_2
complex_letter_1 . should_equal complex_letter_3
complex_letter_3 . should_equal complex_letter_2
common_prefix+complex_letter_1+complex_letter_2+complex_letter_3 . compare_to common_prefix+complex_letter_3+complex_letter_1+complex_letter_2 . should_equal Ordering.Equal
Ordering.compare (common_prefix+complex_letter_1+complex_letter_2+complex_letter_3) (common_prefix+complex_letter_3+complex_letter_1+complex_letter_2) . should_equal Ordering.Equal
'e\u{301}'=='e\u{302}' . should_be_false
'a\u0321\u0302'=='a\u0302\u0321' . should_be_true
'a\u0321\u0302'=='A\u0302\u0321' . should_be_false
accent_1+"a" . compare_to accent_2+"a" . should_equal Ordering.Equal
accent_1+"A" . compare_to accent_2+"a" . should_equal Ordering.Less
Ordering.compare accent_1+"a" accent_2+"a" . should_equal Ordering.Equal
Ordering.compare accent_1+"A" accent_2+"a" . should_equal Ordering.Less
accent_1+"A" . compare_to_ignore_case accent_2+"a" . should_equal Ordering.Equal
accent_1+"a" . compare_to accent_2+"b" . should_equal Ordering.Less
Ordering.compare accent_1+"a" accent_2+"b" . should_equal Ordering.Less
accent_1+"a" . compare_to_ignore_case accent_2+"B" . should_equal Ordering.Less
accent_2+"a" . compare_to accent_1+"b" . should_equal Ordering.Less
accent_1+"a" . compare_to accent_2+"B" . should_equal Ordering.Greater
Ordering.compare accent_2+"a" accent_1+"b" . should_equal Ordering.Less
Ordering.compare accent_1+"a" accent_2+"B" . should_equal Ordering.Greater
accent_1+"a" . compare_to_ignore_case accent_2+"B" . should_equal Ordering.Less
accent_1+"b" . compare_to accent_2+"a" . should_equal Ordering.Greater
accent_2+"b" . compare_to accent_1+"a" . should_equal Ordering.Greater
Ordering.compare accent_1+"b" accent_2+"a" . should_equal Ordering.Greater
Ordering.compare accent_2+"b" accent_1+"a" . should_equal Ordering.Greater
# Handling of Nothing
(accent_1 == Nothing) . should_be_false
(accent_1 != Nothing) . should_be_true
accent_1 . compare_to Nothing . should_fail_with Type_Error.Error
Ordering.compare accent_1 Nothing . should_fail_with Type_Error.Error
(accent_1 > Nothing) . should_fail_with Type_Error.Error
accent_1 . compare_to_ignore_case Nothing . should_fail_with Type_Error.Error
earlier_suffix = "aooooz"
later_suffix = "bo"
common_prefix+complex_letter_1+earlier_suffix . compare_to common_prefix+complex_letter_2+later_suffix . should_equal Ordering.Less
common_prefix+complex_letter_2+earlier_suffix . compare_to common_prefix+complex_letter_1+later_suffix . should_equal Ordering.Less
common_prefix+complex_letter_2+earlier_suffix . compare_to common_prefix+complex_letter_3+later_suffix . should_equal Ordering.Less
common_prefix+complex_letter_3+earlier_suffix . compare_to common_prefix+complex_letter_1+later_suffix . should_equal Ordering.Less
common_prefix+complex_letter_3+later_suffix . compare_to common_prefix+complex_letter_1+earlier_suffix . should_equal Ordering.Greater
common_prefix+complex_letter_1+later_suffix . compare_to common_prefix+complex_letter_2+earlier_suffix . should_equal Ordering.Greater
Ordering.compare common_prefix+complex_letter_1+earlier_suffix common_prefix+complex_letter_2+later_suffix . should_equal Ordering.Less
Ordering.compare common_prefix+complex_letter_2+earlier_suffix common_prefix+complex_letter_1+later_suffix . should_equal Ordering.Less
Ordering.compare common_prefix+complex_letter_2+earlier_suffix common_prefix+complex_letter_3+later_suffix . should_equal Ordering.Less
Ordering.compare common_prefix+complex_letter_3+earlier_suffix common_prefix+complex_letter_1+later_suffix . should_equal Ordering.Less
Ordering.compare common_prefix+complex_letter_3+later_suffix common_prefix+complex_letter_1+earlier_suffix . should_equal Ordering.Greater
Ordering.compare common_prefix+complex_letter_1+later_suffix common_prefix+complex_letter_2+earlier_suffix . should_equal Ordering.Greater
Test.specify "should correctly handle case-insensitive equality" <|
"aBc" . equals_ignore_case "Abc" . should_be_true

View File

@ -122,6 +122,9 @@ spec_with name create_new_date parse_date =
result ->
Test.fail ("Unexpected result: " + result.to_text)
Test.specify "should get default ordered comparator for dates" <|
Comparable.from (create_new_date 2023 2 3) . should_equal Default_Ordered_Comparator
Test.specify "should be comparable" <|
date_1 = parse_date "2021-01-02"
date_2 = parse_date "2021-01-01"
@ -132,7 +135,7 @@ spec_with name create_new_date parse_date =
date_1<date_2 . should_be_false
datetime = Date_Time.new 2021 1 2 12 40
date_1.compare_to datetime . should_fail_with Type_Error.Error
Ordering.compare date_1 datetime . should_fail_with Type_Error.Error
date_1<datetime . should_fail_with Type_Error.Error
date_1>=datetime . should_fail_with Type_Error.Error
date_1==datetime . should_be_false

View File

@ -314,6 +314,11 @@ spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision=
time . nanosecond . should_equal 0
time . zone . zone_id . should_equal Time_Zone.utc.zone_id
Test.specify "should get default ordered comparator for datetimes" <|
Comparable.from (create_new_datetime 2023 2 3 23 59) . should_equal Default_Ordered_Comparator
Comparable.from (parse_datetime "2021-01-01T00:30:12.7102[UTC]") . should_equal Default_Ordered_Comparator
Comparable.from (create_new_datetime 2022 10 31 2 30 55 1234) . should_equal Default_Ordered_Comparator
Test.specify "should be comparable" <|
time_1 = parse_datetime "2021-01-01T00:30:12.7102[UTC]"
time_2 = parse_datetime "2021-01-01T04:00:10.0+04:00"
@ -324,12 +329,12 @@ spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision=
time_1<time_2 . should_be_false
tod = Time_Of_Day.new 4 0 10
time_1.compare_to tod . should_fail_with Type_Error.Error
Ordering.compare time_1 tod . should_fail_with Type_Error.Error
time_1<tod . should_fail_with Type_Error.Error
time_1==tod . should_be_false
date = Date.new 2021 1 1
time_1.compare_to date . should_fail_with Type_Error.Error
Ordering.compare time_1 date . should_fail_with Type_Error.Error
time_1==date . should_be_false
Test.specify "simple computations before Enso epoch should produce a warning" <|

View File

@ -47,7 +47,7 @@ spec =
Test.specify "should be comparable" <|
duration_1 = (Duration.new hours=5)
duration_2 = (Duration.new minutes=1)
duration_1.compare_to duration_1 . should_equal Ordering.Equal
Ordering.compare duration_1 duration_1 . should_equal Ordering.Equal
duration_1==duration_1 . should_be_true
duration_1!=duration_2 . should_be_true
duration_1>duration_2 . should_be_true

View File

@ -116,8 +116,7 @@ specWith name create_new_time parse_time =
time_1!=time_2 . should_be_true
time_1>time_2 . should_be_true
time_1<time_2 . should_be_false
time_2.compare_to (Date_Time.new 1999 1 1 4 0 10) . should_fail_with Type_Error.Error
time_2<(Date_Time.new 1999 1 1 4 0 10) . should_fail_with Type_Error.Error
Test.specify "should correctly determine the type of timeofday" <|
new_timeofday = create_new_time 15 37 58

View File

@ -16,10 +16,12 @@ import Standard.Test.Extensions
type T
Value a b
== self that = self.a == that.a
type T_Comparator
is_ordered = True
compare t1 t2 = Comparable.from t1.a . compare t1.a t2.a
hash t = Comparable.from t.a . hash t.a
compare_to self that = if self == that then Ordering.Equal else
if self.a > that.a then Ordering.Greater else Ordering.Less
Comparable.from (_:T) = T_Comparator
type My_Error
Value a
@ -28,10 +30,10 @@ type Foo
Value vec
compare_tco a b = case a.vec.length == b.vec.length of
False -> a.vec.length . compare_to b.vec.length
False -> Ordering.compare a.vec.length b.vec.length
True ->
go ix = if ix > a.vec.length then Ordering.Equal else
cmp = (a.vec.at ix) . compare_to (b.vec.at ix)
cmp = Ordering.compare (a.vec.at ix) (b.vec.at ix)
case cmp of
Ordering.Equal -> @Tail_Call go ix+1
_ -> cmp
@ -579,20 +581,20 @@ spec = Test.group "Vectors" <|
small_expected = [T.Value -20 0, T.Value 4 0, T.Value -1 1, T.Value 1 3, T.Value 1 8, T.Value -1 10]
small_vec.sort (on = _.b) . should_equal small_expected
Test.specify "should be able to use a custom comparator" <|
Test.specify "should be able to use a custom compare function" <|
small_vec = [2, 7, -3, 383, -392, 28, -90]
small_expected = [383, 28, 7, 2, -3, -90, -392]
small_vec.sort (by = l -> r -> r.compare_to l) . should_equal small_expected
small_vec.sort (by = l -> r -> Ordering.compare r l) . should_equal small_expected
Test.specify "should allow tail-recursive comparators in sort" <|
v = [Foo.Value [4,2,2], Foo.Value [1,2,3], Foo.Value [1,2,4]]
r = [Foo.Value [1,2,3], Foo.Value [1,2,4], Foo.Value [4,2,2]]
v.sort by=compare_tco . should_equal r
Test.specify "should be able to use a custom comparator and projection" <|
Test.specify "should be able to use a custom compare function and projection" <|
small_vec = [T.Value 1 8, T.Value 1 3, T.Value -20 0, T.Value -1 1, T.Value -1 10, T.Value 4 0]
small_expected = [T.Value -1 10, T.Value 1 8, T.Value 1 3, T.Value -1 1, T.Value -20 0, T.Value 4 0]
small_vec.sort (on = _.b) (by = l -> r -> r.compare_to l) . should_equal small_expected
small_vec.sort (on = _.b) (by = l -> r -> Ordering.compare r l) . should_equal small_expected
Test.specify "should be able to sort in descending order" <|
small_vec = [2, 7, -3, 383, -392, 28, -90]
@ -653,7 +655,7 @@ spec = Test.group "Vectors" <|
[1, 1.0, 2, 2.0].distinct . should_equal [1, 2]
[].distinct . should_equal []
Test.specify "should correctly handle distinct with custom types like Atoms that implement compare_to" <|
Test.specify "should correctly handle distinct with types that have custom comparators" <|
[T.Value 1 2, T.Value 3 3, T.Value 1 2].distinct . should_equal [T.Value 1 2, T.Value 3 3]
Test.specify "should return a vector containing only unique elements up to some criteria" <|

View File

@ -14,32 +14,33 @@ type CustomEqType
CustomEqType.C1 f1 -> f1
CustomEqType.C2 f1 f2 -> f1 + f2
== self other = self.sum == other.sum
type CustomEqType_Comparator
is_ordered = False
equals o1 o2 = o1.sum == o2.sum
hash o =
comp = Comparable.from o.sum
comp.hash o.sum
Comparable.from (_:CustomEqType) = CustomEqType_Comparator
type Child
Value number
== : Any -> Boolean
== self other = case other of
_ : Child -> (self.number % 100) == (other.number % 100)
_ -> False
type Child_Comparator
is_ordered = False
equals child1 child2 = child1.number % 100 == child2.number % 100
hash child =
comp = Comparable.from child.number
comp.hash (child.number % 100)
Comparable.from (_:Child) = Child_Comparator
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
@ -120,6 +121,9 @@ spec =
((Point.Value 'ś' 23.0) == (Point.Value 's\u0301' 23)) . should_be_true
Test.specify "should dispatch to overriden `==` on atoms" <|
(Child.Value 11 == Child.Value 111) . should_be_true
Test.specify "should dispatch to overriden `==` on atoms transitively" <|
child1 = Child.Value 11
parent1 = Parent.Value child1
grand_parent1 = GrandParent.Value parent1
@ -130,12 +134,17 @@ spec =
(grand_parent1 == grand_parent2).should_be_true
Test.specify "should handle `==` on types with many fields" <|
Test.specify "should handle `==` on types with many fields with custom comparator" <|
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 handle `==` on atoms with fields with mixed comparators" <|
obj_1 = FourFieldType.Value (Child.Value 1) 42 (Child.Value 2) 83
obj_2 = FourFieldType.Value (Child.Value 101) 42 (Child.Value 102) 83
(obj_1 == obj_2).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
@ -152,16 +161,16 @@ spec =
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" <|
Test.specify "should handle `==` on types" <|
(Child == Child).should_be_true
(Child == Point).should_be_false
(Point == Child).should_be_false
(JavaPath == Child).should_be_false
(Child == JavaPath).should_be_false
(Boolean == Any).should_be_false
(Boolean == Boolean).should_be_true
Test.specify "should handle `==` on types with Any as first operand" pending="Any == Boolean resolves to Any.type.== static method call" <|
(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

View File

@ -3,3 +3,8 @@ type Any
catch_primitive handler = @Builtin_Method "Any.catch_primitive"
to_text self = @Builtin_Method "Any.to_text"
to_display_text self = @Builtin_Method "Any.to_display_text"
# Access EqualsNode directly
== self other = Comparable.equals_builtin self other
@Builtin_Type
type Comparable