Skip to content
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
9 changes: 9 additions & 0 deletions common/src/main/java/dev/cel/common/CelOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ public enum ProtoUnsetFieldOptions {

public abstract boolean enableTimestampOverflowCheck();

public abstract boolean enableIeee754ContainerEquality();

public abstract int maxRegexProgramSize();

public abstract Builder toBuilder();
Expand Down Expand Up @@ -166,6 +168,7 @@ public static Builder newBuilder() {
.fromProtoUnsetFieldOption(ProtoUnsetFieldOptions.BIND_DEFAULT)
.enableComprehension(true)
.enableTimestampOverflowCheck(true)
.enableIeee754ContainerEquality(true)
.maxRegexProgramSize(-1);
}

Expand Down Expand Up @@ -525,6 +528,12 @@ public abstract static class Builder {
*/
public abstract Builder enableTimestampOverflowCheck(boolean value);

/**
* Enforces IEEE 754 NaN inequality on identical container references instead of
* short-circuiting on {@code x == y} (enabled by default).
*/
public abstract Builder enableIeee754ContainerEquality(boolean value);

public abstract CelOptions build();
}
}
1 change: 1 addition & 0 deletions common/src/test/java/dev/cel/common/CelOptionsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ public void current_defaults() {
// Defaults that aren't represented in deprecated CelOptions
assertThat(CelOptions.current().build().enableUnknownTracking()).isFalse();
assertThat(CelOptions.current().build().enablePrattParser()).isFalse();
assertThat(CelOptions.current().build().enableIeee754ContainerEquality()).isTrue();
}
}
11 changes: 1 addition & 10 deletions conformance/src/test/java/dev/cel/conformance/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -138,16 +138,7 @@ _ALL_TESTS = [
]

_TESTS_TO_SKIP_SPEC_IMPORT = [
# Skip until duration/timestamp range, container NaN/negative-zero equality, and proto wrapper validation are updated for spec parity.
"comparisons/bound/bound_list_nan_eq_self_false",
"comparisons/bound/bound_list_nan_ne_self_true",
"comparisons/bound/bound_map_nan_eq_self_false",
"comparisons/bound/bound_map_nan_ne_self_true",
"comparisons/in_list_literal/nan_not_in_list",
"comparisons/in_list_literal/nested_list_nan_not_in_list",
"comparisons/in_list_literal/nested_list_neg_zero_in_list",
"comparisons/in_list_literal/nested_map_nan_not_in_list",
"comparisons/in_list_literal/nested_map_neg_zero_in_list",
# Skip until duration/timestamp range and proto wrapper validation are updated for spec parity.
"timestamps/duration_conversions/from_string_max",
"timestamps/duration_conversions/from_string_min",
"timestamps/duration_conversions/toString_duration_max",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@

import com.google.errorprone.annotations.Immutable;
import com.google.protobuf.Message;
import com.google.protobuf.MessageLiteOrBuilder;
import dev.cel.common.CelOptions;
import dev.cel.common.annotations.Internal;
import dev.cel.common.internal.DynamicProto;
import dev.cel.common.internal.ProtoEquality;
import java.util.Objects;

/**
* ProtoMessageRuntimeEquality contains methods for performing CEL related equality checks,
Expand All @@ -41,23 +41,11 @@ public static ProtoMessageRuntimeEquality create(
}

@Override
public boolean objectEquals(Object x, Object y) {
if (celOptions.disableCelStandardEquality()) {
return Objects.equals(x, y);
boolean protoEquals(MessageLiteOrBuilder x, MessageLiteOrBuilder y) {
if (x instanceof Message && y instanceof Message) {
return protoEquality.equals((Message) x, (Message) y);
}

if (celOptions.enableProtoDifferencerEquality()) {
x = runtimeHelpers.adaptValue(x);
y = runtimeHelpers.adaptValue(y);
if (x instanceof Message) {
if (!(y instanceof Message)) {
return false;
}
return protoEquality.equals((Message) x, (Message) y);
}
}

return super.objectEquals(x, y);
return super.protoEquals(x, y);
}

@Override
Expand Down
178 changes: 104 additions & 74 deletions runtime/src/main/java/dev/cel/runtime/RuntimeEquality.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.RandomAccess;

/** RuntimeEquality contains methods for performing CEL related equality checks. */
@Immutable
Expand All @@ -45,10 +44,14 @@ public static RuntimeEquality create(RuntimeHelpers runtimeHelper, CelOptions ce

/** Determine whether the {@code list} contains the given {@code value}. */
public <A> boolean inList(List<A> list, A value) {
if (list.contains(value)) {
return true;
}
if (value instanceof Number) {
if (list instanceof RandomAccess) {
int size = list.size();
for (int i = 0; i < size; i++) {
if (objectEquals(list.get(i), value)) {
return true;
}
}
} else {
for (A elem : list) {
if (objectEquals(elem, value)) {
return true;
Expand Down Expand Up @@ -83,6 +86,9 @@ public Optional<Object> findInMap(Map<?, ?> map, Object index) {
if (index instanceof MessageLiteOrBuilder) {
index = runtimeHelpers.adaptProtoToValue((MessageLiteOrBuilder) index);
}
if (isNan(index)) {
return Optional.empty();
}
Object v = map.get(index);
if (v != null) {
return Optional.of(v);
Expand Down Expand Up @@ -135,100 +141,116 @@ public Optional<Object> findInMap(Map<?, ?> map, Object index) {
* comparable even if they are not of the same type, where type differences are usually trivially
* false.
*/
@SuppressWarnings({"rawtypes", "unchecked", "ReferenceEquality"})
@SuppressWarnings("ReferenceEquality")
public boolean objectEquals(Object x, Object y) {
if (celOptions.disableCelStandardEquality()) {
return Objects.equals(x, y);
}
if (x instanceof Optional) {
if (!(y instanceof Optional)) {
return false;
}
Optional<?> xOpt = (Optional<?>) x;
Optional<?> yOpt = (Optional<?>) y;
return xOpt.isPresent() == yOpt.isPresent()
&& (!xOpt.isPresent() || objectEquals(xOpt.get(), yOpt.get()));
if (x == y) {
return !containsNan(x);
}
if (x == y && !isNan(x)) {
return true;
if (x instanceof Optional) {
return y instanceof Optional && optionalEquals((Optional<?>) x, (Optional<?>) y);
}
x = runtimeHelpers.adaptValue(x);
y = runtimeHelpers.adaptValue(y);
if (x instanceof Number) {
if (!(y instanceof Number)) {
return false;
}
return ComparisonFunctions.numericEquals((Number) x, (Number) y);
if (ComparisonFunctions.isNumeric(x)) {
return y instanceof Number && ComparisonFunctions.numericEquals((Number) x, (Number) y);
}
if (celOptions.enableProtoDifferencerEquality()) {
if (x instanceof MessageLiteOrBuilder) {
if (!(y instanceof MessageLiteOrBuilder)) {
return false;
}
// TODO: Implement when CelLiteDescriptor is available
throw new UnsupportedOperationException(
"Proto Differencer equality is not supported for MessageLite.");
}
if (celOptions.enableProtoDifferencerEquality() && x instanceof MessageLiteOrBuilder) {
return y instanceof MessageLiteOrBuilder
&& protoEquals((MessageLiteOrBuilder) x, (MessageLiteOrBuilder) y);
}
if (x instanceof Iterable) {
if (!(y instanceof Iterable)) {
return y instanceof Iterable && iterableEquals((Iterable<?>) x, (Iterable<?>) y);
}
if (x instanceof Map) {
return y instanceof Map && mapEquals((Map<?, ?>) x, (Map<?, ?>) y);
}
return Objects.equals(x, y);
}

boolean protoEquals(MessageLiteOrBuilder x, MessageLiteOrBuilder y) {
// TODO: Implement when CelLiteDescriptor is available
throw new UnsupportedOperationException(
"Proto Differencer equality is not supported for MessageLite.");
}

private boolean optionalEquals(Optional<?> x, Optional<?> y) {
return x.isPresent() == y.isPresent() && (!x.isPresent() || objectEquals(x.get(), y.get()));
}

private boolean iterableEquals(Iterable<?> x, Iterable<?> y) {
if (areRandomAccessLists(x, y)) {
List<?> xList = (List<?>) x;
List<?> yList = (List<?>) y;
int size = xList.size();
if (size != yList.size()) {
return false;
}
Iterable<?> xIter = (Iterable<?>) x;
Iterable<?> yIter = (Iterable<?>) y;
Iterator<?> yElems = yIter.iterator();
IllegalArgumentException e = null;
for (Object xElem : xIter) {
if (!yElems.hasNext()) {
for (int i = 0; i < size; i++) {
if (!objectEquals(xList.get(i), yList.get(i))) {
return false;
}
try {
if (!objectEquals(xElem, yElems.next())) {
return false;
}
} catch (IllegalArgumentException iae) {
e = iae;
}
} else {
Iterator<?> yElems = y.iterator();
for (Object xElem : x) {
if (!yElems.hasNext() || !objectEquals(xElem, yElems.next())) {
return false;
}
}
if (yElems.hasNext()) {
return false;
}
if (e != null) {
throw e;
}
return true;
}
if (x instanceof Map) {
if (!(y instanceof Map)) {
return false;
}
Map xMap = (Map) x;
Map yMap = (Map) y;
if (xMap.size() != yMap.size()) {
return true;
}

private boolean mapEquals(Map<?, ?> x, Map<?, ?> y) {
if (x.size() != y.size()) {
return false;
}
for (Map.Entry<?, ?> xEntry : x.entrySet()) {
Optional<Object> yVal = findInMap(y, xEntry.getKey());
// Use isPresent() rather than isEmpty() to stay backwards compatible with Java 8.
if (!yVal.isPresent() || !objectEquals(xEntry.getValue(), yVal.get())) {
return false;
}
IllegalArgumentException e = null;
Set<Entry> entrySet = xMap.entrySet();
for (Map.Entry xEntry : entrySet) {
Optional<Object> yVal = findInMap(yMap, xEntry.getKey());
// Use isPresent() rather than isEmpty() to stay backwards compatible with Java 8.
if (!yVal.isPresent()) {
return false;
}
try {
if (!objectEquals(xEntry.getValue(), yVal.get())) {
return false;
}
} catch (IllegalArgumentException iae) {
e = iae;
}
return true;
}

private boolean containsNan(Object v) {
if (v instanceof Optional) {
Optional<?> opt = (Optional<?>) v;
return opt.isPresent() && containsNan(opt.get());
}
if (!celOptions.enableIeee754ContainerEquality()
&& (v instanceof Iterable || v instanceof Map)) {
return false;
}
v = runtimeHelpers.adaptValue(v);
if (v instanceof Double) {
return ((Double) v).isNaN();
}
if (celOptions.enableProtoDifferencerEquality() && v instanceof MessageLiteOrBuilder) {
return !protoEquals((MessageLiteOrBuilder) v, (MessageLiteOrBuilder) v);
}
if (v instanceof Iterable) {
return !iterableEquals((Iterable<?>) v, (Iterable<?>) v);
}
if (v instanceof Map) {
for (Map.Entry<?, ?> entry : ((Map<?, ?>) v).entrySet()) {
if (entry.getValue() == null
|| containsNan(entry.getKey())
|| containsNan(entry.getValue())) {
return true;
}
}
if (e != null) {
throw e;
}
return true;
return false;
}
return Objects.equals(x, y);
return false;
}

/**
Expand Down Expand Up @@ -294,6 +316,14 @@ private static boolean isNan(Object value) {
return value instanceof Number && Double.isNaN(((Number) value).doubleValue());
}

private static boolean areRandomAccessLists(Iterable<?> x, Iterable<?> y) {
// NOMUTANTS -- RandomAccess fast path is allocation-free; Iterator fallback is equivalent.
return x instanceof List
&& x instanceof RandomAccess
&& y instanceof List
&& y instanceof RandomAccess;
}

RuntimeEquality(RuntimeHelpers runtimeHelpers, CelOptions celOptions) {
this.runtimeHelpers = runtimeHelpers;
this.celOptions = celOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import dev.cel.runtime.CelAsyncCall;
import dev.cel.runtime.CelAsyncFunctionOverload;
import dev.cel.runtime.RuntimeEquality;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import org.jspecify.annotations.Nullable;
Expand Down Expand Up @@ -96,9 +97,6 @@ static int hashCall(long exprId, String overloadId, Object[] args) {
/**
* Returns whether this record tracks a call to the same expression node, function, overload, and
* arguments.
*
* <p>Arguments are compared under CEL equality, except that NaN compares equal to itself so that
* a node re-evaluated with a NaN argument can find its existing record.
*/
boolean matches(
long exprId,
Expand All @@ -115,7 +113,10 @@ boolean matches(
for (int i = 0; i < this.args.length; i++) {
Object arg = this.args[i];
Object otherArg = args[i];
if (!runtimeEquality.objectEquals(arg, otherArg) && !(isNan(arg) && isNan(otherArg))) {
// Treat Java-equal containers and scalar NaNs as matching so NaN args memoize across passes.
if (!Objects.equals(arg, otherArg)
&& !runtimeEquality.objectEquals(arg, otherArg)
&& !(isNan(arg) && isNan(otherArg))) {
return false;
}
}
Expand Down
Loading
Loading