Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NaN can be used as a key in Map #6301

Merged
merged 19 commits into from
Apr 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Data/Map.enso
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ from project import Error, Nothing, Any, Panic
for equality any objects that can appear in Enso - primitives, Atoms, values coming from
different languages, etc.

For keys that are not reflexive, like `Number.nan`,
[Same Value equality specification](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#same-value-zero_equality)
is used. This means that both `Number.nan` and types with comparators that violate
reflexivity (e.g. their `compare` method always returns `Nothing`) can be used as keys
in the Map.

A single key-value pair is called an *entry*.

It is possible to pass a Map created in Enso to foreign functions, where it will be treated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
@BuiltinMethod(
type = "Any",
name = "==",
description = """
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,
False otherwise.
Expand All @@ -32,9 +33,9 @@
Does not throw dataflow errors or panics.

Note that this is different than `Meta.is_same_object`, which checks whether two
references point to the same object on the heap.
"""
)
references point to the same object on the heap. Moreover, `Meta.is_same_object`
implies `Any.==` for all object with the exception of `Number.nan`.
""")
@GenerateUncached
public abstract class EqualsNode extends Node {

Expand All @@ -44,11 +45,6 @@ public static EqualsNode build() {

public abstract boolean execute(@AcceptsError Object self, @AcceptsError Object right);

/**
* Primitive values
**/


@Specialization
boolean equalsBoolBool(boolean self, boolean other) {
return self == other;
Expand Down Expand Up @@ -166,10 +162,7 @@ boolean equalsLongBigInt(long self, EnsoBigInteger other) {
}
}

@Specialization(guards = {
"selfText.is_normalized()",
"otherText.is_normalized()"
})
@Specialization(guards = {"selfText.is_normalized()", "otherText.is_normalized()"})
boolean equalsTextText(Text selfText, Text otherText) {
return selfText.toString().compareTo(otherText.toString()) == 0;
}
Expand Down Expand Up @@ -199,13 +192,14 @@ boolean equalsTextBigInt(Text self, EnsoBigInteger other) {
* normalization. See {@code Text_Utils.compare_to}.
*/
@TruffleBoundary
@Specialization(guards = {
"selfInterop.isString(selfString)",
"otherInterop.isString(otherString)"
}, limit = "3")
boolean equalsStrings(Object selfString, Object otherString,
@CachedLibrary("selfString") InteropLibrary selfInterop,
@CachedLibrary("otherString") InteropLibrary otherInterop) {
@Specialization(
guards = {"selfInterop.isString(selfString)", "otherInterop.isString(otherString)"},
limit = "3")
boolean equalsStrings(
Object selfString,
Object otherString,
@CachedLibrary("selfString") InteropLibrary selfInterop,
@CachedLibrary("otherString") InteropLibrary otherInterop) {
String selfJavaString;
String otherJavaString;
try {
Expand All @@ -214,43 +208,43 @@ boolean equalsStrings(Object selfString, Object otherString,
} catch (UnsupportedMessageException e) {
throw new IllegalStateException(e);
}
return Normalizer.compare(
selfJavaString,
otherJavaString,
Normalizer.FOLD_CASE_DEFAULT
) == 0;
return Normalizer.compare(selfJavaString, otherJavaString, Normalizer.FOLD_CASE_DEFAULT) == 0;
}

@Specialization(
guards = "isPrimitive(self, strings) != isPrimitive(other, strings)"
)
boolean equalsDifferent(Object self, Object other, @CachedLibrary(limit = "10") InteropLibrary strings) {
@Specialization(guards = "isPrimitive(self, interop) != isPrimitive(other, interop)")
boolean equalsDifferent(
Object self, Object other, @CachedLibrary(limit = "10") InteropLibrary interop) {
return false;
}

/** Equals for Atoms and AtomConstructors */

@Specialization
boolean equalsAtomConstructors(AtomConstructor self, AtomConstructor other) {
return self == other;
}

@Specialization
boolean equalsAtoms(Atom self, Atom other, @Cached EqualsAtomNode equalsNode) {
return equalsNode.execute(self, other);
boolean equalsAtoms(
Atom self,
Atom other,
@Cached EqualsAtomNode equalsNode,
@Cached IsSameObjectNode isSameObjectNode) {
return isSameObjectNode.execute(self, other) || equalsNode.execute(self, other);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the fix for #6065, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, #6065 is addressed in these commits:

}
@Specialization(guards = "isNotPrimitive(self, other, strings, warnings)")

@Specialization(guards = "isNotPrimitive(self, other, interop, warnings)")
boolean equalsComplex(
Object self, Object other,
@Cached EqualsComplexNode complex,
@CachedLibrary(limit = "10") InteropLibrary strings,
@CachedLibrary(limit = "10") WarningsLibrary warnings
) {
return complex.execute(self, other);
Object self,
Object other,
@Cached EqualsComplexNode equalsComplex,
@Cached IsSameObjectNode isSameObjectNode,
@CachedLibrary(limit = "10") InteropLibrary interop,
@CachedLibrary(limit = "5") WarningsLibrary warnings) {
return isSameObjectNode.execute(self, other) || equalsComplex.execute(self, other);
}

static boolean isNotPrimitive(Object a, Object b, InteropLibrary strings, WarningsLibrary warnings) {
static boolean isNotPrimitive(
Object a, Object b, InteropLibrary interop, WarningsLibrary warnings) {
if (a instanceof AtomConstructor && b instanceof AtomConstructor) {
return false;
}
Expand All @@ -260,22 +254,22 @@ static boolean isNotPrimitive(Object a, Object b, InteropLibrary strings, Warnin
if (warnings.hasWarnings(a) || warnings.hasWarnings(b)) {
return true;
}
return !isPrimitive(a, strings) && !isPrimitive(b, strings);
return !isPrimitive(a, interop) && !isPrimitive(b, interop);
}

/**
* Return true iff object is a primitive value used in some specializations
* guard. By primitive value we mean any value that can be present in Enso, so,
* for example, not Integer, as that cannot be present in Enso.
* All the primitive types should be handled in their corresponding specializations.
* See {@link org.enso.interpreter.node.expression.builtin.interop.syntax.HostValueToEnsoNode}.
* Return true iff object is a primitive value used in some specializations guard. By primitive
* value we mean any value that can be present in Enso, so, for example, not Integer, as that
* cannot be present in Enso. All the primitive types should be handled in their corresponding
* specializations. See {@link
* org.enso.interpreter.node.expression.builtin.interop.syntax.HostValueToEnsoNode}.
*/
static boolean isPrimitive(Object object, InteropLibrary strings) {
return object instanceof Boolean ||
object instanceof Long ||
object instanceof Double ||
object instanceof EnsoBigInteger ||
object instanceof Text ||
strings.isString(object);
static boolean isPrimitive(Object object, InteropLibrary interop) {
return object instanceof Boolean
|| object instanceof Long
|| object instanceof Double
|| object instanceof EnsoBigInteger
|| object instanceof Text
|| interop.isString(object);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.enso.interpreter.node.expression.builtin.meta;

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;
Expand All @@ -15,6 +16,7 @@
name = "is_same_object",
description = "Checks if the two arguments share an underlying reference.",
autoRegister = false)
@GenerateUncached
public abstract class IsSameObjectNode extends Node {

public static IsSameObjectNode build() {
Expand All @@ -23,6 +25,15 @@ public static IsSameObjectNode build() {

public abstract boolean execute(@AcceptsError Object left, @AcceptsError Object right);

@Specialization
boolean isSameDouble(double left, double right) {
if (Double.isNaN(left) && Double.isNaN(right)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the handling of NaN as being "same".

return true;
} else {
return left == right;
}
}

@Specialization
boolean isSameType(Type typeLeft, Type typeRight) {
return typeLeft == typeRight;
Expand Down
55 changes: 55 additions & 0 deletions test/Tests/src/Data/Map_Spec.enso
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,22 @@ import Standard.Test.Extensions
polyglot java import java.nio.file.Path as JavaPath
polyglot java import java.util.Map as JavaMap

## Type that violates reflexivity
type My_Nan
Value comment:Text

type My_Nan_Comparator
compare _ _ = Nothing
hash _ = 0

Comparable.from (_:My_Nan) = My_Nan_Comparator

foreign js js_str str = """
return new String(str)

foreign js js_null = """
return null

foreign js js_empty_dict = """
return new Map()

Expand Down Expand Up @@ -125,6 +138,18 @@ spec =
map.size.should_equal 1
map.get [1, "a", 2] . should_equal "Value"

Test.specify "should support NaN as keys" <|
Map.empty.insert Number.nan 1 . contains_key Number.nan . should_be_true
Map.empty.insert Number.nan 1 . values . should_equal [1]
Map.empty.insert Number.nan 1 . insert Number.nan 2 . contains_key Number.nan . should_be_true
Map.empty.insert Number.nan 1 . insert Number.nan 2 . values . should_equal [2]
Map.empty.insert Number.nan 1 . insert "key" 2 . insert Number.nan 3 . contains_key Number.nan . should_be_true
Map.empty.insert Number.nan 1 . insert "key" 2 . insert Number.nan 3 . contains_key "key" . should_be_true
Map.empty.insert Number.nan 1 . insert "key" 2 . insert Number.nan 3 . at Number.nan . should_equal 3
Map.empty.insert Number.nan 1 . insert "key" 2 . insert Number.nan 3 . at "key" . should_equal 2
Map.empty.insert Number.nan 1 . insert Number.nan Number.nan . at Number.nan . to_text . should_equal "NaN"
Map.empty.insert Number.nan 1 . insert Number.nan Number.nan . remove Number.nan . size . should_equal 0

Test.specify "should support dates as keys" <|
map = Map.empty.insert (Date.new 1993) 1 . insert (Date.new 1993 2 5) 2 . insert (Date_Time.new 1993 2 5 13 45) 3
map.size.should_equal 3
Expand All @@ -143,6 +168,33 @@ spec =
(map.get key_map).should_equal 23
(map.get map).should_equal Nothing

Test.specify "should support another hash map with NaN keys as key" <|
Map.singleton (Map.singleton Number.nan 1) 42 . size . should_equal 1
Map.singleton (Map.singleton Number.nan 1) 42 . keys . at 0 . keys . to_text . should_equal "[NaN]"
Map.singleton (Map.singleton Number.nan 1) 42 . keys . at 0 . get Number.nan . should_equal 1
Map.singleton (Map.singleton Number.nan 1) 42 . at (Map.singleton Number.nan 1) . should_equal 42

Test.specify "should support atoms with custom comparators that violate reflexivity as keys" <|
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was copied from the issue description

k = My_Nan.Value "foo"
k2 = My_Nan.Value "foo"
(k==k).should_be_true
(k==k2).should_be_false
Meta.is_same_object k k2 . should_be_false
Meta.is_same_object k k . should_be_true
m = Map.empty.insert k 10
m.contains_key k . should_be_true
m.get k . should_equal 10
m.contains_key k2 . should_be_false

m2 = m.insert k2 20
m2.get k . should_equal 10
m2.get k2 . should_equal 20
m2.size . should_equal 2

m3 = m2.insert k 30
m3.size . should_equal 2
m3.get k . should_equal 30

Test.specify "should handle keys with standard equality semantics" <|
map = Map.singleton 2 "Hello"
(map.get 2).should_equal "Hello"
Expand All @@ -152,6 +204,9 @@ spec =
Test.specify "should handle Nothing as keys" <|
Map.singleton Nothing 3 . get Nothing . should_equal 3
Map.singleton Nothing 1 . insert Nothing 2 . get Nothing . should_equal 2
Map.singleton Nothing 1 . should_equal (Map.singleton Nothing 1)
Map.singleton Nothing 1 . insert Nothing 2 . at Nothing . should_equal 2
Map.singleton js_null 1 . at Nothing . should_equal 1

Test.specify "should handle incomparable values as keys" <|
Map.empty.insert Number.nan 1 . insert Number.nan 2 . get Number.nan . should_equal 2
Expand Down
28 changes: 28 additions & 0 deletions test/Tests/src/Data/Vector_Spec.enso
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ type T_Comparator

Comparable.from (_:T) = T_Comparator

## Type that violates reflexivity
type My_Nan
Value value

type My_Nan_Comparator
compare _ _ = Nothing
hash _ = 0

Comparable.from (_:My_Nan) = My_Nan_Comparator

type My_Error
Value a

Expand Down Expand Up @@ -654,8 +664,26 @@ type_spec name alter = Test.group name <|
alter [1, 1.0, 2, 2.0] . distinct . should_equal [1, 2]
alter [] . distinct . should_equal []

Test.specify "should be able to handle distinct on different primitive values" <|
alter [1, "a"] . distinct . should_equal [1, "a"]
alter ["a", 1] . distinct . should_equal ["a", 1]
alter [Nothing, Nothing] . distinct . should_equal [Nothing]
alter [Number.nan, Number.nan] . distinct . to_text . should_equal "[NaN]"
alter [Nothing, Number.nan, Nothing, Number.nan] . distinct . to_text . should_equal "[Nothing, NaN]"
alter [1, Nothing] . distinct . should_equal [1, Nothing]
alter [Nothing, 1, Nothing] . distinct . should_equal [Nothing, 1]
alter [1, Number.nan] . distinct . to_text . should_equal "[1, NaN]"
alter [Number.nan, 1, Number.nan] . distinct . to_text . should_equal "[NaN, 1]"
alter [1, Number.nan, 1] . distinct . to_text . should_equal "[1, NaN]"
Comment on lines +667 to +677
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does distinct work with other non-reflexive values, like My_Nan? Can we get a test for it too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 6bae728

alter [Number.nan, My_Nan.Value 42, My_Nan.Value 23] . distinct . to_text . should_equal "[NaN, (My_Nan.Value 42), (My_Nan.Value 23)]"
my_nan = My_Nan.Value 42
alter [my_nan, Number.nan, my_nan] . distinct . to_text . should_equal "[(My_Nan.Value 42), NaN]"

Test.specify "should correctly handle distinct with types that have custom comparators" <|
alter [T.Value 1 2, T.Value 3 3, T.Value 1 2] . distinct . should_equal [T.Value 1 2, T.Value 3 3]
alter [T.Value 1 2, T.Value 3 3, T.Value 1 2, Nothing] . distinct . should_equal [T.Value 1 2, T.Value 3 3, Nothing]
alter [Nothing, T.Value 1 2, T.Value 3 3, T.Value 1 2, Nothing] . distinct . should_equal [Nothing, T.Value 1 2, T.Value 3 3]
alter [T.Value 1 2, Date.new hours=3] . distinct . should_equal [T.Value 1 2, Date.new hours=3]

Test.specify "should return a vector containing only unique elements up to some criteria" <|
alter [Pair.new 1 "a", Pair.new 2 "b", Pair.new 1 "c"] . distinct (on = _.first) . should_equal [Pair.new 1 "a", Pair.new 2 "b"]
Expand Down
18 changes: 18 additions & 0 deletions test/Tests/src/Semantic/Equals_Spec.enso
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ type Child_Comparator

Comparable.from (_:Child) = Child_Comparator

## Type that violates reflexivity
type My_Nan
Value val

type My_Nan_Comparator
compare _ _ = Nothing
hash _ = 0

Comparable.from (_:My_Nan) = My_Nan_Comparator

type Parent
Value child

Expand Down Expand Up @@ -159,6 +169,14 @@ spec =
four_field = FourFieldType.Value 1 2 3 4
(rect == four_field).should_be_false

Test.specify "Any.== should check for Meta.is_same_object" <|
obj_1 = My_Nan.Value 42
obj_2 = My_Nan.Value 42
(obj_1 == obj_2).should_be_false
Meta.is_same_object obj_1 obj_2 . should_be_false
(obj_1 == obj_1).should_be_true
Meta.is_same_object obj_1 obj_1 . should_be_true

Test.specify "should handle `==` on types" <|
(Child == Child).should_be_true
(Child == Point).should_be_false
Expand Down
7 changes: 6 additions & 1 deletion test/Tests/src/Semantic/Meta_Spec.enso
Original file line number Diff line number Diff line change
Expand Up @@ -281,13 +281,18 @@ spec =
Meta.get_annotation value "Value" "bar" . should_equal Nothing
Meta.get_annotation value "Value" "baz" . should_equal (My_Type.Value 1 2 3)

Test.group "Check Nothing" <|
Test.group "Check Nothing and NaN" <|
Test.specify "Nothing.is_a Nothing" <|
Nothing.is_a Nothing . should_be_true
Meta.is_same_object Nothing Nothing . should_be_true

Test.specify "type_of Nothing is Nothing" <|
Meta.type_of Nothing . should_equal Nothing

Test.specify "NaN and NaN should be the same object" <|
Meta.is_same_object Number.nan Number.nan . should_be_true
(Number.nan == Number.nan) . should_be_false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nan != nan, but Meta.is_same_object gives true. OK.


Test.group "Atom with holes" <|
Test.specify "construct and fill" <|
pair = Meta.atom_with_hole (e -> My_Type.Value 1 e 3)
Expand Down
Loading