Skip to content

Commit fc10b02

Browse files
l46kokcopybara-github
authored andcommitted
Align runtime numeric handlings with IEEE 754 by fixing NaN and negative zero comparisons
PiperOrigin-RevId: 1008426293
1 parent 5eb933c commit fc10b02

20 files changed

Lines changed: 727 additions & 275 deletions

File tree

‎common/src/main/java/dev/cel/common/internal/BUILD.bazel‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ java_library(
8888
tags = [
8989
],
9090
deps = [
91-
"//:auto_value",
9291
"//common/annotations",
9392
"@maven//:com_google_errorprone_error_prone_annotations",
9493
"@maven//:com_google_guava_guava",

‎common/src/main/java/dev/cel/common/internal/ComparisonFunctions.java‎

Lines changed: 19 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -30,32 +30,17 @@
3030
public final class ComparisonFunctions {
3131

3232
private static final UnsignedLong UINT_TO_INT_MAX = UnsignedLong.valueOf(Long.MAX_VALUE);
33-
private static final double DOUBLE_TO_INT_MAX = (double) Long.MAX_VALUE;
34-
private static final double DOUBLE_TO_INT_MIN = (double) Long.MIN_VALUE;
35-
private static final double DOUBLE_TO_UINT_MAX = UnsignedLong.MAX_VALUE.doubleValue();
3633

3734
public static int compareDoubleInt(double d, long l) {
38-
if (d < DOUBLE_TO_INT_MIN) {
39-
return -1;
40-
}
41-
if (d > DOUBLE_TO_INT_MAX) {
42-
return 1;
43-
}
44-
return Double.compare(d, (double) l);
35+
return compareDouble(d, (double) l);
4536
}
4637

4738
public static int compareIntDouble(long l, double d) {
4839
return -compareDoubleInt(d, l);
4940
}
5041

5142
public static int compareDoubleUint(double d, UnsignedLong ul) {
52-
if (d < 0.0) {
53-
return -1;
54-
}
55-
if (d > DOUBLE_TO_UINT_MAX) {
56-
return 1;
57-
}
58-
return Double.compare(d, ul.doubleValue());
43+
return compareDouble(d, ul.doubleValue());
5944
}
6045

6146
public static int compareUintDouble(UnsignedLong ul, double d) {
@@ -73,54 +58,29 @@ public static int compareUintInt(UnsignedLong ul, long l) {
7358
return -compareIntUint(l, ul);
7459
}
7560

61+
/** Returns true if {@code value} is one of CEL's canonical runtime numeric types. */
62+
public static boolean isNumeric(Object value) {
63+
return value instanceof Long || value instanceof Double || value instanceof UnsignedLong;
64+
}
65+
7666
/**
7767
* Compare two numeric values of any type (double, int, uint) for equality.
7868
*
79-
* <p>Floating point values are follow IEEE 754 standard for NaN comparisons.
69+
* <p>Floating point values follow IEEE 754 standard for NaN and negative zero comparisons.
8070
*/
8171
public static boolean numericEquals(Number x, Number y) {
82-
if (x instanceof Double) {
83-
if (y instanceof Double) {
84-
return !(Double.isNaN((Double) x) || Double.isNaN((Double) y))
85-
&& x.doubleValue() == y.doubleValue();
86-
}
87-
if (y instanceof Long) {
88-
return compareDoubleInt((Double) x, (Long) y) == 0;
89-
}
90-
if (y instanceof UnsignedLong) {
91-
return compareDoubleUint((Double) x, (UnsignedLong) y) == 0;
92-
}
93-
}
94-
if (x instanceof Long) {
95-
if (y instanceof Long) {
96-
return x.equals(y);
97-
}
98-
if (y instanceof Double) {
99-
return compareIntDouble((Long) x, (Double) y) == 0;
100-
}
101-
if (y instanceof UnsignedLong) {
102-
return compareIntUint((Long) x, (UnsignedLong) y) == 0;
103-
}
104-
}
105-
if (x instanceof UnsignedLong) {
106-
if (y instanceof UnsignedLong) {
107-
return x.equals(y);
108-
}
109-
if (y instanceof Double) {
110-
return compareUintDouble((UnsignedLong) x, (Double) y) == 0;
111-
}
112-
if (y instanceof Long) {
113-
return compareUintInt((UnsignedLong) x, (Long) y) == 0;
114-
}
115-
}
116-
return false;
72+
// Only x needs a NaN check: if y is NaN and x is not, numericCompare returns -1 != 0.
73+
return isNumeric(x)
74+
&& isNumeric(y)
75+
&& !(x instanceof Double && ((Double) x).isNaN())
76+
&& numericCompare(x, y) == 0;
11777
}
11878

11979
/** Compare two numeric values of any type (double, int, uint). */
12080
public static int numericCompare(Number x, Number y) {
12181
if (x instanceof Double) {
12282
if (y instanceof Double) {
123-
return ((Double) x).compareTo((Double) y);
83+
return compareDouble((Double) x, (Double) y);
12484
}
12585
if (y instanceof Long) {
12686
return compareDoubleInt((Double) x, (Long) y);
@@ -155,5 +115,10 @@ public static int numericCompare(Number x, Number y) {
155115
"Unsupported argument types: " + x.getClass() + ", " + y.getClass());
156116
}
157117

118+
private static int compareDouble(double a, double b) {
119+
// Treat -0.0 and 0.0 as equal via ==; Double.compare orders NaN after all non-NaN numbers.
120+
return a == b ? 0 : Double.compare(a, b);
121+
}
122+
158123
private ComparisonFunctions() {}
159124
}

‎common/src/main/java/dev/cel/common/internal/ProtoEquality.java‎

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
import com.google.auto.value.AutoValue;
1818
import com.google.common.collect.ImmutableMap;
19+
import com.google.common.collect.Lists;
20+
import com.google.common.collect.Maps;
1921
import com.google.errorprone.annotations.CheckReturnValue;
2022
import com.google.errorprone.annotations.Immutable;
2123
import com.google.protobuf.Any;
@@ -26,6 +28,7 @@
2628
import dev.cel.common.annotations.Internal;
2729
import java.util.List;
2830
import java.util.Map;
31+
import java.util.Objects;
2932
import java.util.Optional;
3033

3134
/**
@@ -35,9 +38,11 @@
3538
* <p>These are the key differences between CEL's proto equality and Java's proto equality:
3639
*
3740
* <ul>
38-
* <li/>NaN is not equal to itself.
39-
* <li/>Any values are unpacked before comparison.
40-
* <li/>If two Any values cannot be unpacked, they are compared by bytes.
41+
* <li>NaN is not equal to itself.
42+
* <li>Positive and negative zero (+0.0 and -0.0) compare as equal when both fields are present
43+
* (under proto3 implicit presence, -0.0 is serialized as present while +0.0 is unset).
44+
* <li>Any values are unpacked before comparison.
45+
* <li>If two Any values cannot be unpacked, they are compared by bytes.
4146
* </ul>
4247
*
4348
* <p>CEL Library Internals. Do Not Use.
@@ -56,9 +61,6 @@ public ProtoEquality(DynamicProto dynamicProto) {
5661
}
5762

5863
public boolean equals(Message message1, Message message2) {
59-
if (message1 == message2) {
60-
return true;
61-
}
6264
if (!message1.getDescriptorForType().equals(message2.getDescriptorForType())) {
6365
return false;
6466
}
@@ -124,22 +126,50 @@ public boolean equals(Message message1, Message message2) {
124126
return true;
125127
}
126128

129+
/** Returns a hash code consistent with {@link #equals(Message, Message)}. */
130+
public int hashCode(Message message) {
131+
if (isAny(message)) {
132+
// Constant hash: unpacked Any equality ignores wire bytes, while fallback ignores type_url.
133+
return ANY_FULL_NAME.hashCode();
134+
}
135+
return Objects.hash(
136+
message.getDescriptorForType(),
137+
message.getUnknownFields(),
138+
Maps.transformEntries(message.getAllFields(), this::hashFieldEntry));
139+
}
140+
141+
private int hashFieldEntry(FieldDescriptor field, Object value) {
142+
if (field.isMapField()) {
143+
ProtoMap protoMap = protoMap((List<?>) value);
144+
return Maps.transformValues(protoMap.map(), v -> hashFieldValue(protoMap.valueField(), v))
145+
.hashCode();
146+
}
147+
if (field.isRepeated()) {
148+
return Lists.transform((List<?>) value, elem -> hashFieldValue(field, elem)).hashCode();
149+
}
150+
return hashFieldValue(field, value);
151+
}
152+
153+
private int hashFieldValue(FieldDescriptor field, Object value) {
154+
switch (field.getJavaType()) {
155+
case MESSAGE:
156+
return hashCode((Message) value);
157+
case DOUBLE:
158+
case FLOAT:
159+
// Adding 0.0d normalizes -0.0d to +0.0d per IEEE 754 so equal zeros hash alike.
160+
return Double.hashCode(((Number) value).doubleValue() + 0.0d);
161+
default:
162+
return value.hashCode();
163+
}
164+
}
165+
127166
private boolean equalsFieldValues(FieldDescriptor field, Object value1, Object value2) {
128167
switch (field.getJavaType()) {
129168
case MESSAGE:
130169
return equals((Message) value1, (Message) value2);
131170
case DOUBLE:
132-
// fallthrough
133171
case FLOAT:
134-
Number num1 = (Number) value1;
135-
Number num2 = (Number) value2;
136-
boolean num1IsNaN = Double.isNaN(num1.doubleValue());
137-
boolean num2IsNaN = Double.isNaN(num2.doubleValue());
138-
// handle the NaN case
139-
if (num1IsNaN && num2IsNaN) {
140-
return false;
141-
}
142-
return num1.equals(num2);
172+
return ((Number) value1).doubleValue() == ((Number) value2).doubleValue();
143173
default:
144174
return value1.equals(value2);
145175
}

‎common/src/test/java/dev/cel/common/internal/ComparisonFunctionsTest.java‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
import static com.google.common.truth.Truth.assertThat;
1818

1919
import com.google.common.primitives.UnsignedLong;
20+
import com.google.testing.junit.testparameterinjector.TestParameter;
2021
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
2122
import com.google.testing.junit.testparameterinjector.TestParameters;
23+
import java.math.BigDecimal;
2224
import java.math.BigInteger;
2325
import org.junit.Test;
2426
import org.junit.runner.RunWith;
@@ -28,20 +30,24 @@ public final class ComparisonFunctionsTest {
2830

2931
@Test
3032
@TestParameters("{x: 1.0, y: 1, expect: 0}")
33+
@TestParameters("{x: -0.0, y: 0, expect: 0}")
3134
@TestParameters("{x: -2.1, y: -2, expect: -1}")
3235
@TestParameters("{x: 2.1, y: -3, expect: 1}")
3336
@TestParameters("{x: -9223372036854778809.0, y: 1, expect: -1}")
3437
@TestParameters("{x: 9223372036854778809.0, y: 1, expect: 1}")
38+
@TestParameters("{x: NaN, y: 0, expect: 1}")
3539
public void compareDoubleInt(double x, long y, int expect) {
3640
assertThat(ComparisonFunctions.compareDoubleInt(x, y)).isEqualTo(expect);
3741
assertThat(ComparisonFunctions.compareIntDouble(y, x)).isEqualTo(-1 * expect);
3842
}
3943

4044
@Test
4145
@TestParameters("{x: 1.0, y: 1, expect: 0}")
46+
@TestParameters("{x: -0.0, y: 0, expect: 0}")
4247
@TestParameters("{x: -2.1, y: 0, expect: -1}")
4348
@TestParameters("{x: 2.1, y: 1, expect: 1}")
4449
@TestParameters("{x: 18446744073709653620.1, y: 1, expect: 1}")
50+
@TestParameters("{x: NaN, y: 0, expect: 1}")
4551
public void compareDoubleUint(double x, long y, int expect) {
4652
UnsignedLong uy = UnsignedLong.valueOf(y);
4753
assertThat(ComparisonFunctions.compareDoubleUint(x, uy)).isEqualTo(expect);
@@ -93,4 +99,49 @@ public void numericCompareUintInt(long x, long y, int expect) {
9399
assertThat(ComparisonFunctions.numericCompare(ux, y)).isEqualTo(expect);
94100
assertThat(ComparisonFunctions.numericCompare(y, ux)).isEqualTo(-1 * expect);
95101
}
102+
103+
@Test
104+
@TestParameters("{x: -0.0, y: 0.0, expect: 0}")
105+
@TestParameters("{x: 1.0, y: 2.0, expect: -1}")
106+
@TestParameters("{x: NaN, y: 1.0, expect: 1}")
107+
@TestParameters("{x: NaN, y: NaN, expect: 0}")
108+
public void numericCompareDoubleDouble(double x, double y, int expect) {
109+
assertThat(ComparisonFunctions.numericCompare(x, y)).isEqualTo(expect);
110+
assertThat(ComparisonFunctions.numericCompare(y, x)).isEqualTo(-1 * expect);
111+
}
112+
113+
@SuppressWarnings("ImmutableEnumChecker") // Test only
114+
private enum NumericEqualsTestCase {
115+
NEGATIVE_ZERO_AND_POSITIVE_ZERO(-0.0, 0.0, true),
116+
NEGATIVE_ZERO_AND_LONG_ZERO(-0.0, 0L, true),
117+
NEGATIVE_ZERO_AND_ULONG_ZERO(-0.0, UnsignedLong.ZERO, true),
118+
DOUBLE_ONE_AND_LONG_ONE(1.0, 1L, true),
119+
DOUBLE_ONE_AND_ULONG_ONE(1.0, UnsignedLong.ONE, true),
120+
DISTINCT_DOUBLES(1.0, 2.0, false),
121+
DISTINCT_DOUBLE_AND_LONG(1.0, 2L, false),
122+
NAN_AND_NAN(Double.NaN, Double.NaN, false),
123+
NAN_AND_LONG_ZERO(Double.NaN, 0L, false),
124+
NAN_AND_ULONG_ZERO(Double.NaN, UnsignedLong.ZERO, false),
125+
SHORT_AND_LONG((short) 1, 1L, false),
126+
SHORT_AND_NAN((short) 1, Double.NaN, false),
127+
BIG_DECIMAL_AND_DOUBLE(BigDecimal.ONE, 1.0, false);
128+
129+
private final Number x;
130+
private final Number y;
131+
private final boolean expected;
132+
133+
NumericEqualsTestCase(Number x, Number y, boolean expected) {
134+
this.x = x;
135+
this.y = y;
136+
this.expected = expected;
137+
}
138+
}
139+
140+
@Test
141+
public void numericEquals(@TestParameter NumericEqualsTestCase testCase) {
142+
assertThat(ComparisonFunctions.numericEquals(testCase.x, testCase.y))
143+
.isEqualTo(testCase.expected);
144+
assertThat(ComparisonFunctions.numericEquals(testCase.y, testCase.x))
145+
.isEqualTo(testCase.expected);
146+
}
96147
}

0 commit comments

Comments
 (0)