sort handles incomparable values (#5998)

* Update type ascriptions in some operators in Any

* Add @GenerateUncached to AnyToTextNode.

Will be used in another node with @GenerateUncached.

* Add tests for "sort handles incomparable types"

* Vector.sort handles incomparable types

* Implement sort handling for different comparators

* Comparison operators in Any do not throw Type_Error

* Fix some issues in Ordering_Spec

* Remove the remaining comparison operator overrides for numbers.

* Consolidate all sorting functionality into a single builtin node.

* Fix warnings attachment in sort

* PrimitiveValuesComparator handles other types than primitives

* Fix byFunc calling

* on function can be called from the builtin

* Fix build of native image

* Update changelog

* Add VectorSortTest

* Builtin method should not throw DataflowError.

If yes, the message is discarded (a bug?)

* TypeOfNode may not return only Type

* UnresolvedSymbol is not supported as `on` argument to Vector.sort_builtin

* Fix docs

* Fix bigint spec in LessThanNode

* Small fixes

* Small fixes

* Nothings and Nans are sorted at the end of default comparator group.

But not at the whole end of the resulting vector.

* Fix checking of `by` parameter - now accepts functions with default arguments.

* Fix changelog formatting

* Fix imports in DebuggingEnsoTest

* Remove Array.sort_builtin

* Add comparison operators to micro-distribution

* Remove Array.sort_builtin

* Replace Incomparable_Values by Type_Error in some tests

* Add on_incomparable argument to Vector.sort_builtin

* Fix after merge - Array.sort delegates to Vector.sort

* Add more tests for problem_behavior on Vector.sort

* SortVectorNode throws only Incomparable_Values.

* Delete Collections helper class

* Add test for expected failure for custom incomparable values

* Cosmetics.

* Fix test expecting different comparators warning

* isNothing is checked via interop

* Remove TruffleLogger from SortVectorNode

* Small review refactorings

* Revert "Remove the remaining comparison operator overrides for numbers."

This reverts commit 0df66b1080.

* Improve bench_download.py tool's `--compare` functionality.

- Output table is sorted by benchmark labels.
- Do not fail when there are different benchmark labels in both runs.

* Wrap potential interop values with `HostValueToEnsoNode`

* Use alter function in Vector_Spec

* Update docs

* Invalid comparison throws Incomparable_Values rather than Type_Error

* Number comparison builtin methods return Nothing in case of incomparables
This commit is contained in:
Pavel Marek 2023-04-16 16:40:12 +02:00 committed by GitHub
parent 64ecaa85e3
commit b42e910280
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1402 additions and 319 deletions

View File

@ -684,6 +684,7 @@
- [Ensure calls involving warnings remain instrumented][6067]
- [One can define lazy atom fields][6151]
- [Replace IOContexts with Execution Environment and generic Context][6171]
- [Vector.sort handles incomparable types][5998]
[3227]: https://github.com/enso-org/enso/pull/3227
[3248]: https://github.com/enso-org/enso/pull/3248
@ -789,6 +790,7 @@
[6067]: https://github.com/enso-org/enso/pull/6067
[6151]: https://github.com/enso-org/enso/pull/6151
[6171]: https://github.com/enso-org/enso/pull/6171
[5998]: https://github.com/enso-org/enso/pull/5998
# Enso 2.0.0-alpha.18 (2021-10-12)

View File

@ -481,11 +481,11 @@ type Any
## PRIVATE
Checks if the comparators for the given objects are both of the same type. If so,
proceeds with the given action, and if not, throws `Type_Error`.
assert_same_comparators : Any -> Any -> (Any -> Any) -> Any ! Type_Error
proceeds with the given action, and if not, throws `Incomparable_Values` error.
assert_same_comparators : Any -> Any -> (Any -> Any) -> Any ! Incomparable_Values
assert_same_comparators this that ~action =
comp_this = Comparable.from this
comp_that = Comparable.from that
case Meta.is_same_object comp_this comp_that of
True -> action
False -> Error.throw (Type_Error.Error (Meta.type_of comp_this) comp_that "comp_that")
False -> Error.throw (Incomparable_Values.Error this that)

View File

@ -14,6 +14,7 @@ import project.Error.Error
import project.Errors.Common.Incomparable_Values
import project.Errors.Common.Not_Found
import project.Errors.Common.Index_Out_Of_Bounds
import project.Errors.Problem_Behavior.Problem_Behavior
import project.Meta
import project.Nothing.Nothing
import project.Panic.Panic
@ -160,13 +161,17 @@ type Array
Arguments:
- order: The order in which the array elements are sorted.
- on: A projection from the element type to the value of that element
being sorted on.
being sorted on. If set to `Nothing` (the default),
identity function will be used.
- by: A function that compares the result of applying `on` to two
elements, returning an Ordering to compare them.
elements, returning an an `Ordering` if the two elements are comparable
or `Nothing` if they are not. If set to `Nothing` (the default argument),
`Ordering.compare _ _` method will be used.
- on_incomparable: A `Problem_Behavior` specifying what should happen if
two incomparable values are encountered.
By default, elements are sorted in ascending order.
By default, elements are sorted in ascending order, using the comparator
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.
@ -178,12 +183,32 @@ type Array
- *Average Time:* `O(n * log n)`
- *Worst-Case Space:* `O(n)` additional
? Incomparable values
Incomparable values are either values with different comparators or with
the same comparator returning `Nothing` from its `compare` method.
See the documentation of the `Ordering` module for more info.
? Implementation Note
The sort implementation is based upon an adaptive, iterative mergesort
that requires far fewer than `n * log(n)` comparisons when the array
is partially sorted. When the array is randomly ordered, the
performance is equivalent to a standard mergesort.
? Multiple comparators
Elements with different comparators are incomparable by definition.
This case is handled by first grouping the `self` array into groups
with the same comparator, recursively sorting these groups, and then
merging them back together. The order of the sorted groups in the
resulting array is based on the order of fully qualified names of
the comparators in the `self` array, with the exception of the group
for the default comparator, which is always the first group.
Additionally, an `Incomparable_Values` dataflow error will be returned
if the `on_incomparable` parameter is set to `Problem_Behavior.Report_Error`,
or a warning attached if the `on_incomparable` parameter is set to
`Problem_Behavior.Report_Warning` in case of encountering incomparable
values.
It takes equal advantage of ascending and descending runs in the array,
making it much simpler to merge two or more sorted arrays: simply
concatenate them and sort.
@ -197,8 +222,16 @@ type Array
Sorting an array of `Pair`s on the first element, descending.
[Pair 1 2, Pair -1 8].to_array.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 = (Ordering.compare _ _)) = Vector.sort self order on by
> Example
Sorting an array with elements with different comparators. Values `1`
and `My_Type` have different comparators. `1` will be sorted before `My_Type`
because it has the default comparator.
[My_Type.Value 'hello', 1].to_array.sort == [1, My_Type.Value 'hello'].to_array
sort : Sort_Direction -> (Any -> Any)|Nothing -> (Any -> Any -> (Ordering|Nothing))|Nothing -> Problem_Behavior -> Vector Any ! Incomparable_Values
sort self (order = Sort_Direction.Ascending) on=Nothing by=Nothing on_incomparable=Problem_Behavior.Ignore =
Vector.sort self order on by on_incomparable
## Creates a new `Vector` with only the specified range of elements from the
input, removing any elements outside the range.

View File

@ -117,7 +117,7 @@ invert_range_selection ranges length needs_sorting =
Empty subranges are discarded.
sort_and_merge_ranges : Vector Range -> Vector Range
sort_and_merge_ranges ranges =
sorted = ranges.filter (range-> range.is_empty.not) . sort on=(.start)
sorted = ranges.filter (range-> range.is_empty.not) . sort on=(_.start)
if sorted.is_empty then [] else
current_ref = Ref.new sorted.first
builder = Vector.new_builder

View File

@ -1,6 +1,7 @@
import project.Data.Text.Text
import project.Data.Locale.Locale
import project.Errors.Common.Arithmetic_Error
import project.Errors.Common.Incomparable_Values
import project.Error.Error
import project.Nothing.Nothing
import project.Panic.Panic
@ -279,6 +280,9 @@ type Number
## Checks equality of numbers, using an `epsilon` value.
! Error Conditions
If either of the arguments is `Number.nan`, an `Incomparable_Values` error is raised.
Arguments:
- that: The number to check equality against.
- epsilon: The value by which `self` and `that` can be separated by before
@ -288,7 +292,7 @@ type Number
Check if 1 is equal to 1.0000001 within 0.001.
1.equals 1.0000001 epsilon=0.001
equals : Number -> Number -> Boolean
equals : Number -> Number -> Boolean ! Incomparable_Values
equals self that epsilon=0.0 =
(self == that) || ((self - that).abs <= epsilon)

View File

@ -170,7 +170,7 @@ type Ordering
Greater
## Compares to values and returns an Ordering
compare : Any -> Any -> Ordering ! (Incomparable_Values | Type_Error)
compare : Any -> Any -> Ordering ! Incomparable_Values
compare x y =
if x < y then Ordering.Less else
if x == y then Ordering.Equal else

View File

@ -4,7 +4,6 @@ import project.Data.Filter_Condition.Filter_Condition
import project.Data.List.List
import project.Data.Map.Map
import project.Data.Numbers.Integer
import project.Data.Ordering.Ordering
import project.Data.Ordering.Sort_Direction.Sort_Direction
import project.Data.Pair.Pair
import project.Data.Range.Range
@ -17,12 +16,20 @@ import project.Errors.Common.Not_Found
import project.Errors.Common.Type_Error
import project.Error.Error
import project.Errors.Illegal_Argument.Illegal_Argument
import project.Errors.Problem_Behavior.Problem_Behavior
import project.Function.Function
import project.Math
import project.Meta
import project.Nothing.Nothing
import project.Panic.Panic
import project.Random
import project.Warning.Warning
import project.IO
## We have to import also conversion methods, therefore, we import all from the Ordering
module
from project.Data.Ordering import all
from project.Data.Boolean import Boolean, True, False
from project.Data.Index_Sub_Range import Index_Sub_Range, take_helper, drop_helper
@ -841,13 +848,17 @@ type Vector a
Arguments:
- order: The order in which the vector elements are sorted.
- on: A projection from the element type to the value of that element
being sorted on.
being sorted on. If set to `Nothing` (the default),
identity function will be used.
- by: A function that compares the result of applying `on` to two
elements, returning an Ordering to compare them.
elements, returning an an `Ordering` if the two elements are comparable
or `Nothing` if they are not. If set to `Nothing` (the default argument),
`Ordering.compare _ _` method will be used.
- on_incomparable: A `Problem_Behavior` specifying what should happen if
two incomparable values are encountered.
By default, elements are sorted in ascending order.
By default, elements are sorted in ascending order, using the comparator
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.
@ -859,12 +870,32 @@ type Vector a
- *Average Time:* `O(n * log n)`
- *Worst-Case Space:* `O(n)` additional
? Incomparable values
Incomparable values are either values with different comparators or with
the same comparator returning `Nothing` from its `compare` method.
See the documentation of the `Ordering` module for more info.
? Implementation Note
The sort implementation is based upon an adaptive, iterative mergesort
that requires far fewer than `n * log(n)` comparisons when the vector
is partially sorted. When the vector is randomly ordered, the
performance is equivalent to a standard mergesort.
? Multiple comparators
Elements with different comparators are incomparable by definition.
This case is handled by first grouping the `self` vector into groups
with the same comparator, recursively sorting these groups, and then
merging them back together. The order of the sorted groups in the
resulting vector is based on the order of fully qualified names of
the comparators in the `self` vector, with the exception of the group
for the default comparator, which is always the first group.
Additionally, an `Incomparable_Values` dataflow error will be returned
if the `on_incomparable` parameter is set to `Problem_Behavior.Report_Error`,
or a warning attached if the `on_incomparable` parameter is set to
`Problem_Behavior.Report_Warning` in case of encountering incomparable
values.
It takes equal advantage of ascending and descending runs in the array,
making it much simpler to merge two or more sorted arrays: simply
concatenate them and sort.
@ -878,16 +909,20 @@ type Vector a
Sorting a vector of `Pair`s on the first element, descending.
[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 = (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
comp_descending
new_vec_arr = self.to_array.sort_builtin compare
if new_vec_arr.is_error then Error.throw new_vec_arr else
Vector.from_polyglot_array new_vec_arr
> Example
Sorting a vector with elements with different comparators. Values `1`
and `My_Type` have different comparators. `1` will be sorted before `My_Type`
because it has the default comparator.
[My_Type.Value 'hello', 1].sort == [1, My_Type.Value 'hello']
sort : Sort_Direction -> (Any -> Any)|Nothing -> (Any -> Any -> (Ordering|Nothing))|Nothing -> Problem_Behavior -> Vector Any ! Incomparable_Values
sort self (order = Sort_Direction.Ascending) on=Nothing by=Nothing on_incomparable=Problem_Behavior.Ignore =
comps = case on == Nothing of
True -> self.map it-> Comparable.from it
False -> self.map it-> Comparable.from (on it)
compare_funcs = comps.map (it-> it.compare)
self.sort_builtin order.to_sign comps compare_funcs by on on_incomparable.to_number
## UNSTABLE
Keeps only unique elements within the vector, removing any duplicates.

View File

@ -164,3 +164,11 @@ type Problem_Behavior
warnings = Warning.get_all result . map .value
cleared_result = Warning.set result []
self.attach_problems_after cleared_result warnings
## PRIVATE
Returns a mapping of Problem_Behavior constructors to an integer.
Used for sending the number to Java, rather than sending the atom.
to_number self = case self of
Ignore -> 0
Report_Warning -> 1
Report_Error -> 2

View File

@ -3,7 +3,7 @@ from Standard.Base import all
from project import Test
import project.Extensions
## Returns values of warnings attached to the value.Nothing
## Returns values of warnings attached to the value.
get_attached_warnings v =
Warning.get_all v . map .value

View File

@ -9,7 +9,7 @@ public final class HostEnsoUtils {
/**
* Extracts a string representation for a polyglot exception.
*
* @param exexception the exception
* @param ex the exception
* @return message representing the exception
*/
public static String findExceptionMessage(Throwable ex) {

View File

@ -2,6 +2,7 @@ package org.enso.interpreter.node.expression.builtin.meta;
import com.oracle.truffle.api.CompilerDirectives;
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;
@ -26,6 +27,7 @@ import org.enso.interpreter.runtime.number.EnsoBigInteger;
name = "type_of",
description = "Returns the type of a value.",
autoRegister = false)
@GenerateUncached
public abstract class TypeOfNode extends Node {
public abstract Object execute(@AcceptsError Object value);

View File

@ -1,160 +0,0 @@
package org.enso.interpreter.node.expression.builtin.mutable;
import com.oracle.truffle.api.CompilerDirectives;
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.Specialization;
import com.oracle.truffle.api.interop.InteropLibrary;
import com.oracle.truffle.api.interop.InvalidArrayIndexException;
import com.oracle.truffle.api.interop.UnsupportedMessageException;
import com.oracle.truffle.api.library.CachedLibrary;
import com.oracle.truffle.api.nodes.LoopNode;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.profiles.BranchProfile;
import java.util.Arrays;
import java.util.Comparator;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.node.callable.dispatch.CallOptimiserNode;
import org.enso.interpreter.node.callable.dispatch.SimpleCallOptimiserNode;
import org.enso.interpreter.node.expression.builtin.interop.syntax.HostValueToEnsoNode;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.callable.atom.Atom;
import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.data.Array;
import org.enso.interpreter.runtime.error.DataflowError;
import org.enso.interpreter.runtime.error.PanicException;
import org.enso.interpreter.runtime.state.State;
@BuiltinMethod(type = "Array", name = "sort_builtin", description = "Returns a sorted array.")
public abstract class SortNode extends Node {
private @Child CallOptimiserNode callOptimiserNode = SimpleCallOptimiserNode.build();
private @Child InvalidComparisonNode invalidComparisonNode = InvalidComparisonNode.build();
private final BranchProfile invalidCompareResultProfile = BranchProfile.create();
abstract Object execute(State state, Object self, Object comparator);
static SortNode build() {
return SortNodeGen.create();
}
@Specialization
Object doArray(State state, Array self, Function comparator) {
EnsoContext context = EnsoContext.get(this);
int size = self.getItems().length;
Object[] newArr = new Object[size];
System.arraycopy(self.getItems(), 0, newArr, 0, size);
try {
return getComparatorAndSort(state, newArr, comparator, context);
} catch (CompareException e) {
return DataflowError.withoutTrace(
incomparableValuesError(e.leftOperand, e.rightOperand), this);
}
}
@Specialization(guards = "arrays.hasArrayElements(self)")
Object doPolyglotArray(
State state,
Object self,
Function comparator,
@CachedLibrary(limit = "3") InteropLibrary arrays,
@Cached HostValueToEnsoNode hostValueToEnsoNode) {
long size;
Object[] newArray;
try {
size = arrays.getArraySize(self);
newArray = new Object[(int) size];
for (int i = 0; i < size; i++) {
newArray[i] = hostValueToEnsoNode.execute(arrays.readArrayElement(self, i));
}
} catch (UnsupportedMessageException | InvalidArrayIndexException e) {
throw new IllegalStateException(e);
}
try {
return getComparatorAndSort(state, newArray, comparator, EnsoContext.get(this));
} catch (CompareException e) {
return DataflowError.withoutTrace(
incomparableValuesError(e.leftOperand, e.rightOperand), this);
}
}
@Fallback
Object doOther(State state, Object self, Object comparator) {
CompilerDirectives.transferToInterpreter();
var fun = EnsoContext.get(this).getBuiltins().function();
throw new PanicException(
EnsoContext.get(this).getBuiltins().error().makeTypeError(fun, comparator, "comparator"),
this);
}
private Object getComparatorAndSort(
State state, Object[] rawItems, Function comparator, EnsoContext context) {
Comparator<Object> compare = new SortComparator(comparator, context, this, state);
runSort(compare, rawItems);
return new Array(rawItems);
}
Object[] runSort(Comparator<Object> compare, Object[] self) {
doSort(self, compare);
LoopNode.reportLoopCount(this, self.length);
return self;
}
@TruffleBoundary
void doSort(Object[] items, Comparator<Object> compare) {
Arrays.sort(items, compare);
}
private Object incomparableValuesError(Object left, Object right) {
return EnsoContext.get(this).getBuiltins().error().makeIncomparableValues(left, right);
}
private class SortComparator implements Comparator<Object> {
private final Function compFn;
private final EnsoContext context;
private final Atom less;
private final Atom equal;
private final Atom greater;
private final SortNode outerThis;
private final State state;
SortComparator(Function compFn, EnsoContext context, SortNode outerThis, State state) {
this.compFn = compFn;
this.context = context;
this.less = context.getBuiltins().ordering().newLess();
this.equal = context.getBuiltins().ordering().newEqual();
this.greater = context.getBuiltins().ordering().newGreater();
this.outerThis = outerThis;
this.state = state;
}
@Override
public int compare(Object o1, Object o2) {
Object res = callOptimiserNode.executeDispatch(compFn, null, state, new Object[] {o1, o2});
if (res == less) {
return -1;
} else if (res == equal) {
return 0;
} else if (res == greater) {
return 1;
} else {
invalidCompareResultProfile.enter();
throw new CompareException(o1, o2);
}
}
}
private static final class CompareException extends RuntimeException {
final Object leftOperand;
final Object rightOperand;
private CompareException(Object leftOperand, Object rightOperand) {
this.leftOperand = leftOperand;
this.rightOperand = rightOperand;
}
}
}

View File

@ -34,9 +34,9 @@ public abstract class GreaterNode extends Node {
}
@Fallback
DataflowError doOther(EnsoBigInteger self, Object that) {
Object 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);
var incomparableValsErr = builtins.error().makeIncomparableValues(self, that);
return DataflowError.withoutTrace(incomparableValsErr, this);
}
}

View File

@ -34,9 +34,9 @@ public abstract class GreaterOrEqualNode extends Node {
}
@Fallback
DataflowError doOther(EnsoBigInteger self, Object that) {
Object 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);
var incomparableValsErr = builtins.error().makeIncomparableValues(self, that);
return DataflowError.withoutTrace(incomparableValsErr, this);
}
}

View File

@ -34,9 +34,9 @@ public abstract class LessNode extends Node {
}
@Fallback
DataflowError doOther(EnsoBigInteger self, Object that) {
Object 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);
var incomparableValsErr = builtins.error().makeIncomparableValues(self, that);
return DataflowError.withoutTrace(incomparableValsErr, this);
}
}

View File

@ -34,9 +34,9 @@ public abstract class LessOrEqualNode extends Node {
}
@Fallback
DataflowError doOther(EnsoBigInteger self, Object that) {
Object 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);
var incomparableValsErr = builtins.error().makeIncomparableValues(self, that);
return DataflowError.withoutTrace(incomparableValsErr, this);
}
}

View File

@ -6,6 +6,7 @@ import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.node.expression.builtin.number.utils.BigIntegerOps;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.data.Type;
import org.enso.interpreter.runtime.error.DataflowError;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
@ -19,24 +20,40 @@ public abstract class GreaterNode extends Node {
}
@Specialization
boolean doDouble(double self, double that) {
return self > that;
Object doDouble(double self, double that) {
if (Double.isNaN(self) || Double.isNaN(that)) {
return incomparableError(self, that);
} else {
return self > that;
}
}
@Specialization
boolean doLong(double self, long that) {
return self > (double) that;
Object doLong(double self, long that) {
if (Double.isNaN(self)) {
return incomparableError(self, that);
} else {
return self > (double) that;
}
}
@Specialization
boolean doBigInteger(double self, EnsoBigInteger that) {
return self > BigIntegerOps.toDouble(that.getValue());
Object doBigInteger(double self, EnsoBigInteger that) {
if (Double.isNaN(self)) {
return incomparableError(self, that);
} else {
return self > BigIntegerOps.toDouble(that.getValue());
}
}
@Fallback
DataflowError doOther(double self, Object that) {
Object doOther(double self, Object that) {
return incomparableError(self, that);
}
private DataflowError incomparableError(Object self, Object that) {
var builtins = EnsoContext.get(this).getBuiltins();
var typeError = builtins.error().makeTypeError(builtins.number().getNumber(), that, "that");
return DataflowError.withoutTrace(typeError, this);
var incomparableErr = builtins.error().makeIncomparableValues(self, that);
return DataflowError.withoutTrace(incomparableErr, this);
}
}

View File

@ -6,6 +6,7 @@ import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.node.expression.builtin.number.utils.BigIntegerOps;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.data.Type;
import org.enso.interpreter.runtime.error.DataflowError;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
@ -19,24 +20,40 @@ public abstract class GreaterOrEqualNode extends Node {
}
@Specialization
boolean doDouble(double self, double that) {
return self >= that;
Object doDouble(double self, double that) {
if (Double.isNaN(self) || Double.isNaN(that)) {
return incomparableError(self, that);
} else {
return self >= that;
}
}
@Specialization
boolean doLong(double self, long that) {
return self >= (double) that;
Object doLong(double self, long that) {
if (Double.isNaN(self)) {
return incomparableError(self, that);
} else {
return self >= (double) that;
}
}
@Specialization
boolean doBigInteger(double self, EnsoBigInteger that) {
return self >= BigIntegerOps.toDouble(that.getValue());
Object doBigInteger(double self, EnsoBigInteger that) {
if (Double.isNaN(self)) {
return incomparableError(self, that);
} else {
return self >= BigIntegerOps.toDouble(that.getValue());
}
}
@Fallback
DataflowError doOther(double self, Object that) {
Object doOther(double self, Object that) {
return incomparableError(self, that);
}
private DataflowError incomparableError(Object self, Object that) {
var builtins = EnsoContext.get(this).getBuiltins();
var typeError = builtins.error().makeTypeError(builtins.number().getNumber(), that, "that");
return DataflowError.withoutTrace(typeError, this);
var incomparableErr = builtins.error().makeIncomparableValues(self, that);
return DataflowError.withoutTrace(incomparableErr, this);
}
}

View File

@ -6,6 +6,7 @@ import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.node.expression.builtin.number.utils.BigIntegerOps;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.data.Type;
import org.enso.interpreter.runtime.error.DataflowError;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
@ -19,24 +20,40 @@ public abstract class LessNode extends Node {
}
@Specialization
boolean doDouble(double self, double that) {
return self < that;
Object doDouble(double self, double that) {
if (Double.isNaN(self) || Double.isNaN(that)) {
return incomparableError(self, that);
} else {
return self < that;
}
}
@Specialization
boolean doLong(double self, long that) {
return self < (double) that;
Object doLong(double self, long that) {
if (Double.isNaN(self)) {
return incomparableError(self, that);
} else {
return self < (double) that;
}
}
@Specialization
boolean doBigInteger(double self, EnsoBigInteger that) {
return self < BigIntegerOps.toDouble(that.getValue());
Object doBigInteger(double self, EnsoBigInteger that) {
if (Double.isNaN(self)) {
return incomparableError(self, that);
} else {
return self < BigIntegerOps.toDouble(that.getValue());
}
}
@Fallback
DataflowError doOther(double self, Object that) {
Object doOther(double self, Object that) {
return incomparableError(self, that);
}
private DataflowError incomparableError(Object self, Object that) {
var builtins = EnsoContext.get(this).getBuiltins();
var typeError = builtins.error().makeTypeError(builtins.number().getNumber(), that, "that");
return DataflowError.withoutTrace(typeError, this);
var incomparableErr = builtins.error().makeIncomparableValues(self, that);
return DataflowError.withoutTrace(incomparableErr, this);
}
}

View File

@ -6,6 +6,7 @@ import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.node.expression.builtin.number.utils.BigIntegerOps;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.data.Type;
import org.enso.interpreter.runtime.error.DataflowError;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
@ -19,24 +20,40 @@ public abstract class LessOrEqualNode extends Node {
}
@Specialization
boolean doDouble(double self, double that) {
return self <= that;
Object doDouble(double self, double that) {
if (Double.isNaN(self) || Double.isNaN(that)) {
return incomparableError(self, that);
} else {
return self <= that;
}
}
@Specialization
boolean doLong(double self, long that) {
return self <= (double) that;
Object doLong(double self, long that) {
if (Double.isNaN(self)) {
return incomparableError(self, that);
} else {
return self <= (double) that;
}
}
@Specialization
boolean doBigInteger(double self, EnsoBigInteger that) {
return self <= BigIntegerOps.toDouble(that.getValue());
Object doBigInteger(double self, EnsoBigInteger that) {
if (Double.isNaN(self)) {
return incomparableError(self, that);
} else {
return self <= BigIntegerOps.toDouble(that.getValue());
}
}
@Fallback
DataflowError doOther(double self, Object that) {
Object doOther(double self, Object that) {
return incomparableError(self, that);
}
private DataflowError incomparableError(Object self, Object that) {
var builtins = EnsoContext.get(this).getBuiltins();
var typeError = builtins.error().makeTypeError(builtins.number().getNumber(), that, "that");
return DataflowError.withoutTrace(typeError, this);
var incomparableErr = builtins.error().makeIncomparableValues(self, that);
return DataflowError.withoutTrace(incomparableErr, this);
}
}

View File

@ -33,9 +33,9 @@ public abstract class GreaterNode extends Node {
}
@Fallback
DataflowError doOther(long self, Object that) {
Object 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);
var incomparableValsErr = builtins.error().makeIncomparableValues(self, that);
return DataflowError.withoutTrace(incomparableValsErr, this);
}
}

View File

@ -33,9 +33,9 @@ public abstract class GreaterOrEqualNode extends Node {
}
@Fallback
DataflowError doOther(long self, Object that) {
Object 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);
var incomparableValsErr = builtins.error().makeIncomparableValues(self, that);
return DataflowError.withoutTrace(incomparableValsErr, this);
}
}

View File

@ -33,9 +33,9 @@ public abstract class LessNode extends Node {
}
@Fallback
DataflowError doOther(long self, Object that) {
Object 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);
var incomparableValsErr = builtins.error().makeIncomparableValues(self, that);
return DataflowError.withoutTrace(incomparableValsErr, this);
}
}

View File

@ -33,9 +33,9 @@ public abstract class LessOrEqualNode extends Node {
}
@Fallback
DataflowError doOther(long self, Object that) {
Object 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);
var incomparableValsErr = builtins.error().makeIncomparableValues(self, that);
return DataflowError.withoutTrace(incomparableValsErr, this);
}
}

View File

@ -116,21 +116,13 @@ public abstract class LessThanNode extends Node {
@Specialization
@TruffleBoundary
boolean lessLongBigInt(long self, EnsoBigInteger other) {
if (BigIntegerOps.fitsInLong(other.getValue())) {
return BigInteger.valueOf(self).compareTo(other.getValue()) < 0;
} else {
return true;
}
return BigInteger.valueOf(self).compareTo(other.getValue()) < 0;
}
@Specialization
@TruffleBoundary
boolean lessBigIntLong(EnsoBigInteger self, long other) {
if (BigIntegerOps.fitsInLong(self.getValue())) {
return self.getValue().compareTo(BigInteger.valueOf(other)) < 0;
} else {
return false;
}
return self.getValue().compareTo(BigInteger.valueOf(other)) < 0;
}
/**

View File

@ -0,0 +1,29 @@
package org.enso.interpreter.node.expression.builtin.ordering;
import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.dsl.AcceptsError;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.runtime.state.State;
/** Just a wrapper for {@code Array.sort_builtin} that delegates to {@code Vector.sort_builtin}. */
@BuiltinMethod(type = "Array", name = "sort_builtin")
public final class SortArrayNode extends Node {
@Child private SortVectorNode sortVectorNode = SortVectorNode.build();
public Object execute(
State state,
@AcceptsError Object self,
long ascending,
Object comparators,
Object compareFunctions,
Object byFunc,
Object onFunc,
long problemBehavior) {
return sortVectorNode.execute(
state, self, ascending, comparators, compareFunctions, byFunc, onFunc, problemBehavior);
}
public static SortArrayNode build() {
return new SortArrayNode();
}
}

View File

@ -0,0 +1,804 @@
package org.enso.interpreter.node.expression.builtin.ordering;
import com.oracle.truffle.api.CompilerDirectives;
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.InvalidArrayIndexException;
import com.oracle.truffle.api.interop.UnsupportedMessageException;
import com.oracle.truffle.api.library.CachedLibrary;
import com.oracle.truffle.api.nodes.Node;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
import org.enso.interpreter.dsl.AcceptsError;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.node.callable.dispatch.CallOptimiserNode;
import org.enso.interpreter.node.expression.builtin.interop.syntax.HostValueToEnsoNode;
import org.enso.interpreter.node.expression.builtin.meta.EqualsNode;
import org.enso.interpreter.node.expression.builtin.meta.TypeOfNode;
import org.enso.interpreter.node.expression.builtin.text.AnyToTextNode;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.callable.atom.Atom;
import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.data.Array;
import org.enso.interpreter.runtime.data.ArrayRope;
import org.enso.interpreter.runtime.data.Type;
import org.enso.interpreter.runtime.data.Vector;
import org.enso.interpreter.runtime.data.text.Text;
import org.enso.interpreter.runtime.error.DataflowError;
import org.enso.interpreter.runtime.error.PanicException;
import org.enso.interpreter.runtime.error.Warning;
import org.enso.interpreter.runtime.error.WarningsLibrary;
import org.enso.interpreter.runtime.error.WithWarnings;
import org.enso.interpreter.runtime.state.State;
/**
* Sorts a vector with elements that have only Default_Comparator, thus, only elements with a
* builtin type, which is the most common scenario for sorting.
*/
@BuiltinMethod(type = "Vector", name = "sort_builtin", description = "Returns a sorted vector.")
@GenerateUncached
public abstract class SortVectorNode extends Node {
public static SortVectorNode build() {
return SortVectorNodeGen.create();
}
/**
* Sorts a vector with elements that have only Default_Comparator, thus, only builtin types.
*
* @param self Vector that has elements with only Default_Comparator, that are elements with
* builtin types.
* @param ascending -1 for descending, 1 for ascending
* @param comparators Vector of comparators, with the same length of self. This is gather in the
* Enso code, because doing that in this builtin would be difficult. If {@code onFunc}
* parameter is not {@code Nothing}, comparators are gathered from the result of {@code
* onFunc} projection.
* @param compareFunctions Vector of `Comparator.compare` functions gathered from the comparators
* @param byFunc If Nothing, then the default `by` function should be used. The default `by`
* function is `Ordering.compare`.
* @param onFunc If Nothing, then the default identity function should be used.
* @param problemBehavior A long representation of `Problem_Behavior`. Ignore is 0, Report_warning
* is 1, and Report_Error is 2.
* @return A new, sorted vector.
*/
public abstract Object execute(
State state,
@AcceptsError Object self,
long ascending,
Object comparators,
Object compareFunctions,
Object byFunc,
Object onFunc,
long problemBehavior);
/**
* Sorts primitive values, i.e., values with only Default_Comparator. We can optimize this case.
* It is important that `byFunc` is Nothing, i.e., has the default value. In that case, we can
* hard code the partial ordering for the primitive values. If `byFunc` is a custom user function,
* it can redefine the default partial ordering of the primitive values, which requires
* topological sort.
*/
@Specialization(
guards = {
"interop.hasArrayElements(self)",
"areAllDefaultComparators(interop, hostValueToEnsoNode, comparators)",
"interop.isNull(byFunc)",
"interop.isNull(onFunc)"
})
Object sortPrimitives(
State state,
Object self,
long ascending,
Object comparators,
Object compareFunctions,
Object byFunc,
Object onFunc,
long problemBehavior,
@Cached LessThanNode lessThanNode,
@Cached EqualsNode equalsNode,
@Cached HostValueToEnsoNode hostValueToEnsoNode,
@Cached TypeOfNode typeOfNode,
@Cached AnyToTextNode toTextNode,
@CachedLibrary(limit = "10") InteropLibrary interop) {
EnsoContext ctx = EnsoContext.get(this);
Object[] elems;
try {
long size = interop.getArraySize(self);
assert size < Integer.MAX_VALUE;
elems = new Object[(int) size];
for (int i = 0; i < size; i++) {
if (interop.isArrayElementReadable(self, i)) {
elems[i] = hostValueToEnsoNode.execute(interop.readArrayElement(self, i));
} else {
CompilerDirectives.transferToInterpreter();
throw new PanicException(
ctx.getBuiltins()
.error()
.makeUnsupportedArgumentsError(
new Object[] {self},
"Cannot read array element at index " + i + " of " + self),
this);
}
}
} catch (UnsupportedMessageException | InvalidArrayIndexException e) {
throw new IllegalStateException("Should not reach here", e);
}
var javaComparator =
createDefaultComparator(
lessThanNode, equalsNode, typeOfNode, toTextNode, ascending, problemBehavior, interop);
try {
return sortPrimitiveVector(elems, javaComparator);
} catch (CompareException e) {
return DataflowError.withoutTrace(
incomparableValuesError(e.leftOperand, e.rightOperand), this);
}
}
@TruffleBoundary
private DefaultSortComparator createDefaultComparator(
LessThanNode lessThanNode,
EqualsNode equalsNode,
TypeOfNode typeOfNode,
AnyToTextNode toTextNode,
long ascending,
long problemBehaviorNum,
InteropLibrary interop) {
return new DefaultSortComparator(
lessThanNode,
equalsNode,
typeOfNode,
toTextNode,
ascending > 0,
ProblemBehavior.fromInt((int) problemBehaviorNum),
interop);
}
@TruffleBoundary
@Specialization(
guards = {
"interop.hasArrayElements(self)",
})
Object sortGeneric(
State state,
Object self,
long ascending,
Object comparatorsArray,
Object compareFuncsArray,
Object byFunc,
Object onFunc,
long problemBehaviorNum,
@CachedLibrary(limit = "10") InteropLibrary interop,
@CachedLibrary(limit = "5") WarningsLibrary warningsLib,
@Cached LessThanNode lessThanNode,
@Cached EqualsNode equalsNode,
@Cached TypeOfNode typeOfNode,
@Cached AnyToTextNode toTextNode,
@Cached(value = "build()", uncached = "build()") HostValueToEnsoNode hostValueToEnsoNode,
@Cached(value = "build()", uncached = "build()") CallOptimiserNode callNode) {
var problemBehavior = ProblemBehavior.fromInt((int) problemBehaviorNum);
// Split into groups
List<Object> elems = readInteropArray(interop, hostValueToEnsoNode, warningsLib, self);
List<Type> comparators =
readInteropArray(interop, hostValueToEnsoNode, warningsLib, comparatorsArray);
List<Function> compareFuncs =
readInteropArray(interop, hostValueToEnsoNode, warningsLib, compareFuncsArray);
List<Group> groups = splitByComparators(elems, comparators, compareFuncs);
// Prepare input for DefaultSortComparator and GenericSortComparator and sort the elements
// within groups
var ctx = EnsoContext.get(this);
Atom less = ctx.getBuiltins().ordering().newLess();
Atom equal = ctx.getBuiltins().ordering().newEqual();
Atom greater = ctx.getBuiltins().ordering().newGreater();
Set<String> gatheredWarnings = new HashSet<>();
List<Object> resultVec = new ArrayList<>();
try {
for (var group : groups) {
SortComparator javaComparator;
if (interop.isNull(byFunc) && interop.isNull(onFunc) && isPrimitiveGroup(group)) {
javaComparator =
new DefaultSortComparator(
lessThanNode,
equalsNode,
typeOfNode,
toTextNode,
ascending > 0,
problemBehavior,
interop);
} else {
Object compareFunc = interop.isNull(byFunc) ? group.compareFunc : byFunc;
javaComparator =
new GenericSortComparator(
ascending > 0,
compareFunc,
onFunc,
problemBehavior,
group.comparator,
callNode,
toTextNode,
state,
less,
equal,
greater,
interop);
}
group.elems.sort(javaComparator);
if (javaComparator.hasWarnings()) {
gatheredWarnings.addAll(javaComparator.getEncounteredWarnings());
}
resultVec.addAll(group.elems);
}
var sortedVector = Vector.fromArray(new Array(resultVec.toArray()));
// Attach gathered warnings along with different comparators warning
switch (problemBehavior) {
case REPORT_ERROR -> {
if (groups.size() > 1) {
assert groups.get(0).elems.size() > 0 : "Groups should not be empty";
assert groups.get(1).elems.size() > 0 : "Groups should not be empty";
var firstIncomparableElem = groups.get(0).elems.get(0);
var secondIncomparableElem = groups.get(1).elems.get(0);
var err =
ctx.getBuiltins()
.error()
.makeIncomparableValues(firstIncomparableElem, secondIncomparableElem);
return DataflowError.withoutTrace(err, this);
} else {
// Just one comparator, different from Default_Comparator
if (!gatheredWarnings.isEmpty()) {
throw new UnsupportedOperationException("unimplemented");
} else {
return sortedVector;
}
}
}
case REPORT_WARNING -> {
return attachDifferentComparatorsWarning(
attachWarnings(sortedVector, gatheredWarnings), groups);
}
case IGNORE -> {
return sortedVector;
}
default -> throw new IllegalStateException("unreachable");
}
} catch (CompareException e) {
return DataflowError.withoutTrace(
incomparableValuesError(e.leftOperand, e.rightOperand), this);
}
}
@TruffleBoundary(allowInlining = true)
private Object sortPrimitiveVector(Object[] elems, DefaultSortComparator javaComparator)
throws CompareException {
Arrays.sort(elems, javaComparator);
var sortedVector = Vector.fromArray(new Array(elems));
if (javaComparator.hasWarnings()) {
return attachWarnings(sortedVector, javaComparator.getEncounteredWarnings());
} else {
return sortedVector;
}
}
private List<Group> splitByComparators(
List<Object> elements, List<Type> comparators, List<Function> compareFuncs) {
assert elements.size() == comparators.size();
assert elements.size() == compareFuncs.size();
// Mapping of FQN of comparator to groups
Map<String, Group> groupMap = new HashMap<>();
for (int i = 0; i < elements.size(); i++) {
Object elem = elements.get(i);
Type comparator = comparators.get(i);
Function compareFunc = compareFuncs.get(i);
String qualifiedName = comparator.getQualifiedName().toString();
if (!groupMap.containsKey(qualifiedName)) {
groupMap.put(qualifiedName, new Group(new ArrayList<>(), comparator, compareFunc));
}
var group = groupMap.get(qualifiedName);
group.elems.add(elem);
}
// Sort groups by the FQN of their comparator, with the default comparator
// being the first one (the first group).
String defCompFQN = getDefaultComparatorQualifiedName();
return groupMap.entrySet().stream()
.sorted(
(entry1, entry2) -> {
var fqn1 = entry1.getKey();
var fqn2 = entry2.getKey();
if (fqn1.equals(defCompFQN)) {
return -1;
} else if (fqn2.equals(defCompFQN)) {
return 1;
} else {
return fqn1.compareTo(fqn2);
}
})
.map(Entry::getValue)
.collect(Collectors.toList());
}
private Object attachWarnings(Object vector, Set<String> warnings) {
var warnArray =
warnings.stream()
.map(Text::create)
.map(text -> Warning.create(EnsoContext.get(this), text, this))
.toArray(Warning[]::new);
return WithWarnings.appendTo(vector, new ArrayRope<>(warnArray));
}
private Object attachDifferentComparatorsWarning(Object vector, List<Group> groups) {
var diffCompsMsg =
groups.stream()
.map(Group::comparator)
.map(comparator -> comparator.getQualifiedName().toString())
.collect(Collectors.joining(", "));
var text = Text.create("Different comparators: [" + diffCompsMsg + "]");
var warn = Warning.create(EnsoContext.get(this), text, this);
return WithWarnings.appendTo(vector, new ArrayRope<>(warn));
}
private String getDefaultComparatorQualifiedName() {
return EnsoContext.get(this)
.getBuiltins()
.defaultComparator()
.getType()
.getQualifiedName()
.toString();
}
/** A group is "primitive" iff its comparator is the default comparator. */
private boolean isPrimitiveGroup(Group group) {
return group
.comparator
.getQualifiedName()
.toString()
.equals(getDefaultComparatorQualifiedName());
}
private Object incomparableValuesError(Object left, Object right) {
return EnsoContext.get(this).getBuiltins().error().makeIncomparableValues(left, right);
}
/**
* Helper slow-path method to conveniently gather elements from interop arrays into a java list
*/
@SuppressWarnings("unchecked")
private <T> List<T> readInteropArray(
InteropLibrary interop,
HostValueToEnsoNode hostValueToEnsoNode,
WarningsLibrary warningsLib,
Object vector) {
try {
int size = (int) interop.getArraySize(vector);
List<T> res = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
Object elem = hostValueToEnsoNode.execute(interop.readArrayElement(vector, i));
if (warningsLib.hasWarnings(elem)) {
elem = warningsLib.removeWarnings(elem);
}
res.add((T) elem);
}
return res;
} catch (UnsupportedMessageException | InvalidArrayIndexException | ClassCastException e) {
throw new IllegalStateException("Should not be reachable", e);
}
}
private int getBuiltinTypeCost(Object builtinType) {
assert isBuiltinType(builtinType);
var builtins = EnsoContext.get(this).getBuiltins();
if (builtinType == builtins.number().getNumber()
|| builtinType == builtins.number().getInteger()
|| builtinType == builtins.number().getDecimal()) {
return 1;
} else if (builtinType == builtins.text()) {
return 2;
} else if (builtinType == builtins.bool().getType()) {
return 3;
} else if (builtinType == builtins.date()) {
return 4;
} else if (builtinType == builtins.dateTime()) {
return 5;
} else if (builtinType == builtins.duration()) {
return 6;
} else if (builtinType == builtins.vector()) {
// vectors are incomparable, but we want to sort them before Nothings and NaNs.
return 50;
} else {
// Type is not a builtin type
throw new IllegalStateException("Should be a builtin type: " + builtinType);
}
}
private boolean isBuiltinType(Object type) {
var builtins = EnsoContext.get(this).getBuiltins();
return builtins.number().getNumber() == type
|| builtins.number().getDecimal() == type
|| builtins.number().getInteger() == type
|| builtins.nothing() == type
|| builtins.text() == type
|| builtins.bool().getType() == type
|| builtins.date() == type
|| builtins.dateTime() == type
|| builtins.duration() == type
|| builtins.vector() == type;
}
private boolean isTrue(Object object) {
return Boolean.TRUE.equals(object);
}
/** Returns true iff the given array of comparators is all Default_Comparator */
boolean areAllDefaultComparators(
InteropLibrary interop, HostValueToEnsoNode hostValueToEnsoNode, Object comparators) {
assert interop.hasArrayElements(comparators);
var ctx = EnsoContext.get(this);
try {
int compSize = (int) interop.getArraySize(comparators);
for (int i = 0; i < compSize; i++) {
assert interop.isArrayElementReadable(comparators, i);
Object comparator = hostValueToEnsoNode.execute(interop.readArrayElement(comparators, i));
if (!isDefaultComparator(comparator, ctx)) {
return false;
}
}
} catch (UnsupportedMessageException | InvalidArrayIndexException e) {
throw new IllegalStateException("Should not be reachable", e);
}
return true;
}
boolean isDefaultComparator(Object object, EnsoContext ctx) {
return ctx.getBuiltins().defaultComparator().getType() == object;
}
private boolean isNan(Object object) {
return object instanceof Double dbl && dbl.isNaN();
}
private enum ProblemBehavior {
IGNORE,
REPORT_WARNING,
REPORT_ERROR;
static ProblemBehavior fromInt(int intValue) {
return switch (intValue) {
case 0 -> IGNORE;
case 1 -> REPORT_WARNING;
case 2 -> REPORT_ERROR;
default -> throw new IllegalArgumentException(Integer.toString(intValue));
};
}
}
/**
* Group of elements grouped by comparator.
*
* @param elems Elements of the group.
* @param comparator SortComparator for the elems, i.e., it should hold that {@code elems.each
* it-> (Comparable.from it) == comparator}.
* @param compareFunc `SortComparator.compare` function extracted from the comparator.
*/
private record Group(List<Object> elems, Type comparator, Function compareFunc) {}
/**
* Convenient base class that implements java.util.Comparator, and gathers warnings about
* incomparable values. The warnings are gathered as pure Strings in a hash set, so that they are
* not duplicated.
*/
private abstract static class SortComparator implements java.util.Comparator<Object> {
private final Set<String> warnings = new HashSet<>();
final AnyToTextNode toTextNode;
final ProblemBehavior problemBehavior;
final InteropLibrary interop;
protected SortComparator(
AnyToTextNode toTextNode, ProblemBehavior problemBehavior, InteropLibrary interop) {
this.toTextNode = toTextNode;
this.problemBehavior = problemBehavior;
this.interop = interop;
}
@TruffleBoundary
protected void attachIncomparableValuesWarning(Object x, Object y) {
var xStr = toTextNode.execute(x).toString();
var yStr = toTextNode.execute(y).toString();
String warnText = "Values " + xStr + " and " + yStr + " are incomparable";
warnings.add(warnText);
}
public Set<String> getEncounteredWarnings() {
return warnings;
}
@TruffleBoundary
public boolean hasWarnings() {
return !warnings.isEmpty();
}
}
/**
* SortComparator for comparing all values that have Default_Comparator. These are either
* primitive types, or the types that do not provide their own comparator.
*
* <p>Note that it is essential for this class that the {@code by} method parameter to {@code
* Vector.sort} is set to the default value, which is {@code Ordering.compare}, because then, we
* know that the partial ordering for primitive types was not redefined by the user (we handle
* partial ordering for primitive types specifically, partial ordering for other types is not
* implemented yet - that requires topological sorting).
*/
final class DefaultSortComparator extends SortComparator {
private final LessThanNode lessThanNode;
private final EqualsNode equalsNode;
private final TypeOfNode typeOfNode;
private final boolean ascending;
private DefaultSortComparator(
LessThanNode lessThanNode,
EqualsNode equalsNode,
TypeOfNode typeOfNode,
AnyToTextNode toTextNode,
boolean ascending,
ProblemBehavior problemBehavior,
InteropLibrary interop) {
super(toTextNode, problemBehavior, interop);
this.lessThanNode = lessThanNode;
this.equalsNode = equalsNode;
this.typeOfNode = typeOfNode;
this.ascending = ascending;
}
@Override
public int compare(Object x, Object y) {
return compareValuesWithDefaultComparator(x, y);
}
int compareValuesWithDefaultComparator(Object x, Object y) {
if (equalsNode.execute(x, y)) {
return 0;
} else {
// Check if x < y
Object xLessThanYRes = lessThanNode.execute(x, y);
if (interop.isNull(xLessThanYRes)) {
// x and y are incomparable - this can happen if x and y are different types
return handleIncomparableValues(x, y);
} else if (isTrue(xLessThanYRes)) {
return ascending ? -1 : 1;
} else {
// Check if x > y
Object yLessThanXRes = lessThanNode.execute(y, x);
if (isTrue(yLessThanXRes)) {
return ascending ? 1 : -1;
} else {
// yLessThanXRes is either Nothing or False
return handleIncomparableValues(y, x);
}
}
}
}
private int handleIncomparableValues(Object x, Object y) {
switch (problemBehavior) {
case REPORT_ERROR -> throw new CompareException(x, y);
case REPORT_WARNING -> attachIncomparableValuesWarning(x, y);
}
if (isPrimitiveValue(x) || isPrimitiveValue(y)) {
if (isPrimitiveValue(x) && isPrimitiveValue(y)) {
return handleIncomparablePrimitives(x, y);
} else if (isPrimitiveValue(x)) {
// Primitive values are always before non-primitive values - Default_Comparator
// group should be the first one.
return ascending ? -1 : 1;
} else if (isPrimitiveValue(y)) {
return ascending ? 1 : -1;
} else {
throw new IllegalStateException("Should not be reachable");
}
} else {
// Values other than primitives are compared just by their type's FQN.
var xTypeName = getQualifiedTypeName(x);
var yTypeName = getQualifiedTypeName(y);
return xTypeName.compareTo(yTypeName);
}
}
/**
* Incomparable primitive values have either different builtin types, or are Nothing or NaN. All
* these cases are handled specifically - we hardcode the order of these incomparable values.
*/
private int handleIncomparablePrimitives(Object x, Object y) {
int xCost = getPrimitiveValueCost(x);
int yCost = getPrimitiveValueCost(y);
int res = Integer.compare(xCost, yCost);
return ascending ? res : -res;
}
private boolean isPrimitiveValue(Object object) {
return isBuiltinType(typeOfNode.execute(object));
}
private String getQualifiedTypeName(Object object) {
var typeObj = typeOfNode.execute(object);
return toTextNode.execute(typeObj).toString();
}
private int getPrimitiveValueCost(Object object) {
if (interop.isNull(object)) {
return 200;
} else if (isNan(object)) {
return 100;
} else {
var type = typeOfNode.execute(object);
return getBuiltinTypeCost(type);
}
}
}
/**
* Comparator for any values. This comparator compares the values by calling back to Enso (by
* {@link #compareFunc}), rather than using compare nodes (i.e. {@link LessThanNode}). directly,
* as opposed to {@link DefaultSortComparator}.
*/
private final class GenericSortComparator extends SortComparator {
private final boolean ascending;
/**
* Either function from `by` parameter to the `Vector.sort` method, or the `compare` function
* extracted from the comparator for the appropriate group.
*/
private final Function compareFunc;
private final Function onFunc;
private final boolean hasCustomOnFunc;
private final Type comparator;
private final CallOptimiserNode callNode;
private final State state;
private final Atom less;
private final Atom equal;
private final Atom greater;
private GenericSortComparator(
boolean ascending,
Object compareFunc,
Object onFunc,
ProblemBehavior problemBehavior,
Type comparator,
CallOptimiserNode callNode,
AnyToTextNode toTextNode,
State state,
Atom less,
Atom equal,
Atom greater,
InteropLibrary interop) {
super(toTextNode, problemBehavior, interop);
assert compareFunc != null;
assert comparator != null;
this.comparator = comparator;
this.state = state;
this.ascending = ascending;
this.compareFunc = checkAndConvertByFunc(compareFunc);
if (interop.isNull(onFunc)) {
this.hasCustomOnFunc = false;
this.onFunc = null;
} else {
this.hasCustomOnFunc = true;
this.onFunc = checkAndConvertOnFunc(onFunc);
}
this.callNode = callNode;
this.less = less;
this.equal = equal;
this.greater = greater;
}
@Override
public int compare(Object x, Object y) {
Object xConverted;
Object yConverted;
if (hasCustomOnFunc) {
// onFunc cannot have `self` argument, we assume it has just one argument.
xConverted = callNode.executeDispatch(onFunc, null, state, new Object[] {x});
yConverted = callNode.executeDispatch(onFunc, null, state, new Object[] {y});
} else {
xConverted = x;
yConverted = y;
}
Object[] args;
if (hasFunctionSelfArgument(compareFunc)) {
args = new Object[] {comparator, xConverted, yConverted};
} else {
args = new Object[] {xConverted, yConverted};
}
Object res = callNode.executeDispatch(compareFunc, null, state, args);
if (res == less) {
return ascending ? -1 : 1;
} else if (res == equal) {
return 0;
} else if (res == greater) {
return ascending ? 1 : -1;
} else {
// res is either Nothing, or Incomparable_Values. Either way, it means that x and y are
// incomparable.
// This case is not supported yet, as it requires topological sorting.
// We cannot detect if the result was actually returned from the default comparator (it
// could have been transitively called), so we just bailout.
throw new CompareException(x, y);
}
}
private boolean hasFunctionSelfArgument(Function function) {
if (function.getSchema().getArgumentsCount() > 0) {
return function.getSchema().getArgumentInfos()[0].getName().equals("self");
} else {
return false;
}
}
/**
* Checks value given for {@code by} parameter and converts it to {@link Function}. Throw a
* dataflow error otherwise.
*/
private Function checkAndConvertByFunc(Object byFuncObj) {
return checkAndConvertFunction(
byFuncObj, "Unsupported argument for `by`, expected a method with two arguments", 2, 3);
}
/**
* Checks the value given for {@code on} parameter and converts it to {@link Function}. Throws a
* dataflow error otherwise.
*/
private Function checkAndConvertOnFunc(Object onFuncObj) {
return checkAndConvertFunction(
onFuncObj, "Unsupported argument for `on`, expected a method with one argument", 1, 1);
}
/**
* @param minArgCount Minimal count of arguments without a default value.
* @param maxArgCount Maximal count of argument without a default value.
*/
private Function checkAndConvertFunction(
Object funcObj, String errMsg, int minArgCount, int maxArgCount) {
var err = new IllegalArgumentException(errMsg + ", got " + funcObj);
if (funcObj instanceof Function func) {
var argCount = getNumberOfNonDefaultArguments(func);
if (minArgCount <= argCount && argCount <= maxArgCount) {
return func;
} else {
throw err;
}
} else {
throw err;
}
}
}
private static int getNumberOfNonDefaultArguments(Function function) {
return (int)
Arrays.stream(function.getSchema().getArgumentInfos())
.filter(argInfo -> !argInfo.hasDefaultValue())
.count();
}
private static final class CompareException extends RuntimeException {
final Object leftOperand;
final Object rightOperand;
private CompareException(Object leftOperand, Object rightOperand) {
this.leftOperand = leftOperand;
this.rightOperand = rightOperand;
}
}
}

View File

@ -2,6 +2,7 @@ package org.enso.interpreter.node.expression.builtin.text;
import com.oracle.truffle.api.CompilerDirectives;
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;
@ -14,12 +15,8 @@ import org.enso.interpreter.runtime.callable.atom.StructsLibrary;
import org.enso.interpreter.runtime.data.text.Text;
@BuiltinMethod(type = "Any", name = "to_text", description = "Generic text conversion.")
@GenerateUncached
public abstract class AnyToTextNode extends Node {
private static final int DISPATCH_CACHE = 3;
private @Child InteropLibrary displays =
InteropLibrary.getFactory().createDispatched(DISPATCH_CACHE);
private @Child InteropLibrary strings =
InteropLibrary.getFactory().createDispatched(DISPATCH_CACHE);
public static AnyToTextNode build() {
return AnyToTextNodeGen.create();
@ -28,19 +25,22 @@ public abstract class AnyToTextNode extends Node {
public abstract Text execute(Object self);
@Specialization
Text doAtom(Atom at, @CachedLibrary(limit = "10") StructsLibrary structs) {
Text doAtom(
Atom at,
@CachedLibrary(limit = "10") StructsLibrary structs,
@CachedLibrary(limit = "10") InteropLibrary interop) {
var fields = structs.getFields(at);
if (fields.length == 0) {
return consName(at.getConstructor());
} else {
return doComplexAtom(at, fields);
return doComplexAtom(at, fields, interop);
}
}
@Fallback
Text doOther(Object object) {
Text doOther(Object object, @CachedLibrary(limit = "5") InteropLibrary interop) {
try {
return Text.create(showObject(object));
return Text.create(showObject(object, interop));
} catch (UnsupportedMessageException e) {
CompilerDirectives.transferToInterpreter();
return Text.create(object.toString());
@ -53,18 +53,18 @@ public abstract class AnyToTextNode extends Node {
}
@CompilerDirectives.TruffleBoundary
private Text doComplexAtom(Atom atom, Object[] fields) {
private Text doComplexAtom(Atom atom, Object[] fields, InteropLibrary interop) {
Text res = Text.create("(", consName(atom.getConstructor()));
res = Text.create(res, " ");
try {
res = Text.create(res, showObject(fields[0]));
res = Text.create(res, showObject(fields[0], interop));
} catch (UnsupportedMessageException e) {
res = Text.create(res, fields[0].toString());
}
for (int i = 1; i < fields.length; i++) {
res = Text.create(res, " ");
try {
res = Text.create(res, showObject(fields[i]));
res = Text.create(res, showObject(fields[i], interop));
} catch (UnsupportedMessageException e) {
res = Text.create(res, fields[i].toString());
}
@ -74,7 +74,8 @@ public abstract class AnyToTextNode extends Node {
}
@CompilerDirectives.TruffleBoundary
private String showObject(Object child) throws UnsupportedMessageException {
private String showObject(Object child, InteropLibrary interop)
throws UnsupportedMessageException {
if (child == null) {
// TODO [RW] This is a temporary workaround to make it possible to display errors related to
// https://www.pivotaltracker.com/story/show/181652974
@ -83,7 +84,7 @@ public abstract class AnyToTextNode extends Node {
} else if (child instanceof Boolean) {
return (boolean) child ? "True" : "False";
} else {
return strings.asString(displays.toDisplayString(child));
return interop.asString(interop.toDisplayString(child));
}
}
}

View File

@ -113,6 +113,8 @@ public class DebuggingEnsoTest {
@Test
public void recursiveFactorialCall() {
final Value facFn = createEnsoMethod("""
from Standard.Base.Data.Ordering import all
fac : Number -> Number
fac n =
facacc : Number -> Number -> Number

View File

@ -116,6 +116,17 @@ public abstract class TestBase {
return mainMethod.execute();
}
/**
* Parses the given module and returns a method by the given name from the module.
*
* @param moduleSrc Source of the whole module
* @return Reference to the method.
*/
protected static Value getMethodFromModule(Context ctx, String moduleSrc, String methodName) {
Value module = ctx.eval(Source.create("enso", moduleSrc));
return module.invokeMember(Module.EVAL_EXPRESSION, methodName);
}
/**
* 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

@ -301,10 +301,17 @@ class ValuesGenerator {
collect.add(v(null, "", "42").type());
collect.add(v(null, "", "6.7").type());
collect.add(v(null, "", "40321 * 43202").type());
collect.add(v(null, """
collect.add(
v(
null,
"""
from Standard.Base.Data.Ordering import all
fac s n = if n <= 1 then s else
@Tail_Call fac n*s n-1
""", "fac 1 100").type());
""",
"fac 1 100")
.type());
collect.add(v(null, "", "123 * 10^40").type());
collect.add(v(null, "", "123 * 10^40 + 0.0").type());
collect.add(v(null, "", "123 * 10^40 + 1.0").type());

View File

@ -0,0 +1,100 @@
package org.enso.interpreter.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.List;
import org.enso.interpreter.test.ValuesGenerator.Language;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.junit.AfterClass;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
/**
* This test ensures that any combination of values can be sorted - no attempt to sort should fail.
*/
@RunWith(Theories.class)
public class VectorSortTest extends TestBase {
private static Context context;
private static Value sortFunc;
private static Value equalsFunc;
@BeforeClass
public static void initCtxAndNodes() {
context = createDefaultContext();
var code =
"""
from Standard.Base import all
sort val1 val2 = [val1, val2].sort
equals val1 val2 = val1 == val2
""";
sortFunc = getMethodFromModule(context, code, "sort");
equalsFunc = getMethodFromModule(context, code, "equals");
values = new ArrayList<>();
var valuesGenerator =
ValuesGenerator.create(context, Language.ENSO, Language.JAVA, Language.JAVASCRIPT);
values.addAll(valuesGenerator.numbers());
values.addAll(valuesGenerator.vectors());
values.addAll(valuesGenerator.arrayLike());
values.addAll(valuesGenerator.booleans());
values.addAll(valuesGenerator.durations());
values.addAll(valuesGenerator.maps());
}
@AfterClass
public static void disposeCtx() {
context.close();
}
@DataPoints public static List<Value> values;
@Theory
public void testSortHandlesAllValues(Value value1, Value value2) {
Assume.assumeFalse(isNan(value1) || isNan(value2));
Value res = sortFunc.execute(value1, value2);
assertTrue(res.hasArrayElements());
assertEquals(2, res.getArraySize());
List<Value> resArray = readPolyglotArray(res);
// check that value1 is there unchanged on some index, and the same for value2
assertTrue(
"Sorted vector should contain the first value at any index",
invokeEquals(value1, resArray.get(0)) || invokeEquals(value1, resArray.get(1)));
assertTrue(
"Sorted vector should contain the second value at any index",
invokeEquals(value2, resArray.get(0)) || invokeEquals(value2, resArray.get(1)));
}
private boolean isNan(Value value) {
if (value.isNumber() && value.fitsInDouble()) {
return Double.isNaN(value.asDouble());
} else {
return false;
}
}
private List<Value> readPolyglotArray(Value array) {
assertTrue(array.hasArrayElements());
assertTrue(array.hasIterator());
Value iterator = array.getIterator();
assertTrue(iterator.isIterator());
List<Value> res = new ArrayList<>();
while (iterator.hasIteratorNextElement()) {
res.add(iterator.getIteratorNextElement());
}
return res;
}
private boolean invokeEquals(Value val1, Value val2) {
Value res = equalsFunc.execute(val1, val2);
assertTrue("Result from Any.== should be boolean", res.isBoolean());
return res.asBoolean();
}
}

View File

@ -472,7 +472,7 @@ spec =
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 = 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]
r.to_vector.should_equal [Nothing, My.Data 1 2, My.Data 3 4, My.Data 1 0, My.Data 2 5, My.Data 6 3]
d = Column.from_vector 'foo' [1,3,2,4,Nothing,5]
cmp2 a b = Ordering.compare -a -b

View File

@ -52,10 +52,7 @@ spec =
Test.group "Compare functionality with Vector" <|
Test.specify "compare methods" <|
vector_methods = Meta.meta Vector . methods . sort
array_methods_all = Meta.meta Array . methods . sort
array_methods = array_methods_all.filter (name -> name != "sort_builtin")
array_methods = Meta.meta Array . methods . sort
vector_methods . should_equal array_methods
Test.group "ArrayOverBuffer" <|

View File

@ -1,6 +1,6 @@
from Standard.Base import all
import Standard.Base.Errors.Common.Arithmetic_Error
import Standard.Base.Errors.Common.Type_Error
import Standard.Base.Errors.Common.Incomparable_Values
from Standard.Base.Data.Numbers import Number_Parse_Error
@ -297,21 +297,21 @@ spec =
(1 < 0.99).should_be_false
(3 < hundred_factorial).should_be_true
(3 < very_negative).should_be_false
(3 < Nothing).should_fail_with Type_Error
(3 < Nothing).should_fail_with Incomparable_Values
(1.01 < 0.99).should_be_false
(1.01 < 1.02).should_be_true
(1.01 < 1).should_be_false
(1.01 < 2).should_be_true
(3.14 < hundred_factorial).should_be_true
(3.14 < very_negative).should_be_false
(1.5 < Nothing).should_fail_with Type_Error
(1.5 < Nothing).should_fail_with Incomparable_Values
(hundred_factorial < 1).should_be_false
(hundred_factorial < 1.5).should_be_false
(very_negative < 1).should_be_true
(very_negative < 1.5).should_be_true
(hundred_factorial < very_negative).should_be_false
(very_negative < hundred_factorial).should_be_true
(very_negative < Nothing).should_fail_with Type_Error
(very_negative < Nothing).should_fail_with Incomparable_Values
Test.specify "should support less than or equal to operator" <|
(1 <= 2).should_be_true
@ -321,21 +321,21 @@ spec =
(1 <= 0.99).should_be_false
(3 <= hundred_factorial).should_be_true
(3 <= very_negative).should_be_false
(3 <= Nothing).should_fail_with Type_Error
(3 <= Nothing).should_fail_with Incomparable_Values
(1.01 <= 0.99).should_be_false
(1.01 <= 1.02).should_be_true
(1.01 <= 1).should_be_false
(1.01 <= 2).should_be_true
(3.14 <= hundred_factorial).should_be_true
(3.14 <= very_negative).should_be_false
(1.5 <= Nothing).should_fail_with Type_Error
(1.5 <= Nothing).should_fail_with Incomparable_Values
(hundred_factorial <= 1).should_be_false
(hundred_factorial <= 1.5).should_be_false
(very_negative <= 1).should_be_true
(very_negative <= 1.5).should_be_true
(hundred_factorial <= very_negative).should_be_false
(very_negative <= hundred_factorial).should_be_true
(very_negative <= Nothing).should_fail_with Type_Error
(very_negative <= Nothing).should_fail_with Incomparable_Values
Test.specify "should support greater than operator" <|
(1 > 2).should_be_false
@ -345,21 +345,21 @@ spec =
(1 > 0.99).should_be_true
(3 > hundred_factorial).should_be_false
(3 > very_negative).should_be_true
(3 > Nothing).should_fail_with Type_Error
(3 > Nothing).should_fail_with Incomparable_Values
(1.01 > 0.99).should_be_true
(1.01 > 1.02).should_be_false
(1.01 > 1).should_be_true
(1.01 > 2).should_be_false
(3.14 > hundred_factorial).should_be_false
(3.14 > very_negative).should_be_true
(1.5 > Nothing).should_fail_with Type_Error
(1.5 > Nothing).should_fail_with Incomparable_Values
(hundred_factorial > 1).should_be_true
(hundred_factorial > 1.5).should_be_true
(very_negative > 1).should_be_false
(very_negative > 1.5).should_be_false
(hundred_factorial > very_negative).should_be_true
(very_negative > hundred_factorial).should_be_false
(very_negative > Nothing).should_fail_with Type_Error
(very_negative > Nothing).should_fail_with Incomparable_Values
Test.specify "should support greater than or equal to operator" <|
(1 >= 2).should_be_false
@ -369,21 +369,21 @@ spec =
(1 >= 0.99).should_be_true
(3 >= hundred_factorial).should_be_false
(3 >= very_negative).should_be_true
(3 >= Nothing).should_fail_with Type_Error
(3 >= Nothing).should_fail_with Incomparable_Values
(1.01 >= 0.99).should_be_true
(1.01 >= 1.02).should_be_false
(1.01 >= 1).should_be_true
(1.01 >= 2).should_be_false
(3.14 >= hundred_factorial).should_be_false
(3.14 >= very_negative).should_be_true
(1.5 >= Nothing).should_fail_with Type_Error
(1.5 >= Nothing).should_fail_with Incomparable_Values
(hundred_factorial >= 1).should_be_true
(hundred_factorial >= 1.5).should_be_true
(very_negative >= 1).should_be_false
(very_negative >= 1.5).should_be_false
(hundred_factorial >= very_negative).should_be_true
(very_negative >= hundred_factorial).should_be_false
(very_negative >= Nothing).should_fail_with Type_Error
(very_negative >= Nothing).should_fail_with Incomparable_Values
Test.specify "should be ordered by the default comparator" <|
Ordering.compare 1 2 . should_equal Ordering.Less
@ -393,21 +393,21 @@ spec =
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
Ordering.compare 3 Nothing . should_fail_with Incomparable_Values
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
Ordering.compare 1.5 Nothing . should_fail_with Incomparable_Values
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
Ordering.compare very_negative Nothing . should_fail_with Incomparable_Values
Test.specify "should expose exponentiation operations" <|
(3.14 ^ 2.71).should_equal 22.216689546 epsilon=eps
@ -467,7 +467,7 @@ spec =
Number.negative_infinity . should_equal (-Number.positive_infinity)
Number.negative_infinity . equals (-Number.positive_infinity) . should_be_true
Number.nan . equals Number.nan . should_be_false
Number.nan . equals 0 . should_be_false
Number.nan . equals Number.nan . should_fail_with Incomparable_Values
Number.nan . equals 0 . should_fail_with Incomparable_Values
main = Test_Suite.run_main spec

View File

@ -2,7 +2,7 @@ from Standard.Base import all
import Standard.Base.Errors.Common.Incomparable_Values
import Standard.Base.Errors.Common.Type_Error
from Standard.Test import Test, Test_Suite
from Standard.Test import Test, Test_Suite, Problems
import Standard.Test.Extensions
# === Test Resources ===
@ -16,7 +16,18 @@ type Ord_Comparator
Comparable.from (_:Ord) = Ord_Comparator
## Unordered pair
type My_Type
Value val
type My_Type_Comparator
compare x y = (Comparable.from x.val).compare x.val y.val
hash x = (Comparable.from x.val).hash x.val
Comparable.from (_:My_Type) = My_Type_Comparator
## Unordered pair - its `compare` method returns either `Nothing` or `Ordering.Equal`.
type UPair
Value x y
@ -36,9 +47,33 @@ Comparable.from (_ : UPair) = UPair_Comparator
type Parent
Value child
# === The Tests ===
# Just a type without custom comparator
type No_Comp_Type
Value val
## Expects that `result` contains incomparable values warning.
The values within the warning message can be switched - the order
does not matter. Iterates through all the warnings of result.
expect_incomparable_warn : Any -> Any -> Any -> Nothing
expect_incomparable_warn left_val right_val result =
# Incomparable values warning wraps Text values in simple quotes
left_val_text = left_val.pretty
right_val_text = right_val.pretty
expected_warn_msg_left = "Values " + left_val_text + " and " + right_val_text + " are incomparable"
expected_warn_msg_right = "Values " + right_val_text + " and " + left_val_text + " are incomparable"
has_expected_warning = Warning.get_all result . map (_.value) . any (it-> it == expected_warn_msg_left || it == expected_warn_msg_right)
has_expected_warning . should_be_true
expect_no_warns : Any -> Nothing
expect_no_warns result =
Warning.get_all result . length . should_equal 0
# === The Tests ===
spec =
topo_sort_pending = "Waiting for implementation of topological sort (https://github.com/enso-org/enso/issues/5834)"
Test.group "Default comparator" <|
Test.specify "should support custom comparator" <|
Ordering.compare (Ord.Value 1) (Ord.Value 2) . should_equal Ordering.Less
@ -50,9 +85,9 @@ spec =
((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 Type_Error when comparing different types" <|
Ordering.compare (UPair.Value 1 2) (Ord.Value 2) . should_fail_with Type_Error
Ordering.compare 1 Nothing . should_fail_with Type_Error
Test.specify "should throw Incomparable_Values when comparing different types" <|
Ordering.compare (UPair.Value 1 2) (Ord.Value 2) . should_fail_with Incomparable_Values
Ordering.compare 1 Nothing . should_fail_with Incomparable_Values
Test.group "Ordering" <|
Test.specify "should allow conversion to sign representation" <|
@ -93,10 +128,106 @@ spec =
Ordering.compare 42.5 67.9 . should_equal Ordering.Less
Meta.is_same_object (Comparable.from Number.nan) (Comparable.from 42.0) . should_be_true
Test.specify "should fail with Type_Error for wrong type of that" <|
Ordering.compare Ordering.Less 1 . should_fail_with Type_Error
Ordering.compare Ordering.Less Nothing . should_fail_with Type_Error
Ordering.compare Ordering.Less "Hello" . should_fail_with Type_Error
Test.specify "should fail with Incomparable_Values for wrong type of that" <|
Ordering.compare Ordering.Less 1 . should_fail_with Incomparable_Values
Ordering.compare Ordering.Less Nothing . should_fail_with Incomparable_Values
Ordering.compare Ordering.Less "Hello" . should_fail_with Incomparable_Values
Test.group "Sorting with the default comparator" <|
Test.specify "should be able to sort primitive types" <|
[3, 2, 1, Nothing].sort . should_equal [1, 2, 3, Nothing]
[Nothing, Number.nan].sort . at 0 . is_nan . should_be_true
[Nothing, Number.nan].sort . at 1 . is_nothing . should_be_true
[3, 2.5].sort . should_equal [2.5, 3]
["hello", 3].sort . should_equal [3, "hello"]
["hello", "ahoj", 3].sort . should_equal [3, "ahoj", "hello"]
["hello", "ahoj", 3, 2].sort . should_equal [2, 3, "ahoj", "hello"]
["hello", "ahoj", Number.nan, 3].sort . take 3 . should_equal [3, "ahoj", "hello"]
["hello", "ahoj", Number.nan, 3].sort . at 3 . is_nan . should_be_true
[100, Date.new 2020, 50].sort . should_equal [50, 100, Date.new 2020]
[100, Nothing, Date.new 2020, 50].sort . should_equal [50, 100, Date.new 2020, Nothing]
[3, 2, True, False].sort . should_equal [2, 3, False, True]
[3, True, 2, False].sort . should_equal [2, 3, False, True]
[Nothing, False].sort . should_equal [False, Nothing]
Test.specify "should be able to sort any single-element vector without any warnings" <|
[Nothing].sort . should_equal [Nothing]
expect_no_warns [Nothing].sort
[[Nothing]].sort . should_equal [[Nothing]]
expect_no_warns [[Nothing]].sort
[[1]].sort . should_equal [[1]]
expect_no_warns [[1]].sort
Test.specify "should produce warnings when sorting nested vectors" <|
[[1], [2]].sort . should_equal [[1], [2]]
[[2], [1]].sort . should_equal [[2], [1]]
Test.specify "should be able to sort primitive values in atoms" pending=topo_sort_pending <|
[Ord.Value Nothing, Ord.Value 20, Ord.Value 10].sort . should_equal [Ord.Value 10, Ord.Value 20, Ord.Value Nothing]
Test.specify "should produce warnings when sorting primitive values in atoms" pending=topo_sort_pending <|
expect_incomparable_warn (Ord.Value 1) (Ord.Value Nothing) [Ord.Value 1, Ord.Value Nothing].sort
Test.specify "should fail to sort custom incomparable values until topological sorting is implemented" <|
[(UPair.Value 1 2), (UPair.Value 3 4)].sort . should_fail_with Incomparable_Values
Test.specify "should attach warning when trying to sort incomparable values" <|
expect_incomparable_warn Nothing Number.nan <| [Nothing, Number.nan].sort on_incomparable=Problem_Behavior.Report_Warning
expect_incomparable_warn 1 "hello" <| [1, "hello"].sort on_incomparable=Problem_Behavior.Report_Warning
Test.specify "should respect previous warnings on a vector" <|
Problems.expect_warning "my_warn" <| (Warning.attach "my_warn" [3, 2]) . sort
Problems.expect_warning "my_warn" <| (Warning.attach "my_warn" [3, Number.nan]) . sort
expect_incomparable_warn 3 Number.nan <| (Warning.attach "my_warn" [3, Number.nan]) . sort on_incomparable=Problem_Behavior.Report_Warning
Test.specify "should respect previous warnings on vectors" pending="https://github.com/enso-org/enso/issues/6070" <|
Problems.expect_warning "my_warn" <| [3, Warning.attach "my_warn" 2].sort
expect_incomparable_warn 1 Number.nan [1, Warning.attach "my_warn" Number.nan].sort
Problems.expect_warning "my_warn" <| [1, Warning.attach "my_warn" Number.nan].sort
Test.specify "should not fail when sorting incomparable types without custom comparator" <|
# Parent, and No_Comp_Type do not have custom comparators
[No_Comp_Type.Value 42, "hello"].sort . should_equal ["hello", No_Comp_Type.Value 42]
[Parent.Value 42, No_Comp_Type.Value 42].sort . should_equal [No_Comp_Type.Value 42, Parent.Value 42]
[No_Comp_Type.Value 42, Parent.Value 42].sort . should_equal [No_Comp_Type.Value 42, Parent.Value 42]
Test.group "Sorting with multiple comparators" <|
Test.specify "should sort primitive values with the default comparator as the first group" <|
[Ord.Value 4, Ord.Value 3, 20, 10].sort . should_equal [10, 20, Ord.Value 3, Ord.Value 4]
[Ord.Value 4, 20, Ord.Value 3, 10].sort . should_equal [10, 20, Ord.Value 3, Ord.Value 4]
[20, Ord.Value 4, Ord.Value 3, 10].sort . should_equal [10, 20, Ord.Value 3, Ord.Value 4]
[Ord.Value 4, 20, Ord.Value 3, 10].sort . should_equal [10, 20, Ord.Value 3, Ord.Value 4]
[Nothing, Ord.Value 4, 20, Ord.Value 3, 10].sort . should_equal [10, 20, Nothing, Ord.Value 3, Ord.Value 4]
[Ord.Value 4, 20, Ord.Value 3, Nothing, 10].sort . should_equal [10, 20, Nothing, Ord.Value 3, Ord.Value 4]
Test.specify "should produce warning when sorting types with different comparators" <|
[Ord.Value 1, 1].sort . should_equal [1, Ord.Value 1]
sorted = [Ord.Value 1, 1].sort on_incomparable=Problem_Behavior.Report_Warning
Warning.get_all sorted . at 0 . value . starts_with "Different comparators" . should_be_true
Test.specify "should merge groups of values with custom comparators based on the comparators FQN" <|
[Ord.Value 1, My_Type.Value 1].sort . should_equal [My_Type.Value 1, Ord.Value 1]
[My_Type.Value 1, Ord.Value 1].sort . should_equal [My_Type.Value 1, Ord.Value 1]
sorted = [Ord.Value 1, My_Type.Value 1].sort on_incomparable=Problem_Behavior.Report_Warning
Warning.get_all sorted . at 0 . value . starts_with "Different comparators" . should_be_true
Test.specify "should be stable when sorting values with different comparators" <|
[Ord.Value 1, 20, My_Type.Value 1, 10].sort . should_equal [10, 20, My_Type.Value 1, Ord.Value 1]
[20, Ord.Value 1, My_Type.Value 1, 10].sort . should_equal [10, 20, My_Type.Value 1, Ord.Value 1]
[20, My_Type.Value 1, Ord.Value 1, 10].sort . should_equal [10, 20, My_Type.Value 1, Ord.Value 1]
[20, 10, My_Type.Value 1, Ord.Value 1].sort . should_equal [10, 20, My_Type.Value 1, Ord.Value 1]
[My_Type.Value 1, Ord.Value 1, 20, 10].sort . should_equal [10, 20, My_Type.Value 1, Ord.Value 1]
[Ord.Value 1, 20, 10, My_Type.Value 1].sort . should_equal [10, 20, My_Type.Value 1, Ord.Value 1]
Test.specify "should be able to sort even unordered values" pending=topo_sort_pending <|
[Ord.Value 2, UPair.Value "a" "b", Ord.Value 1, UPair.Value "c" "d"].sort . should_equal [Ord.Value 2, Ord.Value 1, UPair.Value "a" "b", UPair.Value "c" "d"]
[Ord.Value 2, UPair.Value "X" "Y", Ord.Value 1, UPair.Value "c" "d"].sort . should_equal [Ord.Value 2, Ord.Value 1, UPair.Value "X" "Y", UPair.Value "c" "d"]
Test.specify "should produce warning when sorting unordered values" pending=topo_sort_pending <|
expect_incomparable_warn (UPair.Value 1 2) (UPair.Value 3 4) [UPair.Value 1 2, UPair.Value 3 4].sort
main = Test_Suite.run_main spec

View File

@ -557,11 +557,8 @@ type_spec name alter = Test.group name <|
alter [Time_Of_Day.new 12, Time_Of_Day.new 10 30] . sort . should_equal [Time_Of_Day.new 10 30, Time_Of_Day.new 12]
alter [Date_Time.new 2000 12 30 12 30, Date_Time.new 2000 12 30 12 00] . sort . should_equal [Date_Time.new 2000 12 30 12 00, Date_Time.new 2000 12 30 12 30]
alter ["aa", 2] . sort . should_fail_with Incomparable_Values
alter [2, Date.new 1999] . sort . should_fail_with Incomparable_Values
alter [Date_Time.new 1999 1 1 12 30, Date.new 1999] . sort . should_fail_with Incomparable_Values
alter [Date_Time.new 1999 1 1 12 30, Time_Of_Day.new 12 30] . sort . should_fail_with Incomparable_Values
Test.expect_panic_with ([3,2,1].to_array.sort_builtin 42) Type_Error
alter ["aa", 2] . sort . should_equal [2, "aa"]
alter [2, Date.new 1999] . sort . should_equal [2, Date.new 1999]
Test.specify "should leave the original vector unchanged" <|
non_empty_vec = alter [2, 4, 2, 3, 2, 3]
@ -574,6 +571,10 @@ type_spec name alter = Test.group name <|
small_expected = [T.Value -20 0, T.Value -1 1, T.Value -1 10, T.Value 1 8, T.Value 1 3, T.Value 4 0]
small_vec.sort . should_equal small_expected
Test.specify "should fail the sort if Report_Error problem_behavior specified" <|
alter [T.Value 1 8, Nothing] . sort on_incomparable=Problem_Behavior.Report_Error . should_fail_with Incomparable_Values
alter [Nothing, Number.nan] . sort on_incomparable=Problem_Behavior.Report_Error . should_fail_with Incomparable_Values
Test.specify "should be able to use a custom element projection" <|
small_vec = alter [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 -20 0, T.Value 4 0, T.Value -1 1, T.Value 1 3, T.Value 1 8, T.Value -1 10]

View File

@ -5,9 +5,13 @@ 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
is_error self = False
== self other = Comparable.equals_builtin self other
!= self other = (self == other).not
< self other = Comparable.less_than_builtin self other
<= self other = Comparable.less_than_builtin self other || Comparable.equals_builtin self other
> self other = Comparable.less_than_builtin other self
>= self other = Comparable.less_than_builtin other self || Comparable.equals_builtin other self
@Builtin_Type
type Comparable

View File

@ -588,16 +588,28 @@ def compare_runs(bench_run_id_1: str, bench_run_id_2: str, cache: Cache, tmp_dir
bench_run_2 = _parse_bench_run_from_json(res_2)
bench_report_1 = get_bench_report(bench_run_1, cache, tmp_dir)
bench_report_2 = get_bench_report(bench_run_2, cache, tmp_dir)
bench_labels: List[str] = list(bench_report_1.label_score_dict.keys())
# Check that the runs have the same labels, and get their intersection
bench_labels_1 = set(bench_report_1.label_score_dict.keys())
bench_labels_2 = set(bench_report_2.label_score_dict.keys())
if bench_labels_1 != bench_labels_2:
logging.warning(
f"Benchmark labels are not the same in both runs. This means that "
f"there will be some missing numbers in one of the runs. "
f"The set difference is {bench_labels_1.difference(bench_labels_2)}")
all_labels: List[str] = sorted(
list(bench_labels_1.intersection(bench_labels_2)))
bench_report_2.label_score_dict.keys()
df = pd.DataFrame(columns=["bench_label", "score-run-1", "score-run-2"])
for bench_label in bench_labels:
for bench_label in all_labels:
df = pd.concat([df, pd.DataFrame([{
"bench_label": bench_label,
"score-run-1": bench_report_1.label_score_dict[bench_label],
"score-run-2": bench_report_2.label_score_dict[bench_label],
}])], ignore_index=True)
df["score-diff"] = np.diff(df[["score-run-1", "score-run-2"]], axis=1)
df["score-diff-perc"] = df.apply(lambda row: perc_str(percent_change(row["score-run-1"], row["score-run-2"])),
df["score-diff-perc"] = df.apply(lambda row: perc_str(
percent_change(row["score-run-1"], row["score-run-2"])),
axis=1)
print("================================")
print(df.to_string(index=False, header=True, justify="center", float_format="%.5f"))