From 712ac8acf87dcde05e057f8c33601feb3700f8c6 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Thu, 13 Aug 2026 15:45:00 -0700 Subject: [PATCH] Implement iterative eval for program planner PiperOrigin-RevId: 964335064 --- .../test/java/dev/cel/bundle/CelImplTest.java | 82 ++- .../extensions/CelOptionalLibraryTest.java | 213 ++++++- runtime/BUILD.bazel | 6 + .../dev/cel/runtime/AccumulatedUnknowns.java | 9 +- .../src/main/java/dev/cel/runtime/BUILD.bazel | 2 + .../java/dev/cel/runtime/CelAttribute.java | 14 +- .../java/dev/cel/runtime/CelRuntimeImpl.java | 11 +- .../java/dev/cel/runtime/UnknownContext.java | 37 +- .../dev/cel/runtime/planner/Attribute.java | 2 +- .../runtime/planner/AttributeResolution.java | 50 ++ .../java/dev/cel/runtime/planner/BUILD.bazel | 56 +- .../cel/runtime/planner/EvalAttribute.java | 10 +- .../dev/cel/runtime/planner/EvalBinary.java | 2 + .../dev/cel/runtime/planner/EvalHelpers.java | 15 + .../dev/cel/runtime/planner/EvalIndex.java | 243 ++++++++ .../dev/cel/runtime/planner/EvalTestOnly.java | 7 +- .../cel/runtime/planner/ExecutionFrame.java | 32 +- .../planner/InterpretableAttribute.java | 3 + .../cel/runtime/planner/MaybeAttribute.java | 14 +- .../cel/runtime/planner/MissingAttribute.java | 2 +- .../runtime/planner/NamespacedAttribute.java | 133 ++++- .../cel/runtime/planner/PlannedProgram.java | 21 +- .../cel/runtime/planner/ProgramPlanner.java | 12 +- .../runtime/planner/RelativeAttribute.java | 31 +- .../dev/cel/runtime/CelAttributeTest.java | 17 + .../java/dev/cel/runtime/async/BUILD.bazel | 1 + .../async/CelAsyncRuntimeImplTest.java | 57 +- .../java/dev/cel/runtime/planner/BUILD.bazel | 1 + .../runtime/planner/ProgramPlannerTest.java | 548 +++++++++++++++++- .../planner_unknownFieldSelection.baseline | 2 +- 30 files changed, 1528 insertions(+), 105 deletions(-) create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/AttributeResolution.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/EvalIndex.java diff --git a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java index 4f82411a3..ba95cd531 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java @@ -1228,13 +1228,19 @@ private CelVariableResolver fromMap(ImmutableMap m) { } @Test - public void programAdvanceEvaluation_unknownsBasic() throws Exception { + public void programAdvanceEvaluation_unknownsBasic(@TestParameter CelRuntimeFlavor runtimeFlavor) + throws Exception { Cel cel = - standardCelBuilderWithMacros() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + runtimeFlavor + .builder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) .addVar("a", SimpleType.BOOL) .addVar("b", SimpleType.BOOL) - .addFunctionBindings() .setResultType(SimpleType.BOOL) .build(); CelRuntime.Program program = cel.createProgram(cel.compile("a || b").getAst()); @@ -1439,7 +1445,7 @@ public void programAdvanceEvaluation_argumentMergeErrorPriority() throws Excepti CelRuntime.Program program = cel.createProgram(cel.compile("acceptThreeBoolArgs(false, unk, [false][1])").getAst()); - Assert.assertThrows( + assertThrows( CelEvaluationException.class, () -> program.advanceEvaluation( @@ -1634,6 +1640,72 @@ public void programAdvanceEvaluation_unsupportedIndexIgnored() throws Exception .isEqualTo(false); } + @Test + public void programAdvanceEvaluation_sizeList() throws Exception { + Cel cel = + CelRuntimeFlavor.PLANNER + .builder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .addVar("testList", ListType.create(SimpleType.BOOL)) + .setContainer(CelContainer.ofName("")) + .addFunctionBindings() + .setResultType(SimpleType.INT) + .build(); + CelRuntime.Program program = cel.createProgram(cel.compile("size(testList)").getAst()); + Object result = + program.advanceEvaluation( + UnknownContext.create( + fromMap(ImmutableMap.of("testList", ImmutableList.of(true, true, false))), + ImmutableList.of( + CelAttributePattern.create("testList").qualify(Qualifier.ofInt(2))))); + assertThat(result).isEqualTo(3L); + } + + @Test + public void programAdvanceEvaluation_listIndexUnknownElement() throws Exception { + Cel cel = + CelRuntimeFlavor.PLANNER + .builder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .addVar("testList", ListType.create(SimpleType.BOOL)) + .setContainer(CelContainer.ofName("")) + .addFunctionBindings() + .setResultType(SimpleType.BOOL) + .build(); + + CelRuntime.Program program = cel.createProgram(cel.compile("testList[2] == true").getAst()); + + assertThat( + program.advanceEvaluation( + UnknownContext.create( + fromMap(ImmutableMap.of("testList", ImmutableList.of(true, true, false))), + ImmutableList.of( + CelAttributePattern.create("testList").qualify(Qualifier.ofInt(2)))))) + .isEqualTo( + CelUnknownSet.create( + ImmutableSet.of(CelAttribute.create("testList").qualify(Qualifier.ofInt(2))))); + + CelRuntime.Program programIndex1 = + cel.createProgram(cel.compile("testList[1] == true").getAst()); + assertThat( + programIndex1.advanceEvaluation( + UnknownContext.create( + fromMap(ImmutableMap.of("testList", ImmutableList.of(true, true, false))), + ImmutableList.of( + CelAttributePattern.create("testList").qualify(Qualifier.ofInt(2)))))) + .isEqualTo(true); + } + @Test public void programAdvanceEvaluation_listIndexMacroTracking() throws Exception { Cel cel = diff --git a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java index 650c01526..2a347ef08 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java @@ -50,6 +50,7 @@ import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import dev.cel.parser.CelMacro; import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.CelAttribute; import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; @@ -280,7 +281,7 @@ public void optionalOfNonZeroValue_withZeroValue_returnsEmptyOptionalValue( Object result = cel.createProgram(ast).eval(); assertThat(result).isInstanceOf(Optional.class); - assertThat(result).isEqualTo(Optional.empty()); + assertThat((Optional) result).isEmpty(); } @Test @@ -306,7 +307,7 @@ public void optionalOfNonZeroValue_withNullValue_returnsEmptyOptionalValue() thr Object result = cel.createProgram(ast).eval(); assertThat(result).isInstanceOf(Optional.class); - assertThat(result).isEqualTo(Optional.empty()); + assertThat((Optional) result).isEmpty(); } @Test @@ -317,7 +318,7 @@ public void optionalOfNonZeroValue_withEmptyMessage_returnsEmptyOptionalValue() Object result = cel.createProgram(ast).eval(); assertThat(result).isInstanceOf(Optional.class); - assertThat(result).isEqualTo(Optional.empty()); + assertThat((Optional) result).isEmpty(); } @Test @@ -328,7 +329,7 @@ public void optionalNone_success() throws Exception { Object result = cel.createProgram(ast).eval(); assertThat(result).isInstanceOf(Optional.class); - assertThat(result).isEqualTo(Optional.empty()); + assertThat((Optional) result).isEmpty(); } @Test @@ -592,7 +593,7 @@ public void optionalFieldSelection_onMap_returnsOptionalEmpty() throws Exception Object result = cel.createProgram(ast).eval(); - assertThat(result).isEqualTo(Optional.empty()); + assertThat((Optional) result).isEmpty(); } @Test @@ -882,7 +883,7 @@ public void optionalIndex_onOptionalMap_returnsOptionalEmpty() throws Exception cel.createProgram(ast) .eval(ImmutableMap.of("optm", Optional.of(ImmutableMap.of("c", ImmutableMap.of())))); - assertThat(result).isEqualTo(Optional.empty()); + assertThat((Optional) result).isEmpty(); } @Test @@ -900,7 +901,7 @@ public void optionalIndex_onMap_returnsOptionalEmpty() throws Exception { Object result = cel.createProgram(ast).eval(ImmutableMap.of("m", ImmutableMap.of("c", ImmutableMap.of()))); - assertThat(result).isEqualTo(Optional.empty()); + assertThat((Optional) result).isEmpty(); } @Test @@ -982,7 +983,197 @@ public void optionalIndex_onList_returnsOptionalValue() throws Exception { Object result = cel.createProgram(ast).eval(ImmutableMap.of("l", ImmutableList.of("hello"))); - assertThat(result).isEqualTo(Optional.of("hello")); + assertThat((Optional) result).hasValue("hello"); + } + + @Test + public void optionalIndex_onList_negativeIndex_returnsOptionalEmpty() throws Exception { + Cel cel = + newCelBuilder() + .addVar("l", ListType.create(SimpleType.STRING)) + .setResultType(OptionalType.create(SimpleType.STRING)) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "l[?-1]"); + + Object result = cel.createProgram(ast).eval(ImmutableMap.of("l", ImmutableList.of("hello"))); + + assertThat((Optional) result).isEmpty(); + } + + @Test + public void optionalIndex_onList_outOfBoundsIndex_returnsOptionalEmpty() throws Exception { + Cel cel = + newCelBuilder() + .addVar("l", ListType.create(SimpleType.STRING)) + .setResultType(OptionalType.create(SimpleType.STRING)) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "l[?5]"); + + Object result = cel.createProgram(ast).eval(ImmutableMap.of("l", ImmutableList.of("hello"))); + + assertThat((Optional) result).isEmpty(); + } + + @Test + public void optionalIndex_targetIsUnknown_returnsUnknown() throws Exception { + Cel cel = + newCelBuilder() + .addVar("l", ListType.create(SimpleType.STRING)) + .setResultType(OptionalType.create(SimpleType.STRING)) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "l[?0]"); + + Object result = + cel.createProgram(ast) + .eval(PartialVars.of(CelAttributePattern.fromQualifiedIdentifier("l"))); + + assertThat(result).isInstanceOf(CelUnknownSet.class); + } + + @Test + public void optionalIndex_indexIsUnknown_returnsUnknown() throws Exception { + Cel cel = + newCelBuilder() + .addVar("l", ListType.create(SimpleType.STRING)) + .addVar("i", SimpleType.INT) + .setResultType(OptionalType.create(SimpleType.STRING)) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "l[?i]"); + + Object result = + cel.createProgram(ast) + .eval( + PartialVars.of( + ImmutableMap.of("l", ImmutableList.of("hello")), + CelAttributePattern.fromQualifiedIdentifier("i"))); + + assertThat(result).isInstanceOf(CelUnknownSet.class); + } + + @Test + public void optionalIndex_partialUnknownOnList_returnsUnknown() throws Exception { + if (testMode.equals(TestMode.LEGACY_CHECKED)) { + // Legacy runtime executes optional indexing through standard function bindings without attribute + // trail tracking, so it cannot intercept partial sub-attribute unknowns on known containers. + return; + } + + Cel cel = + newCelBuilder() + .addVar("l", ListType.create(SimpleType.STRING)) + .setResultType(OptionalType.create(SimpleType.STRING)) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "l[?1]"); + PartialVars partialVars = + PartialVars.of( + ImmutableMap.of("l", ImmutableList.of("hello", "world")), + CelAttributePattern.fromQualifiedIdentifier("l") + .qualify(CelAttribute.Qualifier.ofInt(1))); + + Object result = cel.createProgram(ast).eval(partialVars); + + assertThat(result).isInstanceOf(CelUnknownSet.class); + } + + @Test + public void optionalIndex_partialUnknownOnList_unrelatedIndexEvaluatesNormally() + throws Exception { + if (testMode.equals(TestMode.LEGACY_CHECKED)) { + // Legacy runtime executes optional indexing through standard function bindings without attribute + // trail tracking, so it cannot intercept partial sub-attribute unknowns on known containers. + return; + } + + Cel cel = + newCelBuilder() + .addVar("l", ListType.create(SimpleType.STRING)) + .setResultType(OptionalType.create(SimpleType.STRING)) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "l[?0]"); + PartialVars partialVars = + PartialVars.of( + ImmutableMap.of("l", ImmutableList.of("hello", "world")), + CelAttributePattern.fromQualifiedIdentifier("l") + .qualify(CelAttribute.Qualifier.ofInt(1))); + + Object result = cel.createProgram(ast).eval(partialVars); + + assertThat((Optional) result).hasValue("hello"); + } + + @Test + public void optionalIndex_partialUnknownOnMap_returnsUnknown() throws Exception { + if (testMode.equals(TestMode.LEGACY_CHECKED)) { + // Legacy runtime executes optional indexing through standard function bindings without attribute + // trail tracking, so it cannot intercept partial sub-attribute unknowns on known containers. + return; + } + + Cel cel = + newCelBuilder() + .addVar("m", MapType.create(SimpleType.STRING, SimpleType.INT)) + .setResultType(OptionalType.create(SimpleType.INT)) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "m[?'b']"); + PartialVars partialVars = + PartialVars.of( + ImmutableMap.of("m", ImmutableMap.of("a", 1, "b", 2)), + CelAttributePattern.fromQualifiedIdentifier("m") + .qualify(CelAttribute.Qualifier.ofString("b"))); + + Object result = cel.createProgram(ast).eval(partialVars); + + assertThat(result).isInstanceOf(CelUnknownSet.class); + } + + @Test + public void optionalIndex_partialUnknownOnMap_unrelatedKeyEvaluatesNormally() throws Exception { + if (testMode.equals(TestMode.LEGACY_CHECKED)) { + // Legacy runtime executes optional indexing through standard function bindings without attribute + // trail tracking, so it cannot intercept partial sub-attribute unknowns on known containers. + return; + } + + Cel cel = + newCelBuilder() + .addVar("m", MapType.create(SimpleType.STRING, SimpleType.INT)) + .setResultType(OptionalType.create(SimpleType.INT)) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "m[?'a']"); + PartialVars partialVars = + PartialVars.of( + ImmutableMap.of("m", ImmutableMap.of("a", 1, "b", 2)), + CelAttributePattern.fromQualifiedIdentifier("m") + .qualify(CelAttribute.Qualifier.ofString("b"))); + + Object result = cel.createProgram(ast).eval(partialVars); + + assertThat((Optional) result).hasValue(1); + } + + @Test + public void optionalIndex_partialUnknownOnMap_missingKeyEvaluatesToEmpty() throws Exception { + if (testMode.equals(TestMode.LEGACY_CHECKED)) { + // Legacy runtime executes optional indexing through standard function bindings without attribute + // trail tracking, so it cannot intercept partial sub-attribute unknowns on known containers. + return; + } + + Cel cel = + newCelBuilder() + .addVar("m", MapType.create(SimpleType.STRING, SimpleType.INT)) + .setResultType(OptionalType.create(SimpleType.INT)) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "m[?'c']"); + PartialVars partialVars = + PartialVars.of( + ImmutableMap.of("m", ImmutableMap.of("a", 1, "b", 2)), + CelAttributePattern.fromQualifiedIdentifier("m") + .qualify(CelAttribute.Qualifier.ofString("b"))); + + Object result = cel.createProgram(ast).eval(partialVars); + + assertThat((Optional) result).isEmpty(); } @Test @@ -1013,7 +1204,7 @@ public void optionalIndex_onOptionalList_returnsOptionalValue() throws Exception cel.createProgram(ast) .eval(ImmutableMap.of("optl", Optional.of(ImmutableList.of("hello")))); - assertThat(result).isEqualTo(Optional.of("hello")); + assertThat((Optional) result).hasValue("hello"); } @Test @@ -1043,13 +1234,13 @@ public void traditionalIndex_onOptionalList_returnsOptionalEmpty() throws Except Object result = cel.createProgram(ast).eval(ImmutableMap.of("optl", Optional.empty())); - assertThat(result).isEqualTo(Optional.empty()); + assertThat((Optional) result).isEmpty(); } @Test public void optionalFieldSelect_fieldMarkedUnknown_returnsUnknownSet() throws Exception { if (testMode.equals(TestMode.LEGACY_CHECKED)) { - // This case is not possible to setup for legacy runtime + // Legacy runtime does not support attribute trail tracking for optional field selection (.?field). return; } diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index c87fadca9..f377e42be 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -379,3 +379,9 @@ cel_android_library( name = "partial_vars_android", exports = ["//runtime/src/main/java/dev/cel/runtime:partial_vars_android"], ) + +cel_android_library( + name = "function_resolver_android", + visibility = ["//:internal"], + exports = ["//runtime/src/main/java/dev/cel/runtime:function_resolver_android"], +) diff --git a/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java b/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java index d4d54c71f..e512b38ca 100644 --- a/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java +++ b/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.Set; import org.jspecify.annotations.Nullable; @@ -36,12 +37,12 @@ public final class AccumulatedUnknowns { private final Set exprIds; private final Set attributes; - Set exprIds() { - return exprIds; + public Set exprIds() { + return Collections.unmodifiableSet(exprIds); } - Set attributes() { - return attributes; + public Set attributes() { + return Collections.unmodifiableSet(attributes); } /** diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 489bb64d8..0b54bdcd5 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -773,6 +773,8 @@ java_library( cel_android_library( name = "function_resolver_android", srcs = ["CelFunctionResolver.java"], + tags = [ + ], deps = [ ":evaluation_exception", ":resolved_overload_android", diff --git a/runtime/src/main/java/dev/cel/runtime/CelAttribute.java b/runtime/src/main/java/dev/cel/runtime/CelAttribute.java index f04418e0c..9cd082ca1 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/CelAttribute.java @@ -21,6 +21,7 @@ import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.Immutable; import com.google.re2j.Pattern; +import org.jspecify.annotations.Nullable; /** * CelAttribute represents the select path from the root (.) to a single leaf value that may be @@ -100,16 +101,27 @@ public static Qualifier ofWildCard() { * index. */ public static Qualifier fromGeneric(Object value) { + Qualifier qualifier = fromGenericOrNull(value); + if (qualifier != null) { + return qualifier; + } + throw new IllegalArgumentException("Unsupported attribute qualifier kind"); + } + + @SuppressWarnings("IfChainToSwitch") + public static @Nullable Qualifier fromGenericOrNull(Object value) { if (value instanceof UnsignedLong) { return ofUint((UnsignedLong) value); } else if (value instanceof Long) { return ofInt((Long) value); + } else if (value instanceof Integer) { + return ofInt(((Integer) value).longValue()); } else if (value instanceof Boolean) { return ofBool((boolean) value); } else if (value instanceof String) { return ofString((String) value); } - throw new IllegalArgumentException("Unsupported attribute qualifier kind"); + return null; } public String toIndexFormat() { diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index 5cda25800..c280f2c23 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -235,7 +235,16 @@ public Object trace(PartialVars partialVars, CelEvaluationListener listener) @Override public Object advanceEvaluation(UnknownContext context) throws CelEvaluationException { - throw new UnsupportedOperationException("Unsupported operation."); + PlannedProgram plannedProgram = (PlannedProgram) program; + return plannedProgram.evalOrThrow( + plannedProgram.interpretable(), + context.variableResolver(), + EMPTY_FUNCTION_RESOLVER, + PartialVars.of( + (name) -> Optional.ofNullable(context.variableResolver().resolve(name)), + context.unresolvedAttributes()), + context.createAttributeResolver(), + /* listener= */ null); } }; } diff --git a/runtime/src/main/java/dev/cel/runtime/UnknownContext.java b/runtime/src/main/java/dev/cel/runtime/UnknownContext.java index c494ff252..ef17b740d 100644 --- a/runtime/src/main/java/dev/cel/runtime/UnknownContext.java +++ b/runtime/src/main/java/dev/cel/runtime/UnknownContext.java @@ -107,6 +107,16 @@ public GlobalResolver variableResolver() { return variableResolver; } + /** Accessor for unresolved attribute patterns. */ + ImmutableList unresolvedAttributes() { + return unresolvedAttributes; + } + + /** Accessor for resolved attribute values. */ + ImmutableMap resolvedAttributes() { + return resolvedAttributes; + } + /** * Creates a new unknown context that is a copy of the current context with the provided * additional attribute values. @@ -123,7 +133,7 @@ public UnknownContext withResolvedAttributes(Map resolvedA ImmutableMap.builder() .putAll(this.resolvedAttributes) .putAll(resolvedAttributes) - .buildOrThrow()); + .buildKeepingLast()); } private boolean patternMaskedByResolvedAttribute( @@ -168,10 +178,27 @@ public Optional resolve(CelAttribute attribute) { @Override public Optional maybePartialUnknown(CelAttribute attribute) { - return unresolvedAttributes.stream() - .filter(p -> p.isPartialMatch(attribute)) - .findFirst() - .map(p -> CelUnknownSet.create(p.simplify(attribute))); + if (attribute.equals(CelAttribute.EMPTY) || attribute.qualifiers().isEmpty()) { + return Optional.empty(); + } + Optional fromUnresolved = + unresolvedAttributes.stream() + .filter(p -> p.isPartialMatch(attribute)) + .findFirst() + .map(p -> CelUnknownSet.create(p.simplify(attribute))); + if (fromUnresolved.isPresent()) { + return fromUnresolved; + } + for (CelAttribute resolved : resolvedAttributes.keySet()) { + if (resolved.qualifiers().size() > attribute.qualifiers().size() + && resolved + .qualifiers() + .subList(0, attribute.qualifiers().size()) + .equals(attribute.qualifiers())) { + return Optional.of(CelUnknownSet.create(attribute)); + } + } + return Optional.empty(); } } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java b/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java index 90165c1ac..03ed34349 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java @@ -20,7 +20,7 @@ /** Represents a resolvable symbol or path (such as a variable or a field selection). */ @Immutable interface Attribute { - Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame); + AttributeResolution resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame); Attribute addQualifier(Qualifier qualifier); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/AttributeResolution.java b/runtime/src/main/java/dev/cel/runtime/planner/AttributeResolution.java new file mode 100644 index 000000000..0364c3af8 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/AttributeResolution.java @@ -0,0 +1,50 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.runtime.CelAttribute; +import org.jspecify.annotations.Nullable; + +/** Bundles a resolved value and its corresponding {@link CelAttribute} trail. */ +@Immutable +final class AttributeResolution { + + @SuppressWarnings("Immutable") + private final @Nullable Object value; + + private final @Nullable CelAttribute attribute; + + static AttributeResolution of(@Nullable Object value, @Nullable CelAttribute attribute) { + return new AttributeResolution(value, attribute); + } + + static AttributeResolution ofValue(@Nullable Object value) { + return new AttributeResolution(value, null); + } + + @Nullable Object value() { + return value; + } + + @Nullable CelAttribute attribute() { + return attribute; + } + + private AttributeResolution(@Nullable Object value, @Nullable CelAttribute attribute) { + this.value = value; + this.attribute = attribute; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index e05fca9b4..97509c6ff 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -29,6 +29,7 @@ java_library( ":eval_exhaustive_conditional", ":eval_exhaustive_or", ":eval_fold", + ":eval_index", ":eval_late_bound_call", ":eval_optional_or", ":eval_optional_or_value", @@ -89,6 +90,7 @@ java_library( "//runtime:partial_vars", "//runtime:program", "//runtime:resolved_overload", + "//runtime:unknown_attributes", "//runtime:variable_resolver", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:org_jspecify_jspecify", @@ -111,6 +113,7 @@ java_library( srcs = [ "Attribute.java", "AttributeFactory.java", + "AttributeResolution.java", "InterpretableAttribute.java", "MaybeAttribute.java", "MissingAttribute.java", @@ -132,7 +135,6 @@ java_library( "//common/values", "//runtime:accumulated_unknowns", "//runtime:interpretable", - "//runtime:interpreter_util", "//runtime:partial_vars", "//runtime:unknown_attributes", "@maven//:com_google_errorprone_error_prone_annotations", @@ -227,6 +229,29 @@ java_library( "//runtime:evaluation_exception", "//runtime:interpretable", "//runtime:resolved_overload", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "eval_index", + srcs = ["EvalIndex.java"], + deps = [ + ":attribute", + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:evaluation_exception", + "//runtime:evaluation_listener", + "//runtime:interpretable", + "//runtime:interpreter_util", + "//runtime:partial_vars", + "//runtime:resolved_overload", + "//runtime:unknown_attributes", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", ], ) @@ -518,6 +543,7 @@ java_library( "//runtime:interpreter_util", "//runtime:partial_vars", "//runtime:resolved_overload", + "//runtime:unknown_attributes", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:org_jspecify_jspecify", ], @@ -544,6 +570,7 @@ cel_android_library( ":eval_exhaustive_conditional_android", ":eval_exhaustive_or_android", ":eval_fold_android", + ":eval_index_android", ":eval_late_bound_call_android", ":eval_optional_or_android", ":eval_optional_or_value_android", @@ -596,6 +623,7 @@ cel_android_library( "//runtime:evaluation_exception_builder", "//runtime:interpretable_android", "//runtime:resolved_overload_android", + "//runtime:unknown_attributes_android", "//runtime:variable_resolver", "//runtime/src/main/java/dev/cel/runtime:evaluation_listener_android", "//runtime/src/main/java/dev/cel/runtime:function_resolver_android", @@ -623,6 +651,7 @@ cel_android_library( srcs = [ "Attribute.java", "AttributeFactory.java", + "AttributeResolution.java", "InterpretableAttribute.java", "MaybeAttribute.java", "MissingAttribute.java", @@ -645,7 +674,6 @@ cel_android_library( "//runtime:interpretable_android", "//runtime:unknown_attributes_android", "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", - "//runtime/src/main/java/dev/cel/runtime:interpreter_util_android", "//runtime/src/main/java/dev/cel/runtime:partial_vars_android", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:org_jspecify_jspecify", @@ -731,6 +759,23 @@ cel_android_library( name = "eval_binary_android", srcs = ["EvalBinary.java"], deps = [ + ":eval_helpers_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/values:values_android", + "//runtime:accumulated_unknowns_android", + "//runtime:evaluation_exception", + "//runtime:interpretable_android", + "//runtime:resolved_overload_android", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "eval_index_android", + srcs = ["EvalIndex.java"], + deps = [ + ":attribute_android", ":eval_helpers_android", ":planned_interpretable_android", "//common/ast:ast_android", @@ -738,7 +783,13 @@ cel_android_library( "//runtime:evaluation_exception", "//runtime:interpretable_android", "//runtime:resolved_overload_android", + "//runtime:unknown_attributes_android", "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", + "//runtime/src/main/java/dev/cel/runtime:evaluation_listener_android", + "//runtime/src/main/java/dev/cel/runtime:interpreter_util_android", + "//runtime/src/main/java/dev/cel/runtime:partial_vars_android", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", ], ) @@ -1023,6 +1074,7 @@ cel_android_library( "//runtime:interpretable_android", "//runtime:interpreter_util_android", "//runtime:resolved_overload_android", + "//runtime:unknown_attributes_android", "//runtime/src/main/java/dev/cel/runtime:function_resolver_android", "//runtime/src/main/java/dev/cel/runtime:partial_vars_android", "@maven//:com_google_errorprone_error_prone_annotations", diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java index 56ea8a832..4b4159103 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java @@ -25,7 +25,8 @@ final class EvalAttribute extends InterpretableAttribute { @Override Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { - Object resolved = attr.resolve(expr().id(), resolver, frame); + AttributeResolution resolution = resolveWithAttribute(resolver, frame); + Object resolved = resolution.value(); if (resolved instanceof MissingAttribute) { ((MissingAttribute) resolved).resolve(expr().id(), resolver, frame); } @@ -34,7 +35,12 @@ Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { } @Override - public EvalAttribute addQualifier(CelExpr expr, Qualifier qualifier) { + AttributeResolution resolveWithAttribute(GlobalResolver resolver, ExecutionFrame frame) { + return attr.resolve(expr().id(), resolver, frame); + } + + @Override + EvalAttribute addQualifier(CelExpr expr, Qualifier qualifier) { Attribute newAttribute = attr.addQualifier(qualifier); return create(expr, newAttribute); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java index 1713195ab..dbc805ca8 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java @@ -17,6 +17,7 @@ import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; import static dev.cel.runtime.planner.EvalHelpers.evalStrictly; +import com.google.errorprone.annotations.Immutable; import dev.cel.common.ast.CelExpr; import dev.cel.common.values.CelValueConverter; import dev.cel.runtime.AccumulatedUnknowns; @@ -24,6 +25,7 @@ import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.GlobalResolver; +@Immutable final class EvalBinary extends PlannedInterpretable { private final String functionName; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java index 1b8d61234..d5ce8d7df 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java @@ -101,6 +101,21 @@ static Object dispatch( } } + /** + * Enforces strictness on a resolved attribute or variable value and adapts unknown sets. + * + *

If the value is a {@link RuntimeException} (e.g. from an asynchronous resolver), strictness + * is enforced by throwing it. Otherwise, {@link CelUnknownSet} is adapted into {@link + * AccumulatedUnknowns}. + */ + static Object enforceStrictnessAndAdaptUnknowns(Object resolvedVal) { + if (resolvedVal instanceof RuntimeException) { + throw (RuntimeException) resolvedVal; + } + + return InterpreterUtil.maybeAdaptToAccumulatedUnknowns(resolvedVal); + } + /** * Converts the raw invocation result into a CEL runtime value, unwraps it if necessary, and * adapts any public {@link CelUnknownSet} instances into internal {@link AccumulatedUnknowns} for diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalIndex.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalIndex.java new file mode 100644 index 000000000..9cf99c701 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalIndex.java @@ -0,0 +1,243 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static dev.cel.runtime.planner.EvalHelpers.enforceStrictnessAndAdaptUnknowns; +import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; +import static dev.cel.runtime.planner.EvalHelpers.evalStrictly; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.CelAttribute; +import dev.cel.runtime.CelAttributePattern; +import dev.cel.runtime.CelAttributeResolver; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelEvaluationListener; +import dev.cel.runtime.CelResolvedOverload; +import dev.cel.runtime.CelUnknownSet; +import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.InterpreterUtil; +import dev.cel.runtime.PartialVars; +import java.util.Objects; +import java.util.Optional; +import org.jspecify.annotations.Nullable; + +@Immutable +final class EvalIndex extends PlannedInterpretable { + + private final String functionName; + private final CelResolvedOverload resolvedOverload; + private final PlannedInterpretable target; + private final PlannedInterpretable index; + private final CelValueConverter celValueConverter; + + @Override + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + boolean isStrict = resolvedOverload.isStrict(); + Object targetVal; + CelAttribute targetAttr = null; + + if (target instanceof InterpretableAttribute) { + AttributeResolution res = + ((InterpretableAttribute) target).resolveWithAttribute(resolver, frame); + targetVal = res.value(); + targetAttr = res.attribute(); + CelEvaluationListener listener = frame.getListener(); + if (listener != null && !(targetVal instanceof MissingAttribute)) { + listener.callback(target.expr(), InterpreterUtil.maybeAdaptToCelUnknownSet(targetVal)); + } + } else { + targetVal = + isStrict + ? evalStrictly(target, resolver, frame) + : evalNonstrictly(target, resolver, frame); + } + + Object indexVal = + isStrict ? evalStrictly(index, resolver, frame) : evalNonstrictly(index, resolver, frame); + + if (targetVal instanceof AccumulatedUnknowns) { + Object indexUnknownResult = + maybeEvaluateIndexUnknown((AccumulatedUnknowns) targetVal, indexVal, frame); + if (indexUnknownResult != null) { + return indexUnknownResult; + } + } + + if (targetAttr != null) { + Object attrUnknownResult = maybeEvaluateAttributeIndexUnknown(targetAttr, indexVal, frame); + if (attrUnknownResult != null) { + return attrUnknownResult; + } + } + + if (targetVal instanceof MissingAttribute) { + ((MissingAttribute) targetVal).resolve(target.expr().id(), resolver, frame); + } + + if (isStrict) { + AccumulatedUnknowns unknowns = AccumulatedUnknowns.maybeMerge(null, targetVal); + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, indexVal); + if (unknowns != null) { + return unknowns; + } + } + + return EvalHelpers.dispatch( + functionName, resolvedOverload, celValueConverter, targetVal, indexVal); + } + + private @Nullable Object maybeEvaluateAttributeIndexUnknown( + CelAttribute targetAttr, Object indexVal, ExecutionFrame frame) { + if (!frame.hasUnknownResolvers()) { + return null; + } + CelAttribute.Qualifier qualifier = CelAttribute.Qualifier.fromGenericOrNull(indexVal); + if (qualifier == null) { + return null; + } + + CelAttribute indexedAttr = targetAttr.qualify(qualifier); + + CelAttributeResolver attributeResolver = frame.getAttributeResolver(); + if (attributeResolver != null) { + Optional resolved = attributeResolver.resolve(indexedAttr); + if (resolved.isPresent()) { + return enforceStrictnessAndAdaptUnknowns(resolved.get()); + } + + Optional partialUnknown = attributeResolver.maybePartialUnknown(indexedAttr); + if (partialUnknown.isPresent()) { + return AccumulatedUnknowns.create( + ImmutableList.of(expr().id()), partialUnknown.get().attributes()); + } + } + + PartialVars partialVars = frame.getPartialVars(); + if (partialVars != null) { + for (CelAttributePattern pattern : partialVars.unknowns()) { + if (pattern.isPartialMatch(indexedAttr)) { + return AccumulatedUnknowns.create( + ImmutableList.of(expr().id()), ImmutableList.of(pattern.simplify(indexedAttr))); + } + } + } + + return null; + } + + private @Nullable Object maybeEvaluateIndexUnknown( + AccumulatedUnknowns targetUnknowns, Object indexVal, ExecutionFrame frame) { + if (targetUnknowns.attributes().isEmpty()) { + return targetUnknowns; + } + + CelAttribute.Qualifier qualifier = CelAttribute.Qualifier.fromGenericOrNull(indexVal); + if (qualifier == null) { + return null; + } + + CelAttributeResolver attributeResolver = frame.getAttributeResolver(); + PartialVars partialVars = frame.getPartialVars(); + + if (targetUnknowns.attributes().size() == 1) { + CelAttribute singleAttr = targetUnknowns.attributes().iterator().next(); + CelAttribute qualifiedAttr = singleAttr.qualify(qualifier); + if (attributeResolver != null) { + Optional resolved = attributeResolver.resolve(qualifiedAttr); + if (resolved.isPresent()) { + return enforceStrictnessAndAdaptUnknowns(resolved.get()); + } + } + return AccumulatedUnknowns.create( + targetUnknowns.exprIds(), + ImmutableList.of(simplifyAttribute(qualifiedAttr, partialVars))); + } + + ImmutableList.Builder remainingUnknowns = ImmutableList.builder(); + for (CelAttribute attr : targetUnknowns.attributes()) { + CelAttribute qualifiedAttr = attr.qualify(qualifier); + if (attributeResolver != null && attributeResolver.resolve(qualifiedAttr).isPresent()) { + continue; + } + remainingUnknowns.add(simplifyAttribute(qualifiedAttr, partialVars)); + } + ImmutableList remaining = remainingUnknowns.build(); + if (remaining.isEmpty()) { + if (attributeResolver == null) { + return targetUnknowns; + } + Object firstVal = null; + boolean first = true; + for (CelAttribute attr : targetUnknowns.attributes()) { + CelAttribute qualifiedAttr = attr.qualify(qualifier); + Optional resolved = attributeResolver.resolve(qualifiedAttr); + if (!resolved.isPresent()) { + return targetUnknowns; + } + if (first) { + firstVal = resolved.get(); + first = false; + } else if (!Objects.equals(firstVal, resolved.get())) { + return targetUnknowns; + } + } + return firstVal != null ? enforceStrictnessAndAdaptUnknowns(firstVal) : targetUnknowns; + } + + return AccumulatedUnknowns.create(targetUnknowns.exprIds(), remaining); + } + + private static CelAttribute simplifyAttribute( + CelAttribute qualifiedAttr, @Nullable PartialVars partialVars) { + if (partialVars == null) { + return qualifiedAttr; + } + for (CelAttributePattern pattern : partialVars.unknowns()) { + if (pattern.isPartialMatch(qualifiedAttr)) { + return pattern.simplify(qualifiedAttr); + } + } + return qualifiedAttr; + } + + static EvalIndex create( + CelExpr expr, + String functionName, + CelResolvedOverload resolvedOverload, + PlannedInterpretable target, + PlannedInterpretable index, + CelValueConverter celValueConverter) { + return new EvalIndex(expr, functionName, resolvedOverload, target, index, celValueConverter); + } + + private EvalIndex( + CelExpr expr, + String functionName, + CelResolvedOverload resolvedOverload, + PlannedInterpretable target, + PlannedInterpretable index, + CelValueConverter celValueConverter) { + super(expr); + this.functionName = functionName; + this.resolvedOverload = resolvedOverload; + this.target = target; + this.index = index; + this.celValueConverter = celValueConverter; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalTestOnly.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalTestOnly.java index b3d2563f0..1e44b7fc7 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalTestOnly.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalTestOnly.java @@ -30,7 +30,12 @@ Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEva } @Override - public EvalTestOnly addQualifier(CelExpr expr, Qualifier qualifier) { + AttributeResolution resolveWithAttribute(GlobalResolver resolver, ExecutionFrame frame) { + return attr.resolveWithAttribute(resolver, frame); + } + + @Override + EvalTestOnly addQualifier(CelExpr expr, Qualifier qualifier) { PresenceTestQualifier presenceTestQualifier = PresenceTestQualifier.create(qualifier.value()); return new EvalTestOnly(expr(), attr.addQualifier(expr, presenceTestQualifier)); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java b/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java index b67f5520c..db59830c0 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java @@ -16,6 +16,7 @@ import dev.cel.common.CelOptions; import dev.cel.common.exceptions.CelIterationLimitExceededException; +import dev.cel.runtime.CelAttributeResolver; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelEvaluationListener; import dev.cel.runtime.CelFunctionResolver; @@ -31,6 +32,7 @@ final class ExecutionFrame { private final int comprehensionIterationLimit; private final CelFunctionResolver functionResolver; private final PartialVars partialVars; + private final @Nullable CelAttributeResolver attributeResolver; private final @Nullable CelEvaluationListener listener; private int iterationCount; private BlockMemoizer blockMemoizer; @@ -68,13 +70,35 @@ static ExecutionFrame create( CelFunctionResolver functionResolver, CelOptions celOptions, @Nullable PartialVars partialVars, + @Nullable CelAttributeResolver attributeResolver, @Nullable CelEvaluationListener listener) { return new ExecutionFrame( - functionResolver, celOptions.comprehensionMaxIterations(), partialVars, listener); + functionResolver, + celOptions.comprehensionMaxIterations(), + partialVars, + attributeResolver, + listener); } - Optional partialVars() { - return Optional.ofNullable(partialVars); + static ExecutionFrame create( + CelFunctionResolver functionResolver, + CelOptions celOptions, + @Nullable PartialVars partialVars, + @Nullable CelEvaluationListener listener) { + return create( + functionResolver, celOptions, partialVars, /* attributeResolver= */ null, listener); + } + + boolean hasUnknownResolvers() { + return attributeResolver != null || partialVars != null; + } + + @Nullable PartialVars getPartialVars() { + return partialVars; + } + + @Nullable CelAttributeResolver getAttributeResolver() { + return attributeResolver; } @Nullable CelEvaluationListener getListener() { @@ -85,10 +109,12 @@ private ExecutionFrame( CelFunctionResolver functionResolver, int limit, @Nullable PartialVars partialVars, + @Nullable CelAttributeResolver attributeResolver, @Nullable CelEvaluationListener listener) { this.comprehensionIterationLimit = limit; this.functionResolver = functionResolver; this.partialVars = partialVars; + this.attributeResolver = attributeResolver; this.listener = listener; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/InterpretableAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/InterpretableAttribute.java index 9ce726f0e..d8d9f9cc0 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/InterpretableAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/InterpretableAttribute.java @@ -16,12 +16,15 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.common.ast.CelExpr; +import dev.cel.runtime.GlobalResolver; @Immutable abstract class InterpretableAttribute extends PlannedInterpretable { abstract InterpretableAttribute addQualifier(CelExpr expr, Qualifier qualifier); + abstract AttributeResolution resolveWithAttribute(GlobalResolver resolver, ExecutionFrame frame); + InterpretableAttribute(CelExpr expr) { super(expr); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java index 1506eb180..7699f442a 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java @@ -16,6 +16,7 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; +import dev.cel.runtime.CelAttribute; import dev.cel.runtime.GlobalResolver; /** @@ -28,25 +29,30 @@ final class MaybeAttribute implements Attribute { private final ImmutableList attributes; @Override - public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { + public AttributeResolution resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { MissingAttribute maybeError = null; + CelAttribute fallbackAttr = null; for (NamespacedAttribute attr : attributes) { - Object value = attr.resolve(exprId, ctx, frame); + AttributeResolution resolution = attr.resolve(exprId, ctx, frame); + Object value = resolution.value(); if (value == null) { continue; } if (value instanceof MissingAttribute) { maybeError = (MissingAttribute) value; + if (fallbackAttr == null && resolution.attribute() != null) { + fallbackAttr = resolution.attribute(); + } // When the variable is missing in a maybe attribute, defer erroring. // The variable may exist in other namespaced attributes. continue; } - return value; + return resolution; } - return maybeError; + return AttributeResolution.of(maybeError, fallbackAttr); } @Override diff --git a/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java index 46af3c701..403691632 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java @@ -27,7 +27,7 @@ final class MissingAttribute implements Attribute { private final Kind kind; @Override - public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { + public AttributeResolution resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { switch (kind) { case ATTRIBUTE_NOT_FOUND: throw CelAttributeNotFoundException.forMissingAttributes(missingAttributes); diff --git a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java index 01673923d..701c208f3 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java @@ -14,9 +14,12 @@ package dev.cel.runtime.planner; +import static dev.cel.runtime.planner.EvalHelpers.enforceStrictnessAndAdaptUnknowns; + import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import com.google.errorprone.annotations.Immutable; import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypeProvider; @@ -27,8 +30,9 @@ import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelAttribute; import dev.cel.runtime.CelAttributePattern; +import dev.cel.runtime.CelAttributeResolver; +import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.GlobalResolver; -import dev.cel.runtime.InterpreterUtil; import dev.cel.runtime.PartialVars; import java.util.Map; import java.util.NoSuchElementException; @@ -52,7 +56,7 @@ ImmutableSet candidateVariableNames() { } @Override - public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { + public AttributeResolution resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { GlobalResolver inputVars = ctx; // Unwrap any local activations to ensure that we reach the variables provided as input // to the expression in the event that we need to disambiguate between global and local @@ -63,44 +67,115 @@ public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { for (Map.Entry entry : candidateAttributes.entrySet()) { String name = entry.getKey(); - CelAttribute attr = entry.getValue(); + CelAttribute candidateAttr = entry.getValue(); + GlobalResolver resolver = disambiguateNames ? inputVars : ctx; + + Object value; + CelAttribute fullyQualifiedAttr = null; + if (!isLocallyBound(resolver, name)) { + if (frame.hasUnknownResolvers()) { + fullyQualifiedAttr = qualify(candidateAttr, qualifiers); + + // Check if the fully-qualified attribute is resolved or unknown + Object fullyQualifiedResult = + maybeResolveFullyQualified(exprId, fullyQualifiedAttr, frame); + if (fullyQualifiedResult != null) { + return AttributeResolution.of(fullyQualifiedResult, fullyQualifiedAttr); + } + } - GlobalResolver resolver = ctx; - if (disambiguateNames) { - resolver = inputVars; + // Resolve the base attribute (via iterative resolver or standard variable resolver) + value = maybeResolveBaseAttribute(candidateAttr, resolver, name, frame); + } else { + // Locally bound variable (e.g. comprehension variable) + Object rawValue = resolver.resolve(name); + value = rawValue != null ? enforceStrictnessAndAdaptUnknowns(rawValue) : null; } - Object value = resolver.resolve(name); - value = InterpreterUtil.maybeAdaptToAccumulatedUnknowns(value); + if (value != null) { + Object resolvedValue; + if (value instanceof AccumulatedUnknowns && fullyQualifiedAttr != null) { + resolvedValue = + AccumulatedUnknowns.create( + ((AccumulatedUnknowns) value).exprIds(), ImmutableList.of(fullyQualifiedAttr)); + } else { + resolvedValue = applyQualifiers(value, celValueConverter, qualifiers); + } + return AttributeResolution.of(resolvedValue, fullyQualifiedAttr); + } - PartialVars partialVars = frame.partialVars().orElse(null); + // Fallback: Attempt to resolve as a qualified type name or enum value + value = findIdent(name); + if (value != null) { + return AttributeResolution.ofValue(value); + } + } - if (partialVars != null && !isLocallyBound(resolver, name)) { - ImmutableList patterns = partialVars.unknowns(); - // Avoid enhanced for loop to prevent UnmodifiableIterator from being allocated - for (int i = 0; i < qualifiers.size(); i++) { - attr = attr.qualify(CelAttribute.Qualifier.fromGeneric(qualifiers.get(i).value())); - } + CelAttribute fallbackAttr = + frame.hasUnknownResolvers() && !candidateAttributes.isEmpty() + ? qualify(Iterables.getLast(candidateAttributes.values()), qualifiers) + : null; + return AttributeResolution.of( + MissingAttribute.newMissingAttribute(candidateAttributes.keySet()), fallbackAttr); + } - CelAttributePattern partialMatch = findPartialMatchingPattern(attr, patterns).orElse(null); - if (partialMatch != null) { - return AccumulatedUnknowns.create( - ImmutableList.of(exprId), ImmutableList.of(partialMatch.simplify(attr))); - } + private static CelAttribute qualify(CelAttribute baseAttr, ImmutableList qualifiers) { + CelAttribute attr = baseAttr; + // Avoid enhanced for loop to prevent UnmodifiableIterator from being allocated + for (int i = 0; i < qualifiers.size(); i++) { + attr = attr.qualify(CelAttribute.Qualifier.fromGeneric(qualifiers.get(i).value())); + } + return attr; + } + + private static @Nullable Object maybeResolveFullyQualified( + long exprId, CelAttribute fullyQualifiedAttr, ExecutionFrame frame) { + // Check iterative eval AttributeResolver + CelAttributeResolver attributeResolver = frame.getAttributeResolver(); + if (attributeResolver != null) { + Optional resolved = attributeResolver.resolve(fullyQualifiedAttr); + if (resolved.isPresent()) { + return enforceStrictnessAndAdaptUnknowns(resolved.get()); } + } - if (value != null) { - return applyQualifiers(value, celValueConverter, qualifiers); + // Check batch PartialVars unknown patterns + PartialVars partialVars = frame.getPartialVars(); + if (partialVars != null) { + ImmutableList patterns = partialVars.unknowns(); + CelAttributePattern match = findMatchingPattern(fullyQualifiedAttr, patterns).orElse(null); + if (match != null) { + return AccumulatedUnknowns.create( + ImmutableList.of(exprId), ImmutableList.of(match.simplify(fullyQualifiedAttr))); } + } - // Attempt to resolve the qualify type name if the name is not a variable identifier - value = findIdent(name); - if (value != null) { - return value; + return null; + } + + private static @Nullable Object maybeResolveBaseAttribute( + CelAttribute candidateAttr, GlobalResolver resolver, String name, ExecutionFrame frame) { + // Standard variable resolution + Object rawValue = resolver.resolve(name); + if (rawValue != null) { + return enforceStrictnessAndAdaptUnknowns(rawValue); + } + + // Check iterative eval AttributeResolver + CelAttributeResolver attributeResolver = frame.getAttributeResolver(); + if (attributeResolver != null) { + Optional baseResolved = attributeResolver.resolve(candidateAttr); + if (baseResolved.isPresent()) { + return enforceStrictnessAndAdaptUnknowns(baseResolved.get()); + } + + Optional partialUnknown = attributeResolver.maybePartialUnknown(candidateAttr); + if (partialUnknown.isPresent()) { + return AccumulatedUnknowns.create(ImmutableList.of(), partialUnknown.get().attributes()); } } - return MissingAttribute.newMissingAttribute(candidateAttributes.keySet()); + return null; } private @Nullable Object findIdent(String name) { @@ -199,10 +274,10 @@ private static Object applyQualifiers( return celValueConverter.maybeUnwrap(obj); } - private static Optional findPartialMatchingPattern( + private static Optional findMatchingPattern( CelAttribute attr, ImmutableList patterns) { for (CelAttributePattern pattern : patterns) { - if (pattern.isPartialMatch(attr)) { + if (pattern.isMatch(attr)) { return Optional.of(pattern); } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java index 1470e4909..006ac20fe 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java @@ -21,6 +21,7 @@ import dev.cel.common.exceptions.CelRuntimeException; import dev.cel.common.values.ErrorValue; import dev.cel.runtime.Activation; +import dev.cel.runtime.CelAttributeResolver; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelEvaluationExceptionBuilder; import dev.cel.runtime.CelEvaluationListener; @@ -134,11 +135,13 @@ public Object evalOrThrow( GlobalResolver resolver, CelFunctionResolver functionResolver, @Nullable PartialVars partialVars, + @Nullable CelAttributeResolver attributeResolver, @Nullable CelEvaluationListener listener) throws CelEvaluationException { try { ExecutionFrame frame = - ExecutionFrame.create(functionResolver, options(), partialVars, listener); + ExecutionFrame.create( + functionResolver, options(), partialVars, attributeResolver, listener); Object evalResult = interpretable.eval(resolver, frame); if (evalResult instanceof ErrorValue) { ErrorValue errorValue = (ErrorValue) evalResult; @@ -151,6 +154,22 @@ public Object evalOrThrow( } } + public Object evalOrThrow( + PlannedInterpretable interpretable, + GlobalResolver resolver, + CelFunctionResolver functionResolver, + @Nullable PartialVars partialVars, + @Nullable CelEvaluationListener listener) + throws CelEvaluationException { + return evalOrThrow( + interpretable, + resolver, + functionResolver, + partialVars, + /* attributeResolver= */ null, + listener); + } + public Object trace( GlobalResolver resolver, CelFunctionResolver functionResolver, diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index 77f605efc..720aa4db7 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -327,6 +327,16 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { return EvalUnary.create( expr, functionName, resolvedOverload, evaluatedArgs[0], celValueConverter); case 2: + if (functionName.equals(Operator.INDEX.getFunction()) + || functionName.equals(Operator.OPTIONAL_INDEX.getFunction())) { + return EvalIndex.create( + expr, + functionName, + resolvedOverload, + evaluatedArgs[0], + evaluatedArgs[1], + celValueConverter); + } return EvalBinary.create( expr, functionName, @@ -385,7 +395,7 @@ private Optional maybeInterceptOptionalCalls( break; } - if (Operator.OPTIONAL_SELECT.getFunction().equals(functionName)) { + if (functionName.equals(Operator.OPTIONAL_SELECT.getFunction())) { String field = expr.call().args().get(1).constant().stringValue(); InterpretableAttribute attribute; if (evaluatedArgs[0] instanceof EvalAttribute) { diff --git a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java index 38f733c79..91c1f108d 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java @@ -18,6 +18,7 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.common.values.CelValueConverter; import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.CelAttribute; import dev.cel.runtime.GlobalResolver; /** @@ -32,10 +33,27 @@ final class RelativeAttribute implements Attribute { private final ImmutableList qualifiers; @Override - public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { - Object obj = EvalHelpers.evalStrictly(operand, ctx, frame); - if (obj instanceof AccumulatedUnknowns) { - return obj; + public AttributeResolution resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { + Object obj; + CelAttribute attr = null; + + if (operand instanceof InterpretableAttribute) { + AttributeResolution res = ((InterpretableAttribute) operand).resolveWithAttribute(ctx, frame); + obj = res.value(); + attr = res.attribute(); + } else { + obj = EvalHelpers.evalStrictly(operand, ctx, frame); + } + + if (obj instanceof AccumulatedUnknowns || obj == null || obj instanceof MissingAttribute) { + CelAttribute qualifiedAttr = attr; + if (qualifiedAttr != null) { + for (int i = 0; i < qualifiers.size(); i++) { + qualifiedAttr = + qualifiedAttr.qualify(CelAttribute.Qualifier.fromGeneric(qualifiers.get(i).value())); + } + } + return AttributeResolution.of(obj, qualifiedAttr); } obj = celValueConverter.toRuntimeValue(obj); @@ -45,9 +63,12 @@ public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { Qualifier element = qualifiers.get(i); obj = element.qualify(obj); obj = celValueConverter.toRuntimeValue(obj); + if (attr != null) { + attr = attr.qualify(CelAttribute.Qualifier.fromGeneric(element.value())); + } } - return celValueConverter.maybeUnwrap(obj); + return AttributeResolution.of(celValueConverter.maybeUnwrap(obj), attr); } @Override diff --git a/runtime/src/test/java/dev/cel/runtime/CelAttributeTest.java b/runtime/src/test/java/dev/cel/runtime/CelAttributeTest.java index fc6cb3442..dce3e376c 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelAttributeTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelAttributeTest.java @@ -143,16 +143,33 @@ public void fromQualifiedIdentifier_parseIdents() { @Test public void fromGeneric_supportedTypes() { + assertThat(Qualifier.fromGeneric(1)).isEqualTo(Qualifier.ofInt(1)); assertThat(Qualifier.fromGeneric(Long.valueOf(1))).isEqualTo(Qualifier.ofInt(1)); assertThat(Qualifier.fromGeneric(UnsignedLong.valueOf(1))).isEqualTo(Qualifier.ofUint(1)); assertThat(Qualifier.fromGeneric("abcd")).isEqualTo(Qualifier.ofString("abcd")); assertThat(Qualifier.fromGeneric(Boolean.valueOf(false))).isEqualTo(Qualifier.ofBool(false)); } + @Test + public void fromGeneric_integerBoundaryValues() { + assertThat(Qualifier.fromGeneric(Integer.MAX_VALUE)) + .isEqualTo(Qualifier.ofInt(Integer.MAX_VALUE)); + assertThat(Qualifier.fromGeneric(Integer.MIN_VALUE)) + .isEqualTo(Qualifier.ofInt(Integer.MIN_VALUE)); + assertThat(Qualifier.fromGeneric(0)).isEqualTo(Qualifier.ofInt(0)); + } + + @Test + public void fromGeneric_nullThrows() { + assertThrows(IllegalArgumentException.class, () -> Qualifier.fromGeneric(null)); + } + @Test public void fromGeneric_unsupportedTypeThrows() { assertThrows( IllegalArgumentException.class, () -> Qualifier.fromGeneric(new ArrayList())); + assertThrows(IllegalArgumentException.class, () -> Qualifier.fromGeneric(1.0)); + assertThrows(IllegalArgumentException.class, () -> Qualifier.fromGeneric(new byte[] {1, 2})); } @Test diff --git a/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel index 29f08eb74..4ed7fc85f 100644 --- a/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel @@ -23,6 +23,7 @@ java_library( "//runtime:unknown_attributes", "//runtime:unknown_options", "//runtime/async", + "//testing:cel_runtime_flavor", "@maven//:junit_junit", "@maven//:com_google_testparameterinjector_test_parameter_injector", "//:java_truth", diff --git a/runtime/src/test/java/dev/cel/runtime/async/CelAsyncRuntimeImplTest.java b/runtime/src/test/java/dev/cel/runtime/async/CelAsyncRuntimeImplTest.java index d24e99860..27b720c28 100644 --- a/runtime/src/test/java/dev/cel/runtime/async/CelAsyncRuntimeImplTest.java +++ b/runtime/src/test/java/dev/cel/runtime/async/CelAsyncRuntimeImplTest.java @@ -29,7 +29,6 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector; // import com.google.testing.testsize.MediumTest; import dev.cel.bundle.Cel; -import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelOptions; @@ -40,6 +39,7 @@ import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.async.CelAsyncRuntime.AsyncProgram; +import dev.cel.testing.CelRuntimeFlavor; import java.time.Duration; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; @@ -52,6 +52,14 @@ // @MediumTest public final class CelAsyncRuntimeImplTest { + private static final CelOptions CEL_OPTIONS = + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build(); + + @TestParameter private CelRuntimeFlavor celRuntimeFlavor; + @Test public void asyncProgram_basicUnknownResolution() throws Exception { // Arrange @@ -62,8 +70,9 @@ public void asyncProgram_basicUnknownResolution() throws Exception { return attr.toString(); }); Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + celRuntimeFlavor + .builder() + .setOptions(CEL_OPTIONS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("com.google.var1", SimpleType.STRING) .addVar("com.google.var2", SimpleType.STRING) @@ -113,8 +122,9 @@ public void asyncProgram_sequentialUnknownResolution() throws Exception { return attr.toString(); }); Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + celRuntimeFlavor + .builder() + .setOptions(CEL_OPTIONS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("com.google.var1", SimpleType.BOOL) .addVar("com.google.var2", SimpleType.STRING) @@ -161,8 +171,9 @@ public void asyncProgram_basicAsyncResolver() throws Exception { SettableFuture var3 = SettableFuture.create(); Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + celRuntimeFlavor + .builder() + .setOptions(CEL_OPTIONS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("com.google.var1", SimpleType.STRING) .addVar("com.google.var2", SimpleType.STRING) @@ -213,8 +224,9 @@ public void asyncProgram_honorsCancellation() throws Exception { SettableFuture var3 = SettableFuture.create(); Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + celRuntimeFlavor + .builder() + .setOptions(CEL_OPTIONS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("com.google.var1", SimpleType.STRING) .addVar("com.google.var2", SimpleType.STRING) @@ -259,13 +271,14 @@ interface ResolverFactory { @Test public void asyncProgram_concurrency( - @TestParameter(valuesProvider = RepeatedTestProvider.class) int testRunIndex) + @TestParameter(valuesProvider = RepeatedTestProvider.class) int unusedTestRunIndex) throws Exception { Duration taskDelay = Duration.ofMillis(500); // Arrange Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + celRuntimeFlavor + .builder() + .setOptions(CEL_OPTIONS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("com.google.var1", SimpleType.STRING) .addVar("com.google.var2", SimpleType.STRING) @@ -317,8 +330,9 @@ public void asyncProgram_concurrency( public void asyncProgram_elementResolver() throws Exception { // Arrange Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + celRuntimeFlavor + .builder() + .setOptions(CEL_OPTIONS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar( "com.google.listVar", @@ -366,8 +380,9 @@ public void asyncProgram_elementResolver() throws Exception { public void asyncProgram_thrownExceptionPropagatesImmediately() throws Exception { // Arrange Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + celRuntimeFlavor + .builder() + .setOptions(CEL_OPTIONS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("com.google.var1", SimpleType.STRING) .addVar("com.google.var2", SimpleType.STRING) @@ -422,8 +437,9 @@ public void asyncProgram_thrownExceptionPropagatesImmediately() throws Exception public void asyncProgram_returnedExceptionPropagatesToEvaluator() throws Exception { // Arrange Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + celRuntimeFlavor + .builder() + .setOptions(CEL_OPTIONS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("com.google.var1", SimpleType.STRING) .addVar("com.google.var2", SimpleType.STRING) @@ -477,8 +493,9 @@ public void asyncProgram_returnedExceptionPropagatesToEvaluator() throws Excepti public void asyncProgram_returnedExceptionPropagatesToEvaluatorIsPruneable() throws Exception { // Arrange Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableUnknownTracking(true).build()) + celRuntimeFlavor + .builder() + .setOptions(CEL_OPTIONS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("com.google.var1", SimpleType.STRING) .addVar("com.google.var2", SimpleType.STRING) diff --git a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel index 9116818dc..898d4bcc0 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel @@ -46,6 +46,7 @@ java_library( "//runtime:partial_vars", "//runtime:program", "//runtime:runtime_equality", + "//runtime:runtime_factory", "//runtime:runtime_helpers", "//runtime:standard_functions", "//runtime:unknown_attributes", diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index 34e7831a6..32a20d473 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -21,6 +21,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertThrows; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -71,6 +72,8 @@ import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelLateFunctionBindings; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; import dev.cel.runtime.CelStandardFunctions; import dev.cel.runtime.CelStandardFunctions.StandardFunction; import dev.cel.runtime.CelUnknownSet; @@ -81,7 +84,9 @@ import dev.cel.runtime.Program; import dev.cel.runtime.RuntimeEquality; import dev.cel.runtime.RuntimeHelpers; +import dev.cel.runtime.UnknownContext; import dev.cel.runtime.standard.TypeFunction; +import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -92,7 +97,9 @@ public final class ProgramPlannerTest { private static final CelTypeProvider TYPE_PROVIDER = new CombinedCelTypeProvider( DefaultTypeProvider.getInstance(), - new ProtoMessageTypeProvider(ImmutableSet.of(TestAllTypes.getDescriptor()))); + ProtoMessageTypeProvider.newBuilder() + .addDescriptors(ImmutableSet.of(TestAllTypes.getDescriptor())) + .build()); private static final RuntimeEquality RUNTIME_EQUALITY = RuntimeEquality.create(RuntimeHelpers.create(), CEL_OPTIONS); private static final CelDescriptorPool DESCRIPTOR_POOL = @@ -255,9 +262,7 @@ private static DefaultDispatcher newDispatcher() { private static void addBindingsToDispatcher( DefaultDispatcher.Builder builder, ImmutableCollection overloadBindings) { - if (overloadBindings.isEmpty()) { - throw new IllegalArgumentException("Invalid bindings"); - } + Preconditions.checkArgument(!overloadBindings.isEmpty(), "Invalid bindings"); overloadBindings.forEach( overload -> @@ -519,7 +524,7 @@ public void plan_call_throws() throws Exception { .hasMessageThat() .contains("evaluation error at :5: Function 'error' failed with arg(s) ''"); assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); - assertThat(e.getCause()).hasMessageThat().contains("Intentional error"); + assertThat(e).hasCauseThat().hasMessageThat().contains("Intentional error"); } @Test @@ -573,6 +578,99 @@ public void plan_call_mapIndex() throws Exception { assertThat(result).isEqualTo(2L); } + @Test + public void plan_call_listIndex() throws Exception { + CelAbstractSyntaxTree ast = compile("[10, 20, 30][1]"); + Program program = PLANNER.plan(ast); + + Long result = (Long) program.eval(); + + assertThat(result).isEqualTo(20L); + } + + @Test + public void plan_call_listIndex_outOfBounds_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("[10, 20, 30][5]"); + Program program = PLANNER.plan(ast); + + assertThrows(CelEvaluationException.class, program::eval); + } + + @Test + public void plan_call_listIndex_negative_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("[10, 20, 30][-1]"); + Program program = PLANNER.plan(ast); + + assertThrows(CelEvaluationException.class, program::eval); + } + + @Test + public void plan_call_mapIndex_missingKey_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("map_var['missing']"); + Program program = PLANNER.plan(ast); + + assertThrows( + CelEvaluationException.class, + () -> program.eval(ImmutableMap.of("map_var", ImmutableMap.of("key", 1L)))); + } + + @Test + public void plan_call_index_withUnknownTarget() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("unk_list", ListType.create(SimpleType.INT)) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "unk_list[0]"); + Program program = PLANNER.plan(ast); + + CelUnknownSet result = + (CelUnknownSet) program.eval(PartialVars.of(CelAttributePattern.create("unk_list"))); + + assertThat(result) + .isEqualTo( + CelUnknownSet.create( + ImmutableSet.of(CelAttribute.create("unk_list")), ImmutableSet.of(1L))); + } + + @Test + public void plan_call_index_withUnknownIndex() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder().addVar("unk_index", SimpleType.INT).build(); + CelAbstractSyntaxTree ast = compile(compiler, "[10, 20, 30][unk_index]"); + Program program = PLANNER.plan(ast); + + CelUnknownSet result = + (CelUnknownSet) program.eval(PartialVars.of(CelAttributePattern.create("unk_index"))); + + assertThat(result) + .isEqualTo( + CelUnknownSet.create( + ImmutableSet.of(CelAttribute.create("unk_index")), ImmutableSet.of(6L))); + } + + @Test + public void plan_call_index_withMultipleUnknowns_mergesUnknowns() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("unk_map", MapType.create(SimpleType.STRING, SimpleType.INT)) + .addVar("unk_key", SimpleType.STRING) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "unk_map[unk_key]"); + Program program = PLANNER.plan(ast); + + CelUnknownSet result = + (CelUnknownSet) + program.eval( + PartialVars.of( + CelAttributePattern.create("unk_map"), CelAttributePattern.create("unk_key"))); + + assertThat(result) + .isEqualTo( + CelUnknownSet.create( + ImmutableSet.of(CelAttribute.create("unk_map"), CelAttribute.create("unk_key")), + ImmutableSet.of(1L, 3L))); + } + @Test public void plan_call_noMatchingOverload_throws() throws Exception { CelAbstractSyntaxTree ast = compile("concat(b'abc', dyn_var)"); @@ -986,10 +1084,10 @@ public void plan_partialEval_withWildcardQualification() throws Exception { .isEqualTo( CelUnknownSet.create( ImmutableSet.of( - CelAttribute.create("unk"), + CelAttribute.create("unk").qualify(CelAttribute.Qualifier.ofString("c")), CelAttribute.create("unk").qualify(CelAttribute.Qualifier.ofString("a")), CelAttribute.create("unk").qualify(CelAttribute.Qualifier.ofString("b"))), - ImmutableSet.of(2L, 5L, 7L))); + ImmutableSet.of(2L, 5L, 8L))); } @Test @@ -1484,4 +1582,440 @@ private enum PresenceTestCase { this.expected = expected; } } + + @Test + public void advanceEvaluation_unresolvedAttribute_returnsUnknownSet() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder().addVar("unk", SimpleType.INT).build(); + CelAbstractSyntaxTree ast = compile(compiler, "unk + 1"); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelRuntime.Program program = celRuntime.createProgram(ast); + UnknownContext context = + UnknownContext.create( + name -> Optional.empty(), ImmutableList.of(CelAttributePattern.create("unk"))); + + Object result = program.advanceEvaluation(context); + + assertThat(result).isEqualTo(CelUnknownSet.create(CelAttribute.create("unk"))); + } + + @Test + public void advanceEvaluation_withResolvedAttributes_returnsResult() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder().addVar("unk", SimpleType.INT).build(); + CelAbstractSyntaxTree ast = compile(compiler, "unk + 1"); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelRuntime.Program program = celRuntime.createProgram(ast); + UnknownContext context = + UnknownContext.create( + name -> Optional.empty(), ImmutableList.of(CelAttributePattern.create("unk"))); + UnknownContext resolvedContext = + context.withResolvedAttributes(ImmutableMap.of(CelAttribute.create("unk"), 41L)); + + Object result = program.advanceEvaluation(resolvedContext); + + assertThat(result).isEqualTo(42L); + } + + @Test + public void advanceEvaluation_multiVariable_bothUnknown_returnsBothUnknowns() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("a", SimpleType.INT) + .addVar("b", SimpleType.INT) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "a + b"); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelRuntime.Program program = celRuntime.createProgram(ast); + UnknownContext context = + UnknownContext.create( + name -> Optional.empty(), + ImmutableList.of(CelAttributePattern.create("a"), CelAttributePattern.create("b"))); + + Object result = program.advanceEvaluation(context); + + assertThat(result) + .isEqualTo( + CelUnknownSet.create( + ImmutableSet.of(CelAttribute.create("a"), CelAttribute.create("b")))); + } + + @Test + public void advanceEvaluation_multiVariable_oneResolved_returnsRemainingUnknown() + throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("a", SimpleType.INT) + .addVar("b", SimpleType.INT) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "a + b"); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelRuntime.Program program = celRuntime.createProgram(ast); + UnknownContext context = + UnknownContext.create( + name -> Optional.empty(), + ImmutableList.of(CelAttributePattern.create("a"), CelAttributePattern.create("b"))) + .withResolvedAttributes(ImmutableMap.of(CelAttribute.create("a"), 10L)); + + Object result = program.advanceEvaluation(context); + + assertThat(result).isEqualTo(CelUnknownSet.create(CelAttribute.create("b"))); + } + + @Test + public void advanceEvaluation_multiVariable_allResolved_returnsValue() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("a", SimpleType.INT) + .addVar("b", SimpleType.INT) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "a + b"); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelRuntime.Program program = celRuntime.createProgram(ast); + UnknownContext context = + UnknownContext.create( + name -> Optional.empty(), + ImmutableList.of(CelAttributePattern.create("a"), CelAttributePattern.create("b"))) + .withResolvedAttributes( + ImmutableMap.of(CelAttribute.create("a"), 10L, CelAttribute.create("b"), 20L)); + + Object result = program.advanceEvaluation(context); + + assertThat(result).isEqualTo(30L); + } + + @Test + public void advanceEvaluation_qualifiedAttribute_unresolved_returnsUnknownSet() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("msg", MapType.create(SimpleType.STRING, SimpleType.STRING)) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "msg.field"); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelRuntime.Program program = celRuntime.createProgram(ast); + UnknownContext context = + UnknownContext.create( + name -> Optional.empty(), + ImmutableList.of( + CelAttributePattern.create("msg").qualify(CelAttribute.Qualifier.ofWildCard()))); + + Object result = program.advanceEvaluation(context); + + assertThat(result) + .isEqualTo( + CelUnknownSet.create( + CelAttribute.create("msg").qualify(CelAttribute.Qualifier.ofString("field")))); + } + + @Test + public void advanceEvaluation_qualifiedAttribute_resolved_returnsValue() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("msg", MapType.create(SimpleType.STRING, SimpleType.STRING)) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "msg.field"); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelRuntime.Program program = celRuntime.createProgram(ast); + CelAttribute qualifiedAttr = + CelAttribute.create("msg").qualify(CelAttribute.Qualifier.ofString("field")); + UnknownContext context = + UnknownContext.create( + name -> Optional.empty(), + ImmutableList.of( + CelAttributePattern.create("msg").qualify(CelAttribute.Qualifier.ofWildCard()))) + .withResolvedAttributes(ImmutableMap.of(qualifiedAttr, "hello")); + + Object result = program.advanceEvaluation(context); + + assertThat(result).isEqualTo("hello"); + } + + @Test + public void advanceEvaluation_unresolvedMapIndex_returnsUnknownSet() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("msg", MapType.create(SimpleType.STRING, SimpleType.STRING)) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "msg['field']"); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelRuntime.Program program = celRuntime.createProgram(ast); + UnknownContext context = + UnknownContext.create( + name -> Optional.empty(), + ImmutableList.of( + CelAttributePattern.create("msg").qualify(CelAttribute.Qualifier.ofWildCard()))); + + Object result = program.advanceEvaluation(context); + + assertThat(result) + .isEqualTo( + CelUnknownSet.create( + CelAttribute.create("msg").qualify(CelAttribute.Qualifier.ofString("field")))); + } + + @Test + public void advanceEvaluation_resolvedMapIndex_returnsValue() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("msg", MapType.create(SimpleType.STRING, SimpleType.STRING)) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "msg['field']"); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelRuntime.Program program = celRuntime.createProgram(ast); + CelAttribute qualifiedAttr = + CelAttribute.create("msg").qualify(CelAttribute.Qualifier.ofString("field")); + UnknownContext context = + UnknownContext.create( + name -> Optional.empty(), + ImmutableList.of( + CelAttributePattern.create("msg").qualify(CelAttribute.Qualifier.ofWildCard()))) + .withResolvedAttributes(ImmutableMap.of(qualifiedAttr, "hello")); + + Object result = program.advanceEvaluation(context); + + assertThat(result).isEqualTo("hello"); + } + + @Test + public void advanceEvaluation_nestedIndex_unresolved_returnsUnknownSet() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("nested", MapType.create(SimpleType.STRING, SimpleType.DYN)) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "nested['a']['b']"); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelRuntime.Program program = celRuntime.createProgram(ast); + UnknownContext context = + UnknownContext.create( + name -> Optional.empty(), + ImmutableList.of( + CelAttributePattern.create("nested") + .qualify(CelAttribute.Qualifier.ofString("a")) + .qualify(CelAttribute.Qualifier.ofWildCard()))); + + Object result = program.advanceEvaluation(context); + + assertThat(result) + .isEqualTo( + CelUnknownSet.create( + CelAttribute.create("nested") + .qualify(CelAttribute.Qualifier.ofString("a")) + .qualify(CelAttribute.Qualifier.ofString("b")))); + } + + @Test + public void advanceEvaluation_nestedIndex_resolved_returnsValue() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("nested", MapType.create(SimpleType.STRING, SimpleType.DYN)) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "nested['a']['b']"); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelRuntime.Program program = celRuntime.createProgram(ast); + CelAttribute targetAttr = + CelAttribute.create("nested") + .qualify(CelAttribute.Qualifier.ofString("a")) + .qualify(CelAttribute.Qualifier.ofString("b")); + UnknownContext context = + UnknownContext.create( + name -> Optional.empty(), + ImmutableList.of( + CelAttributePattern.create("nested") + .qualify(CelAttribute.Qualifier.ofString("a")) + .qualify(CelAttribute.Qualifier.ofWildCard()))) + .withResolvedAttributes(ImmutableMap.of(targetAttr, 99L)); + + Object result = program.advanceEvaluation(context); + + assertThat(result).isEqualTo(99L); + } + + @Test + public void advanceEvaluation_mixedSelectAndIndex_unresolved_returnsUnknownSet() + throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("msg", MapType.create(SimpleType.STRING, SimpleType.DYN)) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "msg.a['b']"); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelRuntime.Program program = celRuntime.createProgram(ast); + UnknownContext context = + UnknownContext.create( + name -> Optional.empty(), + ImmutableList.of( + CelAttributePattern.create("msg") + .qualify(CelAttribute.Qualifier.ofString("a")) + .qualify(CelAttribute.Qualifier.ofWildCard()))); + + Object result = program.advanceEvaluation(context); + + assertThat(result) + .isEqualTo( + CelUnknownSet.create( + CelAttribute.create("msg") + .qualify(CelAttribute.Qualifier.ofString("a")) + .qualify(CelAttribute.Qualifier.ofString("b")))); + } + + @Test + public void advanceEvaluation_mixedSelectAndIndex_resolved_returnsValue() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("msg", MapType.create(SimpleType.STRING, SimpleType.DYN)) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "msg.a['b']"); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelRuntime.Program program = celRuntime.createProgram(ast); + CelAttribute targetAttr = + CelAttribute.create("msg") + .qualify(CelAttribute.Qualifier.ofString("a")) + .qualify(CelAttribute.Qualifier.ofString("b")); + UnknownContext context = + UnknownContext.create( + name -> Optional.empty(), + ImmutableList.of( + CelAttributePattern.create("msg") + .qualify(CelAttribute.Qualifier.ofString("a")) + .qualify(CelAttribute.Qualifier.ofWildCard()))) + .withResolvedAttributes(ImmutableMap.of(targetAttr, "deepValue")); + + Object result = program.advanceEvaluation(context); + + assertThat(result).isEqualTo("deepValue"); + } + + @Test + public void advanceEvaluation_exprIdOnlyUnknown_propagatesUnknownWithoutCrash() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("testList", ListType.create(SimpleType.BOOL)) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "testList[0]"); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions( + CelOptions.current() + .enableUnknownTracking(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelRuntime.Program program = celRuntime.createProgram(ast); + // target has only exprId unknown, no attributes + UnknownContext context = + UnknownContext.create( + name -> + name.equals("testList") + ? Optional.of(CelUnknownSet.create(100L)) + : Optional.empty(), + ImmutableList.of()); + + Object result = program.advanceEvaluation(context); + + assertThat(result).isInstanceOf(CelUnknownSet.class); + assertThat(((CelUnknownSet) result).unknownExprIds()).containsExactly(100L); + } + + @Test + public void advanceEvaluation_contextWithResolvedAttributesMultipleTimes_doesNotCrash() + throws Exception { + CelAttribute attr = CelAttribute.create("x"); + UnknownContext context = + UnknownContext.create(name -> Optional.empty(), ImmutableList.of()) + .withResolvedAttributes(ImmutableMap.of(attr, "val1")) + .withResolvedAttributes(ImmutableMap.of(attr, "val2")); + + assertThat(context.createAttributeResolver().resolve(attr)).hasValue("val2"); + } } diff --git a/runtime/src/test/resources/planner_unknownFieldSelection.baseline b/runtime/src/test/resources/planner_unknownFieldSelection.baseline index 0cbc75299..a7761bac8 100644 --- a/runtime/src/test/resources/planner_unknownFieldSelection.baseline +++ b/runtime/src/test/resources/planner_unknownFieldSelection.baseline @@ -12,7 +12,7 @@ declare x { } =====> bindings: {x=, unknown_attributes=[x.single_int32]} -result: CelUnknownSet{attributes=[x], unknownExprIds=[1]} +result: Source: x.single_int32 declare x {