diff --git a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel index 48915fd02..a9dbfaca2 100644 --- a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel @@ -38,6 +38,8 @@ java_library( "//runtime:interpreter_util", "//runtime:lite_runtime", "//runtime:lite_runtime_factory", + "//runtime:partial_vars", + "//runtime:unknown_attributes", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/test:simple_java_proto", diff --git a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java index 24e9d6d86..ab412fb39 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java @@ -49,10 +49,12 @@ import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import dev.cel.parser.CelMacro; import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelRuntime; import dev.cel.runtime.InterpreterUtil; +import dev.cel.runtime.PartialVars; import java.time.Duration; import java.time.Instant; import java.util.List; @@ -897,14 +899,12 @@ public void optionalIndex_onMap_returnsOptionalValue() throws Exception { @TestParameters("{source: '{?x: x}'}") public void optionalIndex_onMapWithUnknownInput_returnsUnknownResult(String source) throws Exception { - if (testMode.equals(TestMode.PLANNER_CHECKED) || testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { - // TODO: Uncomment once unknowns is implemented - return; - } Cel cel = newCelBuilder().addVar("x", OptionalType.create(SimpleType.INT)).build(); CelAbstractSyntaxTree ast = compile(cel, source); - Object result = cel.createProgram(ast).eval(); + Object result = + cel.createProgram(ast) + .eval(PartialVars.of(CelAttributePattern.fromQualifiedIdentifier("x"))); assertThat(InterpreterUtil.isUnknown(result)).isTrue(); } @@ -987,10 +987,6 @@ public void optionalIndex_onOptionalList_returnsOptionalValue() throws Exception @Test public void optionalIndex_onListWithUnknownInput_returnsUnknownResult() throws Exception { - if (testMode.equals(TestMode.PLANNER_CHECKED) || testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { - // TODO: Uncomment once unknowns is implemented - return; - } Cel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) @@ -998,7 +994,9 @@ public void optionalIndex_onListWithUnknownInput_returnsUnknownResult() throws E .build(); CelAbstractSyntaxTree ast = compile(cel, "[?x]"); - Object result = cel.createProgram(ast).eval(); + Object result = + cel.createProgram(ast) + .eval(PartialVars.of(CelAttributePattern.fromQualifiedIdentifier("x"))); assertThat(InterpreterUtil.isUnknown(result)).isTrue(); } @@ -1017,6 +1015,29 @@ public void traditionalIndex_onOptionalList_returnsOptionalEmpty() throws Except assertThat(result).isEqualTo(Optional.empty()); } + @Test + public void optionalFieldSelect_fieldMarkedUnknown_returnsUnknownSet() throws Exception { + if (testMode.equals(TestMode.LEGACY_CHECKED)) { + // This case is not possible to setup for legacy runtime + return; + } + + Cel cel = + newCelBuilder() + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "msg.?single_int32"); + + Object result = + cel.createProgram(ast) + .eval( + PartialVars.of( + ImmutableMap.of("msg", TestAllTypes.newBuilder().setSingleInt32(42).build()), + CelAttributePattern.fromQualifiedIdentifier("msg.single_int32"))); + + assertThat(InterpreterUtil.isUnknown(result)).isTrue(); + } + @Test // LHS @TestParameters("{expression: 'optx.or(optional.of(1))'}") @@ -1026,10 +1047,6 @@ public void traditionalIndex_onOptionalList_returnsOptionalEmpty() throws Except @TestParameters("{expression: 'optional.none().orValue(optx)'}") public void optionalChainedFunctions_lhsIsUnknown_returnsUnknown(String expression) throws Exception { - if (testMode.equals(TestMode.PLANNER_CHECKED) || testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { - // TODO: Uncomment once unknowns is implemented - return; - } Cel cel = newCelBuilder() .addVar("optx", OptionalType.create(SimpleType.INT)) @@ -1037,7 +1054,9 @@ public void optionalChainedFunctions_lhsIsUnknown_returnsUnknown(String expressi .build(); CelAbstractSyntaxTree ast = compile(cel, expression); - Object result = cel.createProgram(ast).eval(); + Object result = + cel.createProgram(ast) + .eval(PartialVars.of(CelAttributePattern.fromQualifiedIdentifier("optx"))); assertThat(InterpreterUtil.isUnknown(result)).isTrue(); } diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index 55ee241a0..3e183d236 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -351,3 +351,17 @@ java_library( "//runtime/src/main/java/dev/cel/runtime:runtime_planner_impl", ], ) + +java_library( + name = "accumulated_unknowns", + visibility = ["//:internal"], + exports = [ + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns", + ], +) + +java_library( + name = "partial_vars", + visibility = ["//:internal"], + exports = ["//runtime/src/main/java/dev/cel/runtime:partial_vars"], +) diff --git a/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java b/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java index d27de2da2..d4d54c71f 100644 --- a/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java +++ b/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java @@ -15,18 +15,23 @@ package dev.cel.runtime; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import dev.cel.common.annotations.Internal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; /** * An internal representation used for fast accumulation of unknown expr IDs and attributes. For * safety, this object should never be returned as an evaluated result and instead be adapted into * an immutable CelUnknownSet. + * + *

CEL Library Internals. Do Not Use. */ -final class AccumulatedUnknowns { +@Internal +public final class AccumulatedUnknowns { private static final int MAX_UNKNOWN_ATTRIBUTE_SIZE = 500_000; private final Set exprIds; private final Set attributes; @@ -39,8 +44,21 @@ Set attributes() { return attributes; } + /** + * Evaluates if the right hand side is an accumulated unknown, and if so, merges it into the + * accumulator. + */ + public static @Nullable AccumulatedUnknowns maybeMerge( + @Nullable AccumulatedUnknowns accumulator, Object newValue) { + if (newValue instanceof AccumulatedUnknowns) { + AccumulatedUnknowns newUnknowns = (AccumulatedUnknowns) newValue; + return accumulator == null ? newUnknowns : accumulator.merge(newUnknowns); + } + return accumulator; + } + @CanIgnoreReturnValue - AccumulatedUnknowns merge(AccumulatedUnknowns arg) { + public AccumulatedUnknowns merge(AccumulatedUnknowns arg) { enforceMaxAttributeSize(this.attributes, arg.attributes); this.exprIds.addAll(arg.exprIds); this.attributes.addAll(arg.attributes); @@ -55,7 +73,8 @@ static AccumulatedUnknowns create(Collection ids) { return create(ids, new ArrayList<>()); } - static AccumulatedUnknowns create(Collection exprIds, Collection attributes) { + public static AccumulatedUnknowns create( + Collection exprIds, Collection attributes) { return new AccumulatedUnknowns(new HashSet<>(exprIds), new HashSet<>(attributes)); } diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 10dca9ece..2681c17de 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -826,6 +826,7 @@ java_library( ":evaluation_listener", ":function_binding", ":function_resolver", + ":partial_vars", ":program", ":proto_message_runtime_equality", ":runtime", @@ -938,6 +939,7 @@ java_library( ":function_resolver", ":interpretable", ":interpreter", + ":partial_vars", ":program", ":proto_message_activation_factory", ":runtime_equality", @@ -955,7 +957,6 @@ java_library( "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", - "@maven//:org_jspecify_jspecify", ], ) @@ -1014,6 +1015,7 @@ java_library( ":evaluation_exception", ":function_resolver", ":interpretable", + ":partial_vars", ":program", ":variable_resolver", "//:auto_value", @@ -1029,6 +1031,7 @@ cel_android_library( ":evaluation_exception", ":function_resolver_android", ":interpretable_android", + ":partial_vars_android", ":program_android", ":variable_resolver", "//:auto_value", @@ -1199,6 +1202,7 @@ java_library( ":unknown_attributes", "//common/annotations", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", "@maven//:org_jspecify_jspecify", ], ) @@ -1214,6 +1218,7 @@ cel_android_library( "//common/annotations", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", ], ) @@ -1273,10 +1278,13 @@ java_library( java_library( name = "accumulated_unknowns", srcs = ["AccumulatedUnknowns.java"], - visibility = ["//visibility:private"], + tags = [ + ], deps = [ ":unknown_attributes", + "//common/annotations", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", ], ) @@ -1286,7 +1294,9 @@ cel_android_library( visibility = ["//visibility:private"], deps = [ ":unknown_attributes_android", + "//common/annotations", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", ], ) @@ -1318,6 +1328,34 @@ cel_android_library( ], ) +java_library( + name = "partial_vars", + srcs = ["PartialVars.java"], + tags = [ + ], + deps = [ + ":variable_resolver", + "//:auto_value", + "//runtime:unknown_attributes", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "partial_vars_android", + srcs = ["PartialVars.java"], + tags = [ + ], + deps = [ + ":variable_resolver", + "//:auto_value", + "//runtime:unknown_attributes_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "program", srcs = ["Program.java"], @@ -1326,6 +1364,7 @@ java_library( deps = [ ":evaluation_exception", ":function_resolver", + ":partial_vars", ":variable_resolver", "@maven//:com_google_errorprone_error_prone_annotations", ], @@ -1339,8 +1378,8 @@ cel_android_library( deps = [ ":evaluation_exception", ":function_resolver_android", + ":partial_vars_android", ":variable_resolver", - "//:auto_value", "@maven//:com_google_errorprone_error_prone_annotations", ], ) diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index 346b25ae9..cab2c666e 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -134,6 +134,11 @@ public Object eval( return program.eval(resolver, lateBoundFunctionResolver); } + @Override + public Object eval(PartialVars partialVars) throws CelEvaluationException { + return program.eval(partialVars); + } + @Override public Object trace(CelEvaluationListener listener) throws CelEvaluationException { throw new UnsupportedOperationException("Trace is not yet supported."); diff --git a/runtime/src/main/java/dev/cel/runtime/CelUnknownSet.java b/runtime/src/main/java/dev/cel/runtime/CelUnknownSet.java index c7f1d0c91..62d975f93 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelUnknownSet.java +++ b/runtime/src/main/java/dev/cel/runtime/CelUnknownSet.java @@ -59,7 +59,7 @@ static CelUnknownSet create(Iterable unknownExprIds) { return create(ImmutableSet.of(), ImmutableSet.copyOf(unknownExprIds)); } - static CelUnknownSet create( + public static CelUnknownSet create( ImmutableSet attributes, ImmutableSet unknownExprIds) { return new AutoValue_CelUnknownSet(attributes, unknownExprIds); } diff --git a/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java b/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java index f84897ac2..73607cefd 100644 --- a/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java +++ b/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java @@ -14,6 +14,7 @@ package dev.cel.runtime; +import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CheckReturnValue; import dev.cel.common.annotations.Internal; import org.jspecify.annotations.Nullable; @@ -55,12 +56,12 @@ public static boolean isUnknown(Object obj) { return obj instanceof CelUnknownSet; } - static boolean isAccumulatedUnknowns(Object obj) { + public static boolean isAccumulatedUnknowns(Object obj) { return obj instanceof AccumulatedUnknowns; } /** If the argument is {@link CelUnknownSet}, adapts it into {@link AccumulatedUnknowns} */ - static Object maybeAdaptToAccumulatedUnknowns(Object val) { + public static Object maybeAdaptToAccumulatedUnknowns(Object val) { if (!(val instanceof CelUnknownSet)) { return val; } @@ -68,10 +69,20 @@ static Object maybeAdaptToAccumulatedUnknowns(Object val) { return adaptToAccumulatedUnknowns((CelUnknownSet) val); } - static AccumulatedUnknowns adaptToAccumulatedUnknowns(CelUnknownSet unknowns) { + public static AccumulatedUnknowns adaptToAccumulatedUnknowns(CelUnknownSet unknowns) { return AccumulatedUnknowns.create(unknowns.unknownExprIds(), unknowns.attributes()); } + public static Object maybeAdaptToCelUnknownSet(Object val) { + if (!(val instanceof AccumulatedUnknowns)) { + return val; + } + + AccumulatedUnknowns unknowns = (AccumulatedUnknowns) val; + return CelUnknownSet.create( + ImmutableSet.copyOf(unknowns.attributes()), ImmutableSet.copyOf(unknowns.exprIds())); + } + /** * Enforces strictness on both lhs/rhs arguments from logical operators (i.e: intentionally throws * an appropriate exception when {@link Throwable} is encountered as part of evaluated result. diff --git a/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java b/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java index 5e57f497b..af8c1a6d0 100644 --- a/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java @@ -52,6 +52,12 @@ public Object eval(CelVariableResolver resolver) throws CelEvaluationException { throw new UnsupportedOperationException("To be implemented"); } + @Override + public Object eval(PartialVars partialVars) throws CelEvaluationException { + // TODO: Wire in program planner + throw new UnsupportedOperationException("To be implemented"); + } + static Program plan(Interpretable interpretable) { return new AutoValue_LiteProgramImpl(interpretable); } diff --git a/runtime/src/main/java/dev/cel/runtime/PartialVars.java b/runtime/src/main/java/dev/cel/runtime/PartialVars.java new file mode 100644 index 000000000..1cd081040 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/PartialVars.java @@ -0,0 +1,70 @@ +// 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; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import java.util.Map; +import java.util.Optional; + +/** + * A holder for a {@link CelVariableResolver} and a set of {@link CelAttributePattern}s that + * indicate variables or parts of variables whose value are not yet known. + */ +@AutoValue +public abstract class PartialVars { + + /** The resolver to use for resolving evaluation variables. */ + public abstract CelVariableResolver resolver(); + + /** + * A list of attribute patterns specifying which missing attribute paths should be tracked as + * unknown values. + */ + public abstract ImmutableList unknowns(); + + /** Constructs a new {@code PartialVars} from one or more {@link CelAttributePattern}s. */ + public static PartialVars of(CelAttributePattern... unknownAttributes) { + return of((unused) -> Optional.empty(), ImmutableList.copyOf(unknownAttributes)); + } + + /** + * Constructs a new {@code PartialVars} from a {@link CelVariableResolver} and a list of {@link + * CelAttributePattern}s. + */ + public static PartialVars of( + CelVariableResolver resolver, Iterable unknownAttributes) { + return new AutoValue_PartialVars(resolver, ImmutableList.copyOf(unknownAttributes)); + } + + /** + * Constructs a new {@code PartialVars} from a map of variables and an array of {@link + * CelAttributePattern}s. + */ + public static PartialVars of(Map variables, CelAttributePattern... unknownAttributes) { + return of( + (name) -> variables.containsKey(name) ? Optional.of(variables.get(name)) : Optional.empty(), + unknownAttributes); + } + + /** + * Constructs a new {@code PartialVars} from a {@link CelVariableResolver} and an array of {@link + * CelAttributePattern}s. + */ + public static PartialVars of( + CelVariableResolver resolver, CelAttributePattern... unknownAttributes) { + return of(resolver, ImmutableList.copyOf(unknownAttributes)); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/Program.java b/runtime/src/main/java/dev/cel/runtime/Program.java index c0982f1f8..e808a373c 100644 --- a/runtime/src/main/java/dev/cel/runtime/Program.java +++ b/runtime/src/main/java/dev/cel/runtime/Program.java @@ -43,4 +43,7 @@ Object eval(Map mapValue, CelFunctionResolver lateBoundFunctionResolv */ Object eval(CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver) throws CelEvaluationException; + + /** Evaluate a compiled program with unknown attribute patterns {@code partialVars}. */ + Object eval(PartialVars partialVars) throws CelEvaluationException; } diff --git a/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java b/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java index d0e64429b..c9f4d083b 100644 --- a/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java @@ -60,6 +60,14 @@ public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctio return evalInternal(Activation.copyOf(mapValue), lateBoundFunctionResolver); } + @Override + public Object eval(PartialVars partialVars) throws CelEvaluationException { + return evalInternal( + UnknownContext.create(partialVars.resolver(), partialVars.unknowns()), + /* lateBoundFunctionResolver= */ Optional.empty(), + /* listener= */ Optional.empty()); + } + @Override public Object trace(CelEvaluationListener listener) throws CelEvaluationException { return evalInternal(Activation.EMPTY, listener); 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 cc011ed34..90165c1ac 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(GlobalResolver ctx, ExecutionFrame frame); + Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame); Attribute addQualifier(Qualifier qualifier); } 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 6561e4e5c..3c18b192f 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -78,6 +78,8 @@ java_library( "//runtime:evaluation_exception_builder", "//runtime:function_resolver", "//runtime:interpretable", + "//runtime:interpreter_util", + "//runtime:partial_vars", "//runtime:program", "//runtime:resolved_overload", "//runtime:variable_resolver", @@ -128,7 +130,11 @@ java_library( "//common/types", "//common/types:type_providers", "//common/values", + "//runtime:accumulated_unknowns", "//runtime:interpretable", + "//runtime:interpreter_util", + "//runtime:partial_vars", + "//runtime:unknown_attributes", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:org_jspecify_jspecify", @@ -181,7 +187,6 @@ java_library( ":qualifier", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", ], ) @@ -235,6 +240,7 @@ java_library( ":execution_frame", ":planned_interpretable", "//common/values", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", "//runtime:resolved_overload", @@ -250,6 +256,7 @@ java_library( ":planned_interpretable", "//common/exceptions:overload_not_found", "//common/values", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", "//runtime:resolved_overload", @@ -265,6 +272,7 @@ java_library( ":execution_frame", ":planned_interpretable", "//common/values", + "//runtime:accumulated_unknowns", "//runtime:interpretable", "@maven//:com_google_guava_guava", ], @@ -278,6 +286,7 @@ java_library( ":execution_frame", ":planned_interpretable", "//common/values", + "//runtime:accumulated_unknowns", "//runtime:interpretable", "@maven//:com_google_guava_guava", ], @@ -289,6 +298,7 @@ java_library( deps = [ ":execution_frame", ":planned_interpretable", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", "@maven//:com_google_guava_guava", @@ -299,11 +309,13 @@ java_library( name = "eval_create_struct", srcs = ["EvalCreateStruct.java"], deps = [ + ":eval_helpers", ":execution_frame", ":planned_interpretable", "//common/types:type_providers", "//common/values", "//common/values:cel_value_provider", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", @@ -318,6 +330,7 @@ java_library( ":eval_helpers", ":execution_frame", ":planned_interpretable", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", @@ -329,11 +342,13 @@ java_library( name = "eval_create_map", srcs = ["EvalCreateMap.java"], deps = [ + ":eval_helpers", ":execution_frame", ":localized_evaluation_exception", ":planned_interpretable", "//common/exceptions:duplicate_key", "//common/exceptions:invalid_argument", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", @@ -348,6 +363,7 @@ java_library( ":activation_wrapper", ":execution_frame", ":planned_interpretable", + "//runtime:accumulated_unknowns", "//runtime:concatenated_list_view", "//runtime:evaluation_exception", "//runtime:interpretable", @@ -365,6 +381,7 @@ java_library( "//common/exceptions:iteration_budget_exceeded", "//runtime:evaluation_exception", "//runtime:function_resolver", + "//runtime:partial_vars", "//runtime:resolved_overload", ], ) @@ -424,6 +441,7 @@ java_library( ":execution_frame", ":planned_interpretable", "//common/exceptions:overload_not_found", + "//runtime:accumulated_unknowns", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -438,6 +456,7 @@ java_library( ":execution_frame", ":planned_interpretable", "//common/exceptions:overload_not_found", + "//runtime:accumulated_unknowns", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -452,6 +471,7 @@ java_library( ":execution_frame", ":planned_interpretable", "//common/values", + "//runtime:accumulated_unknowns", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java index 763f8faba..eb7406071 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java @@ -1,66 +1,73 @@ -// Copyright 2025 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.evalNonstrictly; - -import com.google.common.base.Preconditions; -import dev.cel.common.values.ErrorValue; -import dev.cel.runtime.GlobalResolver; - -final class EvalAnd extends PlannedInterpretable { - - @SuppressWarnings("Immutable") - private final PlannedInterpretable[] args; - - @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) { - ErrorValue errorValue = null; - for (PlannedInterpretable arg : args) { - Object argVal = evalNonstrictly(arg, resolver, frame); - if (argVal instanceof Boolean) { - // Short-circuit on false - if (!((boolean) argVal)) { - return false; - } - } else if (argVal instanceof ErrorValue) { - errorValue = (ErrorValue) argVal; - } else { - // TODO: Handle unknowns - errorValue = - ErrorValue.create( - arg.exprId(), - new IllegalArgumentException( - String.format("Expected boolean value, found: %s", argVal))); - } - } - - if (errorValue != null) { - return errorValue; - } - - return true; - } - - static EvalAnd create(long exprId, PlannedInterpretable[] args) { - return new EvalAnd(exprId, args); - } - - private EvalAnd(long exprId, PlannedInterpretable[] args) { - super(exprId); - Preconditions.checkArgument(args.length == 2); - this.args = args; - } -} +// Copyright 2025 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.evalNonstrictly; + +import com.google.common.base.Preconditions; +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.GlobalResolver; + +final class EvalAnd extends PlannedInterpretable { + + @SuppressWarnings("Immutable") + private final PlannedInterpretable[] args; + + @Override + public Object eval(GlobalResolver resolver, ExecutionFrame frame) { + ErrorValue errorValue = null; + AccumulatedUnknowns unknowns = null; + for (PlannedInterpretable arg : args) { + Object argVal = evalNonstrictly(arg, resolver, frame); + if (argVal instanceof Boolean) { + // Short-circuit on false + if (!((boolean) argVal)) { + return false; + } + } else if (argVal instanceof ErrorValue) { + errorValue = (ErrorValue) argVal; + } else if (argVal instanceof AccumulatedUnknowns) { + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVal); + } else { + errorValue = + ErrorValue.create( + arg.exprId(), + new IllegalArgumentException( + String.format("Expected boolean value, found: %s", argVal))); + } + } + + if (unknowns != null) { + return unknowns; + } + + if (errorValue != null) { + return errorValue; + } + + return true; + } + + static EvalAnd create(long exprId, PlannedInterpretable[] args) { + return new EvalAnd(exprId, args); + } + + private EvalAnd(long exprId, PlannedInterpretable[] args) { + super(exprId); + Preconditions.checkArgument(args.length == 2); + this.args = args; + } +} 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 fdd7ad2a3..a0a95c47a 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java @@ -24,9 +24,9 @@ final class EvalAttribute extends InterpretableAttribute { @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) { - Object resolved = attr.resolve(resolver, frame); + Object resolved = attr.resolve(exprId(), resolver, frame); if (resolved instanceof MissingAttribute) { - ((MissingAttribute) resolved).resolve(resolver, frame); + ((MissingAttribute) resolved).resolve(exprId(), resolver, frame); } return resolved; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java index 74482d629..3be1f016a 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java @@ -15,6 +15,7 @@ package dev.cel.runtime.planner; import com.google.common.base.Preconditions; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.GlobalResolver; @@ -28,8 +29,10 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval PlannedInterpretable condition = args[0]; PlannedInterpretable truthy = args[1]; PlannedInterpretable falsy = args[2]; - // TODO: Handle unknowns Object condResult = condition.eval(resolver, frame); + if (condResult instanceof AccumulatedUnknowns) { + return condResult; + } if (!(condResult instanceof Boolean)) { throw new IllegalArgumentException( String.format("Expected boolean value, found :%s", condResult)); diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java index 773272ea3..bae1e9302 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java @@ -16,6 +16,7 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.GlobalResolver; import java.util.Optional; @@ -32,9 +33,15 @@ final class EvalCreateList extends PlannedInterpretable { @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(values.length); + AccumulatedUnknowns unknowns = null; for (int i = 0; i < values.length; i++) { Object element = EvalHelpers.evalStrictly(values[i], resolver, frame); + if (element instanceof AccumulatedUnknowns) { + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, element); + continue; + } + if (isOptional[i]) { if (!(element instanceof Optional)) { throw new IllegalArgumentException( @@ -51,6 +58,11 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval builder.add(element); } + + if (unknowns != null) { + return unknowns; + } + return builder.build(); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java index f6f73e842..1e1b831bb 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java @@ -21,6 +21,7 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.common.exceptions.CelDuplicateKeyException; import dev.cel.common.exceptions.CelInvalidArgumentException; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.GlobalResolver; import java.util.HashSet; @@ -46,38 +47,49 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(keys.length); HashSet keysSeen = Sets.newHashSetWithExpectedSize(keys.length); + AccumulatedUnknowns unknowns = null; for (int i = 0; i < keys.length; i++) { PlannedInterpretable keyInterpretable = keys[i]; Object key = keyInterpretable.eval(resolver, frame); - if (!(key instanceof String - || key instanceof Long - || key instanceof UnsignedLong - || key instanceof Boolean)) { - throw new LocalizedEvaluationException( - new CelInvalidArgumentException("Unsupported key type: " + key), - keyInterpretable.exprId()); - } - boolean isDuplicate = !keysSeen.add(key); - if (!isDuplicate) { - if (key instanceof Long) { - long longVal = (Long) key; - if (longVal >= 0) { - isDuplicate = keysSeen.contains(UnsignedLong.valueOf(longVal)); + if (key instanceof AccumulatedUnknowns) { + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, key); + } else { + if (!(key instanceof String + || key instanceof Long + || key instanceof UnsignedLong + || key instanceof Boolean)) { + throw new LocalizedEvaluationException( + new CelInvalidArgumentException("Unsupported key type: " + key), + keyInterpretable.exprId()); + } + + boolean isDuplicate = !keysSeen.add(key); + if (!isDuplicate) { + if (key instanceof Long) { + long longVal = (Long) key; + if (longVal >= 0) { + isDuplicate = keysSeen.contains(UnsignedLong.valueOf(longVal)); + } + } else if (key instanceof UnsignedLong) { + UnsignedLong ulongVal = (UnsignedLong) key; + isDuplicate = keysSeen.contains(ulongVal.longValue()); } - } else if (key instanceof UnsignedLong) { - UnsignedLong ulongVal = (UnsignedLong) key; - isDuplicate = keysSeen.contains(ulongVal.longValue()); } - } - if (isDuplicate) { - throw new LocalizedEvaluationException( - CelDuplicateKeyException.of(key), keyInterpretable.exprId()); + if (isDuplicate) { + throw new LocalizedEvaluationException( + CelDuplicateKeyException.of(key), keyInterpretable.exprId()); + } } - Object val = values[i].eval(resolver, frame); + Object val = EvalHelpers.evalStrictly(values[i], resolver, frame); + + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, val); + if (unknowns != null) { + continue; + } if (isOptional[i]) { if (!(val instanceof Optional)) { @@ -94,13 +106,15 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval continue; } val = opt.get(); - } else { - System.out.println(); } builder.put(key, val); } + if (unknowns != null) { + return unknowns; + } + return builder.buildOrThrow(); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java index 4edc87b79..cdeb0c574 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java @@ -14,14 +14,15 @@ package dev.cel.runtime.planner; +import com.google.common.collect.Maps; import com.google.errorprone.annotations.Immutable; import dev.cel.common.types.CelType; import dev.cel.common.values.CelValueProvider; import dev.cel.common.values.StructValue; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.GlobalResolver; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -45,17 +46,22 @@ final class EvalCreateStruct extends PlannedInterpretable { @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { - Map fieldValues = new HashMap<>(); + Map fieldValues = Maps.newHashMapWithExpectedSize(keys.length); + AccumulatedUnknowns unknowns = null; for (int i = 0; i < keys.length; i++) { - Object value = values[i].eval(resolver, frame); + Object value = EvalHelpers.evalStrictly(values[i], resolver, frame); + + if (value instanceof AccumulatedUnknowns) { + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, value); + continue; + } if (isOptional[i]) { if (!(value instanceof Optional)) { throw new IllegalArgumentException( String.format( - "Cannot initialize optional entry 'single_double_wrapper' from non-optional value" - + " %s", - value)); + "Cannot initialize optional entry '%s' from non-optional value" + " %s", + keys[i], value)); } Optional opt = (Optional) value; @@ -71,6 +77,10 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval fieldValues.put(keys[i], value); } + if (unknowns != null) { + return unknowns; + } + // Either a primitive (wrappers) or a struct is produced Object value = valueProvider diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java index 3545ee4f7..197db42ad 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java @@ -16,6 +16,7 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.ConcatenatedListView; import dev.cel.runtime.GlobalResolver; @@ -73,6 +74,9 @@ private EvalFold( @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { Object iterRangeRaw = iterRange.eval(resolver, frame); + if (iterRangeRaw instanceof AccumulatedUnknowns) { + return iterRangeRaw; + } Folder folder = new Folder(resolver, accuVar, iterVar, iterVar2); folder.accuVal = maybeWrapAccumulator(accuInit.eval(folder, frame)); 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 92d234acc..38b060b92 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java @@ -1,78 +1,78 @@ -// Copyright 2025 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.common.base.Joiner; -import dev.cel.common.CelErrorCode; -import dev.cel.common.exceptions.CelRuntimeException; -import dev.cel.common.values.CelValueConverter; -import dev.cel.common.values.ErrorValue; -import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelResolvedOverload; -import dev.cel.runtime.GlobalResolver; - -final class EvalHelpers { - - static Object evalNonstrictly( - PlannedInterpretable interpretable, GlobalResolver resolver, ExecutionFrame frame) { - try { - return interpretable.eval(resolver, frame); - } catch (LocalizedEvaluationException e) { - // Intercept the localized exception to get a more specific expr ID for error reporting - // Example: foo [1] && strict_err [2] -> ID 2 is propagated. - return ErrorValue.create(e.exprId(), e); - } catch (Exception e) { - return ErrorValue.create(interpretable.exprId(), e); - } - } - - static Object evalStrictly( - PlannedInterpretable interpretable, GlobalResolver resolver, ExecutionFrame frame) { - try { - return interpretable.eval(resolver, frame); - } catch (LocalizedEvaluationException e) { - // Already localized - propagate as-is to preserve inner expression ID - throw e; - } catch (CelRuntimeException e) { - // Wrap with current interpretable's location - throw new LocalizedEvaluationException(e, interpretable.exprId()); - } catch (Exception e) { - // Wrap generic exceptions with location - throw new LocalizedEvaluationException( - e, CelErrorCode.INTERNAL_ERROR, interpretable.exprId()); - } - } - - static Object dispatch( - CelResolvedOverload overload, CelValueConverter valueConverter, Object[] args) - throws CelEvaluationException { - try { - Object result = overload.getDefinition().apply(args); - return valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result)); - } catch (CelRuntimeException e) { - // Function dispatch failure that's already been handled -- just propagate. - throw e; - } catch (RuntimeException e) { - // Unexpected function dispatch failure. - throw new IllegalArgumentException( - String.format( - "Function '%s' failed with arg(s) '%s'", - overload.getOverloadId(), Joiner.on(", ").join(args)), - e); - } - } - - private EvalHelpers() {} -} +// Copyright 2025 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.common.base.Joiner; +import dev.cel.common.CelErrorCode; +import dev.cel.common.exceptions.CelRuntimeException; +import dev.cel.common.values.CelValueConverter; +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelResolvedOverload; +import dev.cel.runtime.GlobalResolver; + +final class EvalHelpers { + + static Object evalNonstrictly( + PlannedInterpretable interpretable, GlobalResolver resolver, ExecutionFrame frame) { + try { + return interpretable.eval(resolver, frame); + } catch (LocalizedEvaluationException e) { + // Intercept the localized exception to get a more specific expr ID for error reporting + // Example: foo [1] && strict_err [2] -> ID 2 is propagated. + return ErrorValue.create(e.exprId(), e); + } catch (Exception e) { + return ErrorValue.create(interpretable.exprId(), e); + } + } + + static Object evalStrictly( + PlannedInterpretable interpretable, GlobalResolver resolver, ExecutionFrame frame) { + try { + return interpretable.eval(resolver, frame); + } catch (LocalizedEvaluationException e) { + // Already localized - propagate as-is to preserve inner expression ID + throw e; + } catch (CelRuntimeException e) { + // Wrap with current interpretable's location + throw new LocalizedEvaluationException(e, interpretable.exprId()); + } catch (Exception e) { + // Wrap generic exceptions with location + throw new LocalizedEvaluationException( + e, CelErrorCode.INTERNAL_ERROR, interpretable.exprId()); + } + } + + static Object dispatch( + CelResolvedOverload overload, CelValueConverter valueConverter, Object[] args) + throws CelEvaluationException { + try { + Object result = overload.getDefinition().apply(args); + return valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result)); + } catch (CelRuntimeException e) { + // Function dispatch failure that's already been handled -- just propagate. + throw e; + } catch (RuntimeException e) { + // Unexpected function dispatch failure. + throw new IllegalArgumentException( + String.format( + "Function '%s' failed with arg(s) '%s'", + overload.getOverloadId(), Joiner.on(", ").join(args)), + e); + } + } + + private EvalHelpers() {} +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java index a22ba8e94..cdee878ee 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java @@ -19,6 +19,7 @@ import com.google.common.collect.ImmutableList; import dev.cel.common.exceptions.CelOverloadNotFoundException; import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.GlobalResolver; @@ -36,10 +37,17 @@ final class EvalLateBoundCall extends PlannedInterpretable { @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { Object[] argVals = new Object[args.length]; + AccumulatedUnknowns unknowns = null; for (int i = 0; i < args.length; i++) { PlannedInterpretable arg = args[i]; // Late bound functions are assumed to be strict. argVals[i] = evalStrictly(arg, resolver, frame); + + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVals[i]); + } + + if (unknowns != null) { + return unknowns; } CelResolvedOverload resolvedOverload = diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.java index 70009d567..5ad1933d7 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.java @@ -17,6 +17,7 @@ import com.google.common.base.Preconditions; import com.google.errorprone.annotations.Immutable; import dev.cel.common.exceptions.CelOverloadNotFoundException; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.GlobalResolver; import java.util.Optional; @@ -29,6 +30,10 @@ final class EvalOptionalOr extends PlannedInterpretable { public Object eval(GlobalResolver resolver, ExecutionFrame frame) { Object lhsValue = EvalHelpers.evalStrictly(lhs, resolver, frame); + if (lhsValue instanceof AccumulatedUnknowns) { + return lhsValue; + } + if (!(lhsValue instanceof Optional)) { throw new CelOverloadNotFoundException("or"); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.java index 7a4940c7c..6634d60f6 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.java @@ -17,6 +17,7 @@ import com.google.common.base.Preconditions; import com.google.errorprone.annotations.Immutable; import dev.cel.common.exceptions.CelOverloadNotFoundException; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.GlobalResolver; import java.util.Optional; @@ -28,6 +29,10 @@ final class EvalOptionalOrValue extends PlannedInterpretable { @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) { Object lhsValue = EvalHelpers.evalStrictly(lhs, resolver, frame); + if (lhsValue instanceof AccumulatedUnknowns) { + return lhsValue; + } + if (!(lhsValue instanceof Optional)) { throw new CelOverloadNotFoundException("orValue"); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java index bc14149f3..8887aa697 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java @@ -18,6 +18,7 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.common.values.CelValueConverter; import dev.cel.common.values.SelectableValue; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.GlobalResolver; import java.util.Map; import java.util.Optional; @@ -42,6 +43,10 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { } Object runtimeOperandValue = celValueConverter.toRuntimeValue(operandValue); + if (runtimeOperandValue instanceof AccumulatedUnknowns) { + return runtimeOperandValue; + } + boolean hasField = false; if (runtimeOperandValue instanceof SelectableValue) { @@ -62,6 +67,10 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { return resultValue; } + if (resultValue instanceof AccumulatedUnknowns) { + return resultValue; + } + return Optional.of(resultValue); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java index 22fc56a7f..bc19ed81a 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java @@ -1,66 +1,73 @@ -// Copyright 2025 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.evalNonstrictly; - -import com.google.common.base.Preconditions; -import dev.cel.common.values.ErrorValue; -import dev.cel.runtime.GlobalResolver; - -final class EvalOr extends PlannedInterpretable { - - @SuppressWarnings("Immutable") - private final PlannedInterpretable[] args; - - @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) { - ErrorValue errorValue = null; - for (PlannedInterpretable arg : args) { - Object argVal = evalNonstrictly(arg, resolver, frame); - if (argVal instanceof Boolean) { - // Short-circuit on true - if (((boolean) argVal)) { - return true; - } - } else if (argVal instanceof ErrorValue) { - errorValue = (ErrorValue) argVal; - } else { - // TODO: Handle unknowns - errorValue = - ErrorValue.create( - arg.exprId(), - new IllegalArgumentException( - String.format("Expected boolean value, found: %s", argVal))); - } - } - - if (errorValue != null) { - return errorValue; - } - - return false; - } - - static EvalOr create(long exprId, PlannedInterpretable[] args) { - return new EvalOr(exprId, args); - } - - private EvalOr(long exprId, PlannedInterpretable[] args) { - super(exprId); - Preconditions.checkArgument(args.length == 2); - this.args = args; - } -} +// Copyright 2025 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.evalNonstrictly; + +import com.google.common.base.Preconditions; +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.GlobalResolver; + +final class EvalOr extends PlannedInterpretable { + + @SuppressWarnings("Immutable") + private final PlannedInterpretable[] args; + + @Override + public Object eval(GlobalResolver resolver, ExecutionFrame frame) { + ErrorValue errorValue = null; + AccumulatedUnknowns unknowns = null; + for (PlannedInterpretable arg : args) { + Object argVal = evalNonstrictly(arg, resolver, frame); + if (argVal instanceof Boolean) { + // Short-circuit on true + if (((boolean) argVal)) { + return true; + } + } else if (argVal instanceof ErrorValue) { + errorValue = (ErrorValue) argVal; + } else if (argVal instanceof AccumulatedUnknowns) { + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVal); + } else { + errorValue = + ErrorValue.create( + arg.exprId(), + new IllegalArgumentException( + String.format("Expected boolean value, found: %s", argVal))); + } + } + + if (unknowns != null) { + return unknowns; + } + + if (errorValue != null) { + return errorValue; + } + + return false; + } + + static EvalOr create(long exprId, PlannedInterpretable[] args) { + return new EvalOr(exprId, args); + } + + private EvalOr(long exprId, PlannedInterpretable[] args) { + super(exprId); + Preconditions.checkArgument(args.length == 2); + this.args = args; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java index 9f14f8bf9..eb8745632 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java @@ -18,6 +18,7 @@ import static dev.cel.runtime.planner.EvalHelpers.evalStrictly; import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.GlobalResolver; @@ -34,12 +35,19 @@ final class EvalVarArgsCall extends PlannedInterpretable { @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { Object[] argVals = new Object[args.length]; + AccumulatedUnknowns unknowns = null; for (int i = 0; i < args.length; i++) { PlannedInterpretable arg = args[i]; argVals[i] = resolvedOverload.isStrict() ? evalStrictly(arg, resolver, frame) : evalNonstrictly(arg, resolver, frame); + + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVals[i]); + } + + if (unknowns != null) { + return unknowns; } return EvalHelpers.dispatch(resolvedOverload, celValueConverter, argVals); 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 80ee4b318..e29c68dd8 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java @@ -19,6 +19,7 @@ import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.CelResolvedOverload; +import dev.cel.runtime.PartialVars; import java.util.Collection; import java.util.Optional; @@ -27,6 +28,7 @@ final class ExecutionFrame { private final int comprehensionIterationLimit; private final CelFunctionResolver functionResolver; + private final PartialVars partialVars; private int iterationCount; Optional findOverload( @@ -47,12 +49,19 @@ void incrementIterations() { } } - static ExecutionFrame create(CelFunctionResolver functionResolver, CelOptions celOptions) { - return new ExecutionFrame(functionResolver, celOptions.comprehensionMaxIterations()); + static ExecutionFrame create( + CelFunctionResolver functionResolver, PartialVars partialVars, CelOptions celOptions) { + return new ExecutionFrame( + functionResolver, partialVars, celOptions.comprehensionMaxIterations()); } - private ExecutionFrame(CelFunctionResolver functionResolver, int limit) { + Optional partialVars() { + return Optional.ofNullable(partialVars); + } + + private ExecutionFrame(CelFunctionResolver functionResolver, PartialVars partialVars, int limit) { this.comprehensionIterationLimit = limit; this.functionResolver = functionResolver; + this.partialVars = partialVars; } } 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 40a9f6203..1506eb180 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java @@ -28,10 +28,10 @@ final class MaybeAttribute implements Attribute { private final ImmutableList attributes; @Override - public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { + public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { MissingAttribute maybeError = null; for (NamespacedAttribute attr : attributes) { - Object value = attr.resolve(ctx, frame); + Object value = attr.resolve(exprId, ctx, frame); if (value == null) { continue; } 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 02b04781c..b7fb8ad72 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java @@ -25,7 +25,7 @@ final class MissingAttribute implements Attribute { private final Kind kind; @Override - public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { + public Object 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 cc8ca1d97..ed37eada1 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java @@ -15,6 +15,7 @@ package dev.cel.runtime.planner; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.Immutable; import dev.cel.common.types.CelType; @@ -23,14 +24,21 @@ import dev.cel.common.types.SimpleType; import dev.cel.common.types.TypeType; 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.GlobalResolver; +import dev.cel.runtime.InterpreterUtil; +import dev.cel.runtime.PartialVars; +import java.util.Map; import java.util.NoSuchElementException; +import java.util.Optional; import org.jspecify.annotations.Nullable; @Immutable final class NamespacedAttribute implements Attribute { private final boolean disambiguateNames; - private final ImmutableSet namespacedNames; + private final ImmutableMap candidateAttributes; private final ImmutableList qualifiers; private final CelValueConverter celValueConverter; private final CelTypeProvider typeProvider; @@ -40,11 +48,11 @@ ImmutableList qualifiers() { } ImmutableSet candidateVariableNames() { - return namespacedNames; + return candidateAttributes.keySet(); } @Override - public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { + public Object 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 @@ -53,13 +61,33 @@ public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { inputVars = unwrapToNonLocal(ctx); } - for (String name : namespacedNames) { + for (Map.Entry entry : candidateAttributes.entrySet()) { + String name = entry.getKey(); + CelAttribute attr = entry.getValue(); + GlobalResolver resolver = ctx; if (disambiguateNames) { resolver = inputVars; } Object value = resolver.resolve(name); + value = InterpreterUtil.maybeAdaptToAccumulatedUnknowns(value); + + PartialVars partialVars = frame.partialVars().orElse(null); + + if (partialVars != null) { + ImmutableList patterns = partialVars.unknowns(); + for (Qualifier qualifier : qualifiers) { + attr = attr.qualify(CelAttribute.Qualifier.fromGeneric(qualifier.value())); + } + + CelAttributePattern partialMatch = findPartialMatchingPattern(attr, patterns).orElse(null); + if (partialMatch != null) { + return AccumulatedUnknowns.create( + ImmutableList.of(exprId), ImmutableList.of(partialMatch.simplify(attr))); + } + } + if (value != null) { return applyQualifiers(value, celValueConverter, qualifiers); } @@ -71,7 +99,7 @@ public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { } } - return MissingAttribute.newMissingAttribute(namespacedNames); + return MissingAttribute.newMissingAttribute(candidateAttributes.keySet()); } private @Nullable Object findIdent(String name) { @@ -131,10 +159,17 @@ private GlobalResolver unwrapToNonLocal(GlobalResolver resolver) { @Override public NamespacedAttribute addQualifier(Qualifier qualifier) { + ImmutableMap.Builder attributesBuilder = ImmutableMap.builder(); + CelAttribute.Qualifier celQualifier = CelAttribute.Qualifier.fromGeneric(qualifier.value()); + + for (Map.Entry entry : candidateAttributes.entrySet()) { + attributesBuilder.put(entry.getKey(), entry.getValue().qualify(celQualifier)); + } + return new NamespacedAttribute( typeProvider, celValueConverter, - namespacedNames, + attributesBuilder.buildOrThrow(), disambiguateNames, ImmutableList.builder().addAll(qualifiers).add(qualifier).build()); } @@ -150,37 +185,49 @@ private static Object applyQualifiers( return celValueConverter.maybeUnwrap(obj); } + private static Optional findPartialMatchingPattern( + CelAttribute attr, ImmutableList patterns) { + for (CelAttributePattern pattern : patterns) { + if (pattern.isPartialMatch(attr)) { + return Optional.of(pattern); + } + } + return Optional.empty(); + } + static NamespacedAttribute create( CelTypeProvider typeProvider, CelValueConverter celValueConverter, ImmutableSet namespacedNames) { - ImmutableSet.Builder namesBuilder = ImmutableSet.builder(); + ImmutableMap.Builder attributesBuilder = ImmutableMap.builder(); boolean disambiguateNames = false; + for (String name : namespacedNames) { + String baseName = name; if (name.startsWith(".")) { disambiguateNames = true; - namesBuilder.add(name.substring(1)); - } else { - namesBuilder.add(name); + baseName = name.substring(1); } + attributesBuilder.put(baseName, CelAttribute.fromQualifiedIdentifier(baseName)); } + return new NamespacedAttribute( typeProvider, celValueConverter, - namesBuilder.build(), + attributesBuilder.buildOrThrow(), disambiguateNames, ImmutableList.of()); } - NamespacedAttribute( + private NamespacedAttribute( CelTypeProvider typeProvider, CelValueConverter celValueConverter, - ImmutableSet namespacedNames, + ImmutableMap candidateAttributes, boolean disambiguateNames, ImmutableList qualifiers) { this.typeProvider = typeProvider; this.celValueConverter = celValueConverter; - this.namespacedNames = namespacedNames; + this.candidateAttributes = candidateAttributes; this.disambiguateNames = disambiguateNames; this.qualifiers = qualifiers; } 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 8b419cab2..34fc34b50 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java @@ -26,6 +26,8 @@ import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.CelVariableResolver; import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.InterpreterUtil; +import dev.cel.runtime.PartialVars; import dev.cel.runtime.Program; import java.util.Collection; import java.util.Map; @@ -58,47 +60,61 @@ public Optional findOverloadMatchingArgs( @Override public Object eval() throws CelEvaluationException { - return evalOrThrow(interpretable(), GlobalResolver.EMPTY, EMPTY_FUNCTION_RESOLVER); + return evalOrThrow(interpretable(), GlobalResolver.EMPTY, EMPTY_FUNCTION_RESOLVER, null); } @Override public Object eval(Map mapValue) throws CelEvaluationException { - return evalOrThrow(interpretable(), Activation.copyOf(mapValue), EMPTY_FUNCTION_RESOLVER); + return evalOrThrow(interpretable(), Activation.copyOf(mapValue), EMPTY_FUNCTION_RESOLVER, null); } @Override public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctionResolver) throws CelEvaluationException { - return evalOrThrow(interpretable(), Activation.copyOf(mapValue), lateBoundFunctionResolver); + return evalOrThrow( + interpretable(), Activation.copyOf(mapValue), lateBoundFunctionResolver, null); } @Override public Object eval(CelVariableResolver resolver) throws CelEvaluationException { return evalOrThrow( - interpretable(), (name) -> resolver.find(name).orElse(null), EMPTY_FUNCTION_RESOLVER); + interpretable(), (name) -> resolver.find(name).orElse(null), EMPTY_FUNCTION_RESOLVER, null); } @Override public Object eval(CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver) throws CelEvaluationException { return evalOrThrow( - interpretable(), (name) -> resolver.find(name).orElse(null), lateBoundFunctionResolver); + interpretable(), + (name) -> resolver.find(name).orElse(null), + lateBoundFunctionResolver, + null); + } + + @Override + public Object eval(PartialVars partialVars) throws CelEvaluationException { + return evalOrThrow( + interpretable(), + (name) -> partialVars.resolver().find(name).orElse(null), + EMPTY_FUNCTION_RESOLVER, + partialVars); } private Object evalOrThrow( PlannedInterpretable interpretable, GlobalResolver resolver, - CelFunctionResolver functionResolver) + CelFunctionResolver functionResolver, + PartialVars partialVars) throws CelEvaluationException { try { - ExecutionFrame frame = ExecutionFrame.create(functionResolver, options()); + ExecutionFrame frame = ExecutionFrame.create(functionResolver, partialVars, options()); Object evalResult = interpretable.eval(resolver, frame); if (evalResult instanceof ErrorValue) { ErrorValue errorValue = (ErrorValue) evalResult; throw newCelEvaluationException(errorValue.exprId(), errorValue.value()); } - return evalResult; + return InterpreterUtil.maybeAdaptToCelUnknownSet(evalResult); } catch (RuntimeException e) { throw newCelEvaluationException(interpretable.exprId(), e); } 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 b3d83c390..1ab2fa3e7 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java @@ -17,6 +17,7 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.GlobalResolver; /** @@ -31,15 +32,18 @@ final class RelativeAttribute implements Attribute { private final ImmutableList qualifiers; @Override - public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { + public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { Object obj = EvalHelpers.evalStrictly(operand, ctx, frame); + if (obj instanceof AccumulatedUnknowns) { + return obj; + } + obj = celValueConverter.toRuntimeValue(obj); for (Qualifier qualifier : qualifiers) { obj = qualifier.qualify(obj); } - // TODO: Handle unknowns return celValueConverter.maybeUnwrap(obj); } diff --git a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel index 8a0b1f9de..577010971 100644 --- a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel @@ -130,16 +130,25 @@ java_library( srcs = [ "PlannerInterpreterTest.java", ], + resources = [ + "//runtime/testdata", + ], deps = [ "//common:cel_ast", "//common:compiler_common", "//common:container", "//common:options", + "//common/types", "//common/types:type_providers", "//extensions", "//runtime", + "//runtime:function_binding", "//runtime:runtime_experimental_factory", + "//runtime:unknown_attributes", "//testing:base_interpreter_test", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", ], diff --git a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java index 3254855c7..2c0bec739 100644 --- a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java +++ b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java @@ -14,6 +14,8 @@ package dev.cel.runtime; +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Timestamp; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.common.CelAbstractSyntaxTree; @@ -21,8 +23,15 @@ import dev.cel.common.CelOptions; import dev.cel.common.CelValidationException; import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.ListType; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.extensions.CelExtensions; import dev.cel.testing.BaseInterpreterTest; +import java.util.Arrays; +import java.util.Objects; +import org.junit.Test; import org.junit.runner.RunWith; /** Interpreter tests using ProgramPlanner */ @@ -37,7 +46,8 @@ protected CelRuntimeBuilder newBaseRuntimeBuilder(CelOptions celOptions) { .addLateBoundFunctions("record") .setOptions(celOptions) .addLibraries(CelExtensions.optional()) - .addFileTypes(TEST_FILE_DESCRIPTORS); + .addFileTypes(TEST_FILE_DESCRIPTORS) + .addMessageTypes(TestAllTypes.getDescriptor()); } @Override @@ -70,26 +80,247 @@ protected CelAbstractSyntaxTree prepareTest(CelTypeProvider typeProvider) { } } + @Override + public void optional_errors() { + if (isParseOnly) { + // Parsed-only evaluation contains function name in the + // error message instead of the function overload. + skipBaselineVerification(); + } else { + super.optional_errors(); + } + } + @Override public void unknownField() { - // TODO: Unknown support not implemented yet + // Exercised in planner_unknownFieldAccess instead skipBaselineVerification(); } @Override public void unknownResultSet() { - // TODO: Unknown support not implemented yet + // Exercised in planner_unknownResultSet_success instead skipBaselineVerification(); } - @Override - public void optional_errors() { - if (isParseOnly) { - // Parsed-only evaluation contains function name in the - // error message instead of the function overload. - skipBaselineVerification(); - } else { - super.optional_errors(); - } + @Test + public void planner_unknownFieldSelection() { + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + + CelAttributePattern patternX = CelAttributePattern.fromQualifiedIdentifier("x"); + + source = "x"; + // We have the full message, but we're claiming that the attribute is unknown. + runTest(ImmutableMap.of("x", TestAllTypes.getDefaultInstance()), patternX); + // A "partially known message". The result is still an unknown. + runTest( + ImmutableMap.of("x", TestAllTypes.getDefaultInstance()), + CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); + + source = "x.single_int32"; + runTest(ImmutableMap.of(), patternX); + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); + + source = "x.map_int32_int64[22]"; + runTest(ImmutableMap.of(), patternX); + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.map_int32_int64")); + + source = "x.repeated_nested_message[1]"; + runTest(ImmutableMap.of(), patternX); + runTest( + ImmutableMap.of(), + CelAttributePattern.fromQualifiedIdentifier("x.repeated_nested_message")); + + source = "x.single_nested_message.bb"; + runTest(ImmutableMap.of(), patternX); + runTest( + ImmutableMap.of(), + CelAttributePattern.fromQualifiedIdentifier("x.single_nested_message.bb")); + + source = "{1: x.single_int32}"; + runTest(ImmutableMap.of(), patternX); + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); + + source = "[1, x.single_int32]"; + runTest(ImmutableMap.of(), patternX); + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); + } + + @Test + public void planner_unknownResultSet_success() { + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + TestAllTypes message = + TestAllTypes.newBuilder() + .setSingleString("test") + .setSingleTimestamp(Timestamp.newBuilder().setSeconds(15)) + .build(); + ImmutableMap variables = ImmutableMap.of("x", message); + CelAttributePattern unknownInt32 = + CelAttributePattern.fromQualifiedIdentifier("x.single_int32"); + CelAttributePattern unknownInt64 = + CelAttributePattern.fromQualifiedIdentifier("x.single_int64"); + + source = "x.single_int32 == 1 && true"; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 && false"; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 && x.single_int64 == 1"; + runTest(variables, unknownInt32, unknownInt64); + + source = "true && x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = "false && x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_string == \"test\""; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_string != \"test\""; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_int64 == 1"; + runTest(variables, unknownInt32, unknownInt64); + + source = "true || x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = "false || x.single_int32 == 1"; + runTest(variables, unknownInt32); + + // dispatch test + declareFunction( + "f", memberOverload("f", Arrays.asList(SimpleType.INT, SimpleType.INT), SimpleType.BOOL)); + celRuntime = + newBaseRuntimeBuilder( + CelOptions.current() + .enableTimestampEpoch(true) + .enableHeterogeneousNumericComparisons(true) + .enableOptionalSyntax(true) + .comprehensionMaxIterations(1_000) + .build()) + .addFunctionBindings( + CelFunctionBinding.from("f", Integer.class, Integer.class, Objects::equals)) + .setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())) + .build(); + + source = "x.single_int32.f(1)"; + runTest(variables, unknownInt32); + + source = "1.f(x.single_int32)"; + runTest(variables, unknownInt32); + + source = "x.single_int64.f(x.single_int32)"; + runTest(variables, unknownInt32, unknownInt64); + + source = "[0, 2, 4].exists(z, z == 2 || z == x.single_int32)"; + runTest(variables, unknownInt32); + + source = "[0, 2, 4].exists(z, z == x.single_int32)"; + runTest(variables, unknownInt32); + + source = + "[0, 2, 4].exists_one(z, z == 0 || (z == 2 && z == x.single_int32) " + + "|| (z == 4 && z == x.single_int64))"; + runTest(variables, unknownInt32, unknownInt64); + + source = "[0, 2].all(z, z == 2 || z == x.single_int32)"; + runTest(variables, unknownInt32); + + source = + "[0, 2, 4].filter(z, z == 0 || (z == 2 && z == x.single_int32) " + + "|| (z == 4 && z == x.single_int64))"; + runTest(variables, unknownInt32, unknownInt64); + + source = + "[0, 2, 4].map(z, z == 0 || (z == 2 && z == x.single_int32) " + + "|| (z == 4 && z == x.single_int64))"; + runTest(variables, unknownInt32, unknownInt64); + + source = "x.single_int32 == 1 ? 1 : 2"; + runTest(variables, unknownInt32); + + source = "true ? x.single_int32 : 2"; + runTest(variables, unknownInt32); + + source = "true ? 1 : x.single_int32"; + runTest(variables, unknownInt32); + + source = "false ? x.single_int32 : 2"; + runTest(variables, unknownInt32); + + source = "false ? 1 : x.single_int32"; + runTest(variables, unknownInt32); + + source = "x.single_int64 == 1 ? x.single_int32 : x.single_int32"; + runTest(variables, unknownInt32, unknownInt64); + + source = "{x.single_int32: 2, 3: 4}"; + runTest(variables, unknownInt32); + + source = "{1: x.single_int32, 3: 4}"; + runTest(variables, unknownInt32); + + source = "{1: x.single_int32, x.single_int64: 4}"; + runTest(variables, unknownInt32, unknownInt64); + + source = "[1, x.single_int32, 3, 4]"; + runTest(variables, unknownInt32); + + source = "[1, x.single_int32, x.single_int64, 4]"; + runTest(variables, unknownInt32, unknownInt64); + + source = "TestAllTypes{single_int32: x.single_int32}.single_int32 == 2"; + runTest(variables, unknownInt32); + + source = "TestAllTypes{single_int32: x.single_int32, single_int64: x.single_int64}"; + runTest(variables, unknownInt32, unknownInt64); + + clearAllDeclarations(); + declareVariable("unknown_list", ListType.create(SimpleType.INT)); + source = "unknown_list.map(x, x)"; + runTest(variables, CelAttributePattern.fromQualifiedIdentifier("unknown_list")); + } + + @Test + public void planner_unknownResultSet_errors() { + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + TestAllTypes message = + TestAllTypes.newBuilder() + .setSingleString("test") + .setSingleTimestamp(Timestamp.newBuilder().setSeconds(15)) + .build(); + ImmutableMap variables = ImmutableMap.of("x", message); + CelAttributePattern unknownInt32 = + CelAttributePattern.fromQualifiedIdentifier("x.single_int32"); + + source = "x.single_int32 == 1 && x.single_timestamp <= timestamp(\"bad timestamp string\")"; + runTest(variables, unknownInt32); + + source = "x.single_timestamp <= timestamp(\"bad timestamp string\") && x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = + "x.single_timestamp <= timestamp(\"bad timestamp string\") " + + "&& x.single_timestamp > timestamp(\"another bad timestamp string\")"; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_timestamp <= timestamp(\"bad timestamp string\")"; + runTest(variables, unknownInt32); + + source = "x.single_timestamp <= timestamp(\"bad timestamp string\") || x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = + "x.single_timestamp <= timestamp(\"bad timestamp string\") " + + "|| x.single_timestamp > timestamp(\"another bad timestamp string\")"; + runTest(variables, unknownInt32); + + source = "x"; + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x")); } } 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 fb05b0b31..9116818dc 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel @@ -43,10 +43,12 @@ java_library( "//runtime:descriptor_type_resolver", "//runtime:dispatcher", "//runtime:function_binding", + "//runtime:partial_vars", "//runtime:program", "//runtime:runtime_equality", "//runtime:runtime_helpers", "//runtime:standard_functions", + "//runtime:unknown_attributes", "//runtime/planner:program_planner", "//runtime/standard:type", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", 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 20b4e641a..de30902d3 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -65,13 +65,17 @@ import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import dev.cel.extensions.CelExtensions; 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; import dev.cel.runtime.CelLateFunctionBindings; import dev.cel.runtime.CelStandardFunctions; import dev.cel.runtime.CelStandardFunctions.StandardFunction; +import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.DefaultDispatcher; import dev.cel.runtime.DescriptorTypeResolver; +import dev.cel.runtime.PartialVars; import dev.cel.runtime.Program; import dev.cel.runtime.RuntimeEquality; import dev.cel.runtime.RuntimeHelpers; @@ -946,6 +950,35 @@ public void plan_comprehension_iterationLimit_success() throws Exception { ImmutableList.of(2L, 3L), ImmutableList.of(3L, 4L), ImmutableList.of(4L, 5L))); } + @Test + public void plan_partialEval_withWildcardQualification() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("unk", MapType.create(SimpleType.STRING, SimpleType.BOOL)) + .addVar("unk.a", SimpleType.BOOL) + .addVar("unk.b", SimpleType.BOOL) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "unk.a && unk.b && unk['c']"); + + Program program = PLANNER.plan(ast); + + CelUnknownSet result = + (CelUnknownSet) + program.eval( + PartialVars.of( + CelAttributePattern.create("unk") + .qualify(CelAttribute.Qualifier.ofWildCard()))); + + assertThat(result) + .isEqualTo( + CelUnknownSet.create( + ImmutableSet.of( + CelAttribute.create("unk"), + CelAttribute.create("unk").qualify(CelAttribute.Qualifier.ofString("a")), + CelAttribute.create("unk").qualify(CelAttribute.Qualifier.ofString("b"))), + ImmutableSet.of(2L, 5L, 7L))); + } + @Test public void localShadowIdentifier_inSelect() throws Exception { CelCompiler celCompiler = diff --git a/runtime/src/test/resources/planner_unknownFieldSelection.baseline b/runtime/src/test/resources/planner_unknownFieldSelection.baseline new file mode 100644 index 000000000..0cbc75299 --- /dev/null +++ b/runtime/src/test/resources/planner_unknownFieldSelection.baseline @@ -0,0 +1,111 @@ +Source: x +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=, unknown_attributes=[x]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[1]} + +Source: x +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[1]} + +Source: x.single_int32 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[2]} + +Source: x.single_int32 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[2]} + +Source: x.map_int32_int64[22] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[2]} + +Source: x.map_int32_int64[22] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x.map_int32_int64]} +result: CelUnknownSet{attributes=[x.map_int32_int64], unknownExprIds=[2]} + +Source: x.repeated_nested_message[1] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[2]} + +Source: x.repeated_nested_message[1] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x.repeated_nested_message]} +result: CelUnknownSet{attributes=[x.repeated_nested_message], unknownExprIds=[2]} + +Source: x.single_nested_message.bb +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[3]} + +Source: x.single_nested_message.bb +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x.single_nested_message.bb]} +result: CelUnknownSet{attributes=[x.single_nested_message.bb], unknownExprIds=[3]} + +Source: {1: x.single_int32} +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[5]} + +Source: {1: x.single_int32} +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[5]} + +Source: [1, x.single_int32] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[4]} + +Source: [1, x.single_int32] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[4]} diff --git a/runtime/src/test/resources/planner_unknownResultSet_errors.baseline b/runtime/src/test/resources/planner_unknownResultSet_errors.baseline new file mode 100644 index 000000000..812067ddf --- /dev/null +++ b/runtime/src/test/resources/planner_unknownResultSet_errors.baseline @@ -0,0 +1,81 @@ +Source: x.single_int32 == 1 && x.single_timestamp <= timestamp("bad timestamp string") +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[2]} + +Source: x.single_timestamp <= timestamp("bad timestamp string") && x.single_int32 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[8]} + +Source: x.single_timestamp <= timestamp("bad timestamp string") && x.single_timestamp > timestamp("another bad timestamp string") +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +error: evaluation error at test_location:89: Text 'another bad timestamp string' could not be parsed at index 0 +error_code: BAD_FORMAT + +Source: x.single_int32 == 1 || x.single_timestamp <= timestamp("bad timestamp string") +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[2]} + +Source: x.single_timestamp <= timestamp("bad timestamp string") || x.single_int32 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[8]} + +Source: x.single_timestamp <= timestamp("bad timestamp string") || x.single_timestamp > timestamp("another bad timestamp string") +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +error: evaluation error at test_location:89: Text 'another bad timestamp string' could not be parsed at index 0 +error_code: BAD_FORMAT + +Source: x +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[1]} \ No newline at end of file diff --git a/runtime/src/test/resources/planner_unknownResultSet_success.baseline b/runtime/src/test/resources/planner_unknownResultSet_success.baseline new file mode 100644 index 000000000..2f2c218d0 --- /dev/null +++ b/runtime/src/test/resources/planner_unknownResultSet_success.baseline @@ -0,0 +1,461 @@ +Source: x.single_int32 == 1 && true +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[2]} + +Source: x.single_int32 == 1 && false +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: false + +Source: x.single_int32 == 1 && x.single_int64 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[2, 7]} + +Source: true && x.single_int32 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[4]} + +Source: false && x.single_int32 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: false + +Source: x.single_int32 == 1 || x.single_string == "test" +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: true + +Source: x.single_int32 == 1 || x.single_string != "test" +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[2]} + +Source: x.single_int32 == 1 || x.single_int64 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[2, 7]} + +Source: true || x.single_int32 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: true + +Source: false || x.single_int32 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[4]} + +Source: x.single_int32.f(1) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[2]} + +Source: 1.f(x.single_int32) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[4]} + +Source: x.single_int64.f(x.single_int32) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[2, 5]} + +Source: [0, 2, 4].exists(z, z == 2 || z == x.single_int32) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: true + +Source: [0, 2, 4].exists(z, z == x.single_int32) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[10]} + +Source: [0, 2, 4].exists_one(z, z == 0 || (z == 2 && z == x.single_int32) || (z == 4 && z == x.single_int64)) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int64], unknownExprIds=[27]} + +Source: [0, 2].all(z, z == 2 || z == x.single_int32) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[13]} + +Source: [0, 2, 4].filter(z, z == 0 || (z == 2 && z == x.single_int32) || (z == 4 && z == x.single_int64)) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int64], unknownExprIds=[27]} + +Source: [0, 2, 4].map(z, z == 0 || (z == 2 && z == x.single_int32) || (z == 4 && z == x.single_int64)) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[18, 27]} + +Source: x.single_int32 == 1 ? 1 : 2 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[2]} + +Source: true ? x.single_int32 : 2 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[4]} + +Source: true ? 1 : x.single_int32 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: 1 + +Source: false ? x.single_int32 : 2 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: 2 + +Source: false ? 1 : x.single_int32 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[5]} + +Source: x.single_int64 == 1 ? x.single_int32 : x.single_int32 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int64], unknownExprIds=[2]} + +Source: {x.single_int32: 2, 3: 4} +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[4]} + +Source: {1: x.single_int32, 3: 4} +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[5]} + +Source: {1: x.single_int32, x.single_int64: 4} +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[5, 8]} + +Source: [1, x.single_int32, 3, 4] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[4]} + +Source: [1, x.single_int32, x.single_int64, 4] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[4, 6]} + +Source: TestAllTypes{single_int32: x.single_int32}.single_int32 == 2 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[4]} + +Source: TestAllTypes{single_int32: x.single_int32, single_int64: x.single_int64} +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[4, 7]} + +Source: unknown_list.map(x, x) +declare unknown_list { + value list(int) +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[unknown_list]} +result: CelUnknownSet{attributes=[unknown_list], unknownExprIds=[1]} \ No newline at end of file diff --git a/runtime/src/test/resources/unknownField.baseline b/runtime/src/test/resources/unknownField.baseline index c5f3c755a..8e4598bef 100644 --- a/runtime/src/test/resources/unknownField.baseline +++ b/runtime/src/test/resources/unknownField.baseline @@ -52,4 +52,4 @@ declare x { } =====> bindings: {} -result: CelUnknownSet{attributes=[], unknownExprIds=[3]} +result: CelUnknownSet{attributes=[], unknownExprIds=[3]} \ No newline at end of file diff --git a/testing/src/main/java/dev/cel/testing/BUILD.bazel b/testing/src/main/java/dev/cel/testing/BUILD.bazel index f2480a034..2ecabdf05 100644 --- a/testing/src/main/java/dev/cel/testing/BUILD.bazel +++ b/testing/src/main/java/dev/cel/testing/BUILD.bazel @@ -90,7 +90,7 @@ java_library( "//extensions:optional_library", "//runtime", "//runtime:function_binding", - "//runtime:late_function_binding", + "//runtime:partial_vars", "//runtime:unknown_attributes", "@cel_spec//proto/cel/expr:checked_java_proto", "@cel_spec//proto/cel/expr:syntax_java_proto", diff --git a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java index f3c1cf398..69db9c9db 100644 --- a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java +++ b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java @@ -75,6 +75,7 @@ import dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum; import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelLateFunctionBindings; @@ -83,6 +84,7 @@ import dev.cel.runtime.CelRuntimeFactory; import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.CelVariableResolver; +import dev.cel.runtime.PartialVars; import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; import java.io.IOException; import java.time.Duration; @@ -193,24 +195,43 @@ private Object runTest(Map input, CelLateFunctionBindings lateFunctio return runTestInternal(input, Optional.of(lateFunctionBindings)); } + @CanIgnoreReturnValue + protected Object runTest(Map input, CelAttributePattern... patterns) { + return runTestInternal(input, Optional.empty(), patterns); + } + /** * Helper to run a test for configured instance variables. Input must be of type map or {@link * CelVariableResolver}. */ - @SuppressWarnings("unchecked") private Object runTestInternal( Object input, Optional lateFunctionBindings) { + return runTestInternal(input, lateFunctionBindings, new CelAttributePattern[0]); + } + + // Test only + @SuppressWarnings("unchecked") + private Object runTestInternal( + Object input, + Optional lateFunctionBindings, + CelAttributePattern... patterns) { CelAbstractSyntaxTree ast = compileTestCase(); if (ast == null) { // Usually indicates test was not setup correctly println("Source compilation failed"); return null; } - printBinding(input); + printBinding(input, patterns); Object result = null; try { CelRuntime.Program program = celRuntime.createProgram(ast); - if (lateFunctionBindings.isPresent()) { + if (patterns.length > 0) { + PartialVars partialVars = + input instanceof Map + ? PartialVars.of((Map) input, patterns) + : PartialVars.of((CelVariableResolver) input, patterns); + result = program.eval(partialVars); + } else if (lateFunctionBindings.isPresent()) { if (input instanceof Map) { Map map = ((Map) input); CelVariableResolver variableResolver = (name) -> Optional.ofNullable(map.get(name)); @@ -2532,17 +2553,17 @@ private static String readResourceContent(String path) throws IOException { } @SuppressWarnings("unchecked") - private void printBinding(Object input) { + private void printBinding(Object input, CelAttributePattern... patterns) { if (input instanceof Map) { Map inputMap = (Map) input; - if (inputMap.isEmpty()) { + if (inputMap.isEmpty() && patterns.length == 0) { println("bindings: {}"); return; } boolean first = true; StringBuilder sb = new StringBuilder().append("{"); - for (Map.Entry entry : ((Map) input).entrySet()) { + for (Map.Entry entry : inputMap.entrySet()) { if (!first) { sb.append(", "); } @@ -2556,10 +2577,21 @@ private void printBinding(Object input) { sb.append(UnredactedDebugFormatForTest.unredactedToString(entry.getValue())); } } + if (patterns.length > 0) { + if (!inputMap.isEmpty()) { + sb.append(", "); + } + sb.append("unknown_attributes="); + sb.append(Arrays.toString(patterns)); + } sb.append("}"); println("bindings: " + sb); } else { - println("bindings: " + input); + if (patterns.length > 0) { + println("bindings: " + input + ", unknown_attributes=" + Arrays.toString(patterns)); + } else { + println("bindings: " + input); + } } }