From 2c83ae9fd3ca77cf4af8b962c967814024eb5888 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 2 Mar 2026 15:53:15 -0800 Subject: [PATCH] Support partial evaluation via unknowns in planner PiperOrigin-RevId: 877616092 --- .../test/java/dev/cel/extensions/BUILD.bazel | 2 + .../extensions/CelOptionalLibraryTest.java | 32 +- runtime/BUILD.bazel | 6 + .../src/main/java/dev/cel/runtime/BUILD.bazel | 37 +- .../java/dev/cel/runtime/CelRuntimeImpl.java | 11 + .../java/dev/cel/runtime/LiteProgramImpl.java | 13 + .../java/dev/cel/runtime/PartialVars.java | 64 +++ .../main/java/dev/cel/runtime/Program.java | 6 + .../java/dev/cel/runtime/ProgramImpl.java | 17 + .../java/dev/cel/runtime/planner/BUILD.bazel | 16 +- .../java/dev/cel/runtime/planner/EvalAnd.java | 12 +- .../cel/runtime/planner/EvalConditional.java | 5 +- .../cel/runtime/planner/EvalCreateList.java | 13 + .../cel/runtime/planner/EvalCreateMap.java | 65 ++- .../cel/runtime/planner/EvalCreateStruct.java | 12 + .../runtime/planner/EvalLateBoundCall.java | 12 + .../cel/runtime/planner/EvalOptionalOr.java | 5 + .../runtime/planner/EvalOptionalOrValue.java | 5 + .../java/dev/cel/runtime/planner/EvalOr.java | 12 +- .../cel/runtime/planner/EvalVarArgsCall.java | 12 + .../cel/runtime/planner/ExecutionFrame.java | 16 +- .../runtime/planner/NamespacedAttribute.java | 33 +- .../cel/runtime/planner/PlannedProgram.java | 28 +- .../runtime/planner/RelativeAttribute.java | 6 +- .../src/test/java/dev/cel/runtime/BUILD.bazel | 9 + .../cel/runtime/PlannerInterpreterTest.java | 252 +++++++++- .../resources/planner_unknownField.baseline | 95 ++++ .../planner_unknownResultSet_errors.baseline | 95 ++++ .../planner_unknownResultSet_success.baseline | 449 ++++++++++++++++++ .../src/test/resources/unknownField.baseline | 2 +- .../src/main/java/dev/cel/testing/BUILD.bazel | 2 +- .../dev/cel/testing/BaseInterpreterTest.java | 33 +- 32 files changed, 1305 insertions(+), 72 deletions(-) create mode 100644 runtime/src/main/java/dev/cel/runtime/PartialVars.java create mode 100644 runtime/src/test/resources/planner_unknownField.baseline create mode 100644 runtime/src/test/resources/planner_unknownResultSet_errors.baseline create mode 100644 runtime/src/test/resources/planner_unknownResultSet_success.baseline 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..ef4a464d0 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,14 @@ 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( + ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x"))); assertThat(InterpreterUtil.isUnknown(result)).isTrue(); } @@ -987,10 +989,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 +996,11 @@ 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( + ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x"))); assertThat(InterpreterUtil.isUnknown(result)).isTrue(); } @@ -1026,10 +1028,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 +1035,11 @@ 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( + ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("optx"))); assertThat(InterpreterUtil.isUnknown(result)).isTrue(); } diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index 55ee241a0..96e81ea9d 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -351,3 +351,9 @@ java_library( "//runtime/src/main/java/dev/cel/runtime:runtime_planner_impl", ], ) + +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/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 10dca9ece..6ae20cd3b 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", @@ -1318,6 +1321,35 @@ 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 = [ + ], + visibility = ["//third_party/java/cel:__subpackages__"], + 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 +1358,7 @@ java_library( deps = [ ":evaluation_exception", ":function_resolver", + ":partial_vars", ":variable_resolver", "@maven//:com_google_errorprone_error_prone_annotations", ], @@ -1339,8 +1372,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..9a260389a 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -134,6 +134,17 @@ public Object eval( return program.eval(resolver, lateBoundFunctionResolver); } + @Override + public Object eval(PartialVars partialVars) throws CelEvaluationException { + return program.eval(partialVars); + } + + @Override + public Object eval(PartialVars partialVars, CelFunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException { + return program.eval(partialVars, lateBoundFunctionResolver); + } + @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/LiteProgramImpl.java b/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java index 5e57f497b..c8e500514 100644 --- a/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java @@ -52,6 +52,19 @@ 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"); + } + + @Override + public Object eval(PartialVars partialVars, CelFunctionResolver lateBoundFunctionResolver) + 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..44b096826 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/PartialVars.java @@ -0,0 +1,64 @@ +// 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; + +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 dictate + * which missing attributes should evaluate to {@code dev.cel.runtime.CelUnknownSet}. + */ +@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 a {@link CelVariableResolver} and a list of {@link + * CelAttributePattern}s. + */ + public static PartialVars of( + CelVariableResolver resolver, Iterable unknowns) { + return new AutoValue_PartialVars(resolver, ImmutableList.copyOf(unknowns)); + } + + /** + * Constructs a new {@code PartialVars} from a map of variables and an array of {@link + * CelAttributePattern}s. + */ + public static PartialVars of(Map variables, CelAttributePattern... unknowns) { + return of( + (name) -> variables.containsKey(name) ? Optional.of(variables.get(name)) : Optional.empty(), + unknowns); + } + + /** + * Constructs a new {@code PartialVars} from a {@link CelVariableResolver} and an array of {@link + * CelAttributePattern}s. + */ + public static PartialVars of(CelVariableResolver resolver, CelAttributePattern... unknowns) { + return of(resolver, ImmutableList.copyOf(unknowns)); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/Program.java b/runtime/src/main/java/dev/cel/runtime/Program.java index c0982f1f8..c74f3ec17 100644 --- a/runtime/src/main/java/dev/cel/runtime/Program.java +++ b/runtime/src/main/java/dev/cel/runtime/Program.java @@ -43,4 +43,10 @@ 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; + + Object eval(PartialVars partialVars, CelFunctionResolver lateBoundFunctionResolver) + 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..1a2c1af77 100644 --- a/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java @@ -60,6 +60,23 @@ 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()), + Optional.empty(), + Optional.empty()); + } + + @Override + public Object eval(PartialVars partialVars, CelFunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException { + return evalInternal( + UnknownContext.create(partialVars.resolver(), partialVars.unknowns()), + Optional.of(lateBoundFunctionResolver), + 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/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index 6561e4e5c..f4d7c7282 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,7 @@ java_library( "//runtime:evaluation_exception_builder", "//runtime:function_resolver", "//runtime:interpretable", + "//runtime:partial_vars", "//runtime:program", "//runtime:resolved_overload", "//runtime:variable_resolver", @@ -129,6 +130,8 @@ java_library( "//common/types:type_providers", "//common/values", "//runtime:interpretable", + "//runtime:partial_vars", + "//runtime:unknown_attributes", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:org_jspecify_jspecify", @@ -181,7 +184,6 @@ java_library( ":qualifier", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", ], ) @@ -238,6 +240,7 @@ java_library( "//runtime:evaluation_exception", "//runtime:interpretable", "//runtime:resolved_overload", + "//runtime:unknown_attributes", ], ) @@ -253,6 +256,7 @@ java_library( "//runtime:evaluation_exception", "//runtime:interpretable", "//runtime:resolved_overload", + "//runtime:unknown_attributes", "@maven//:com_google_guava_guava", ], ) @@ -266,6 +270,7 @@ java_library( ":planned_interpretable", "//common/values", "//runtime:interpretable", + "//runtime:unknown_attributes", "@maven//:com_google_guava_guava", ], ) @@ -279,6 +284,7 @@ java_library( ":planned_interpretable", "//common/values", "//runtime:interpretable", + "//runtime:unknown_attributes", "@maven//:com_google_guava_guava", ], ) @@ -291,6 +297,7 @@ java_library( ":planned_interpretable", "//runtime:evaluation_exception", "//runtime:interpretable", + "//runtime:unknown_attributes", "@maven//:com_google_guava_guava", ], ) @@ -306,8 +313,8 @@ java_library( "//common/values:cel_value_provider", "//runtime:evaluation_exception", "//runtime:interpretable", + "//runtime:unknown_attributes", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", ], ) @@ -320,6 +327,7 @@ java_library( ":planned_interpretable", "//runtime:evaluation_exception", "//runtime:interpretable", + "//runtime:unknown_attributes", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], @@ -336,6 +344,7 @@ java_library( "//common/exceptions:invalid_argument", "//runtime:evaluation_exception", "//runtime:interpretable", + "//runtime:unknown_attributes", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], @@ -365,6 +374,7 @@ java_library( "//common/exceptions:iteration_budget_exceeded", "//runtime:evaluation_exception", "//runtime:function_resolver", + "//runtime:partial_vars", "//runtime:resolved_overload", ], ) @@ -425,6 +435,7 @@ java_library( ":planned_interpretable", "//common/exceptions:overload_not_found", "//runtime:interpretable", + "//runtime:interpreter_util", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], @@ -439,6 +450,7 @@ java_library( ":planned_interpretable", "//common/exceptions:overload_not_found", "//runtime:interpretable", + "//runtime:interpreter_util", "@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..a7c871e19 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java @@ -18,6 +18,7 @@ import com.google.common.base.Preconditions; import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.GlobalResolver; final class EvalAnd extends PlannedInterpretable { @@ -28,6 +29,7 @@ final class EvalAnd extends PlannedInterpretable { @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) { ErrorValue errorValue = null; + CelUnknownSet unknownSet = null; for (PlannedInterpretable arg : args) { Object argVal = evalNonstrictly(arg, resolver, frame); if (argVal instanceof Boolean) { @@ -37,8 +39,12 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { } } else if (argVal instanceof ErrorValue) { errorValue = (ErrorValue) argVal; + } else if (argVal instanceof CelUnknownSet) { + unknownSet = + unknownSet == null + ? (CelUnknownSet) argVal + : unknownSet.merge((CelUnknownSet) argVal); } else { - // TODO: Handle unknowns errorValue = ErrorValue.create( arg.exprId(), @@ -47,6 +53,10 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { } } + if (unknownSet != null) { + return unknownSet; + } + if (errorValue != null) { return errorValue; } 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..1a5eaf945 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java @@ -16,6 +16,7 @@ import com.google.common.base.Preconditions; import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.GlobalResolver; final class EvalConditional extends PlannedInterpretable { @@ -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 CelUnknownSet) { + 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..9a8bc55e9 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java @@ -17,6 +17,7 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.GlobalResolver; import java.util.Optional; @@ -32,9 +33,16 @@ final class EvalCreateList extends PlannedInterpretable { @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(values.length); + CelUnknownSet unknownSet = null; for (int i = 0; i < values.length; i++) { Object element = EvalHelpers.evalStrictly(values[i], resolver, frame); + if (element instanceof CelUnknownSet) { + CelUnknownSet currentUnknown = (CelUnknownSet) element; + unknownSet = unknownSet == null ? currentUnknown : unknownSet.merge(currentUnknown); + continue; + } + if (isOptional[i]) { if (!(element instanceof Optional)) { throw new IllegalArgumentException( @@ -51,6 +59,11 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval builder.add(element); } + + if (unknownSet != null) { + return unknownSet; + } + 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..fce871203 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java @@ -22,6 +22,7 @@ import dev.cel.common.exceptions.CelDuplicateKeyException; import dev.cel.common.exceptions.CelInvalidArgumentException; import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.GlobalResolver; import java.util.HashSet; import java.util.Optional; @@ -46,39 +47,55 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(keys.length); HashSet keysSeen = Sets.newHashSetWithExpectedSize(keys.length); + CelUnknownSet unknownSet = 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 CelUnknownSet) { + CelUnknownSet currentUnknown = (CelUnknownSet) key; + unknownSet = unknownSet == null ? currentUnknown : unknownSet.merge(currentUnknown); + } 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); + if (val instanceof CelUnknownSet) { + CelUnknownSet currentUnknown = (CelUnknownSet) val; + unknownSet = unknownSet == null ? currentUnknown : unknownSet.merge(currentUnknown); + } + + if (unknownSet != null) { + continue; + } + if (isOptional[i]) { if (!(val instanceof Optional)) { throw new IllegalArgumentException( @@ -94,13 +111,15 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval continue; } val = opt.get(); - } else { - System.out.println(); } builder.put(key, val); } + if (unknownSet != null) { + return unknownSet; + } + 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..e55808791 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java @@ -19,6 +19,7 @@ import dev.cel.common.values.CelValueProvider; import dev.cel.common.values.StructValue; import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.GlobalResolver; import java.util.Collections; import java.util.HashMap; @@ -46,9 +47,16 @@ final class EvalCreateStruct extends PlannedInterpretable { @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { Map fieldValues = new HashMap<>(); + CelUnknownSet unknownSet = null; for (int i = 0; i < keys.length; i++) { Object value = values[i].eval(resolver, frame); + if (value instanceof CelUnknownSet) { + CelUnknownSet currentUnknown = (CelUnknownSet) value; + unknownSet = unknownSet == null ? currentUnknown : unknownSet.merge(currentUnknown); + continue; + } + if (isOptional[i]) { if (!(value instanceof Optional)) { throw new IllegalArgumentException( @@ -71,6 +79,10 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval fieldValues.put(keys[i], value); } + if (unknownSet != null) { + return unknownSet; + } + // Either a primitive (wrappers) or a struct is produced Object value = valueProvider 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..9ed9e5de1 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java @@ -21,6 +21,7 @@ import dev.cel.common.values.CelValueConverter; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelResolvedOverload; +import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.GlobalResolver; final class EvalLateBoundCall extends PlannedInterpretable { @@ -36,10 +37,21 @@ final class EvalLateBoundCall extends PlannedInterpretable { @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { Object[] argVals = new Object[args.length]; + CelUnknownSet unknownSet = 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); + if (argVals[i] instanceof CelUnknownSet) { + unknownSet = + unknownSet == null + ? (CelUnknownSet) argVals[i] + : unknownSet.merge((CelUnknownSet) argVals[i]); + } + } + + if (unknownSet != null) { + return unknownSet; } 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..e31849f04 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.java @@ -18,6 +18,7 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.common.exceptions.CelOverloadNotFoundException; import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.InterpreterUtil; import java.util.Optional; @Immutable @@ -29,6 +30,10 @@ final class EvalOptionalOr extends PlannedInterpretable { public Object eval(GlobalResolver resolver, ExecutionFrame frame) { Object lhsValue = EvalHelpers.evalStrictly(lhs, resolver, frame); + if (InterpreterUtil.isUnknown(lhsValue)) { + 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..836f9ad95 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.java @@ -18,6 +18,7 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.common.exceptions.CelOverloadNotFoundException; import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.InterpreterUtil; import java.util.Optional; @Immutable @@ -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 (InterpreterUtil.isUnknown(lhsValue)) { + return lhsValue; + } + if (!(lhsValue instanceof Optional)) { throw new CelOverloadNotFoundException("orValue"); } 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..dccbfbf48 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java @@ -18,6 +18,7 @@ import com.google.common.base.Preconditions; import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.GlobalResolver; final class EvalOr extends PlannedInterpretable { @@ -28,6 +29,7 @@ final class EvalOr extends PlannedInterpretable { @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) { ErrorValue errorValue = null; + CelUnknownSet unknownSet = null; for (PlannedInterpretable arg : args) { Object argVal = evalNonstrictly(arg, resolver, frame); if (argVal instanceof Boolean) { @@ -37,8 +39,12 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { } } else if (argVal instanceof ErrorValue) { errorValue = (ErrorValue) argVal; + } else if (argVal instanceof CelUnknownSet) { + unknownSet = + unknownSet == null + ? (CelUnknownSet) argVal + : unknownSet.merge((CelUnknownSet) argVal); } else { - // TODO: Handle unknowns errorValue = ErrorValue.create( arg.exprId(), @@ -47,6 +53,10 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { } } + if (unknownSet != null) { + return unknownSet; + } + if (errorValue != null) { return errorValue; } 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..d7c089f19 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java @@ -20,6 +20,7 @@ import dev.cel.common.values.CelValueConverter; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelResolvedOverload; +import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.GlobalResolver; final class EvalVarArgsCall extends PlannedInterpretable { @@ -34,12 +35,23 @@ final class EvalVarArgsCall extends PlannedInterpretable { @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { Object[] argVals = new Object[args.length]; + CelUnknownSet unknownSet = 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); + if (resolvedOverload.isStrict() && argVals[i] instanceof CelUnknownSet) { + unknownSet = + unknownSet == null + ? (CelUnknownSet) argVals[i] + : unknownSet.merge((CelUnknownSet) argVals[i]); + } + } + + if (unknownSet != null) { + return unknownSet; } 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..f9dccfc3d 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( @@ -48,11 +50,21 @@ void incrementIterations() { } static ExecutionFrame create(CelFunctionResolver functionResolver, CelOptions celOptions) { - return new ExecutionFrame(functionResolver, celOptions.comprehensionMaxIterations()); + return new ExecutionFrame(functionResolver, null, celOptions.comprehensionMaxIterations()); } - private ExecutionFrame(CelFunctionResolver functionResolver, int limit) { + static ExecutionFrame create( + CelFunctionResolver functionResolver, PartialVars partialVars, CelOptions celOptions) { + return new ExecutionFrame(functionResolver, partialVars, celOptions.comprehensionMaxIterations()); + } + + 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/NamespacedAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java index cc8ca1d97..259ebf61b 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java @@ -23,8 +23,13 @@ import dev.cel.common.types.SimpleType; import dev.cel.common.types.TypeType; import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.CelAttribute; +import dev.cel.runtime.CelAttributePattern; +import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.PartialVars; import java.util.NoSuchElementException; +import java.util.Optional; import org.jspecify.annotations.Nullable; @Immutable @@ -60,7 +65,23 @@ public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { } Object value = resolver.resolve(name); - if (value != null) { + Optional partialVars = frame.partialVars(); + if (partialVars.isPresent() && !partialVars.get().unknowns().isEmpty()) { + ImmutableList patterns = partialVars.get().unknowns(); + CelAttribute attr = CelAttribute.fromQualifiedIdentifier(name); + for (Qualifier qualifier : qualifiers) { + attr = attr.qualify(CelAttribute.Qualifier.fromGeneric(qualifier.value())); + } + + Optional matchingPattern = findMatchingPattern(attr, patterns); + if (matchingPattern.isPresent()) { + return CelUnknownSet.create(matchingPattern.get().simplify(attr)); + } + + if (value != null) { + return applyQualifiers(value, celValueConverter, qualifiers); + } + } else if (value != null) { return applyQualifiers(value, celValueConverter, qualifiers); } @@ -150,6 +171,16 @@ private static Object applyQualifiers( return celValueConverter.maybeUnwrap(obj); } + private static Optional findMatchingPattern( + CelAttribute attr, ImmutableList patterns) { + for (CelAttributePattern pattern : patterns) { + if (pattern.isMatch(attr)) { + return Optional.of(pattern); + } + } + return Optional.empty(); + } + static NamespacedAttribute create( CelTypeProvider typeProvider, CelValueConverter celValueConverter, 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..4aa121015 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,7 @@ import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.CelVariableResolver; import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.PartialVars; import dev.cel.runtime.Program; import java.util.Collection; import java.util.Map; @@ -58,40 +59,53 @@ 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); + } + + @Override + public Object eval(PartialVars partialVars, CelFunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException { + return evalOrThrow( + interpretable(), (name) -> partialVars.resolver().find(name).orElse(null), lateBoundFunctionResolver, 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; 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..03ace54b0 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.CelUnknownSet; import dev.cel.runtime.GlobalResolver; /** @@ -33,13 +34,16 @@ final class RelativeAttribute implements Attribute { @Override public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { Object obj = EvalHelpers.evalStrictly(operand, ctx, frame); + if (obj instanceof CelUnknownSet) { + 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..962a169b5 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,13 @@ import dev.cel.common.CelOptions; import dev.cel.common.CelValidationException; import dev.cel.common.types.CelTypeProvider; +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.Map; +import org.junit.Test; import org.junit.runner.RunWith; /** Interpreter tests using ProgramPlanner */ @@ -37,7 +44,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 +78,246 @@ 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 unknownField_success instead skipBaselineVerification(); } @Override public void unknownResultSet() { - // TODO: Unknown support not implemented yet + // Exercised in 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_unknownField() { + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + + CelAttributePattern patternX = CelAttributePattern.fromQualifiedIdentifier("x"); + + source = "x.single_int32"; + runTestWithUnknowns(ImmutableMap.of(), patternX); + runTestWithUnknowns( + ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); + + source = "x.map_int32_int64[22]"; + runTestWithUnknowns(ImmutableMap.of(), patternX); + runTestWithUnknowns( + ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.map_int32_int64")); + + source = "x.repeated_nested_message[1]"; + runTestWithUnknowns(ImmutableMap.of(), patternX); + runTestWithUnknowns( + ImmutableMap.of(), + CelAttributePattern.fromQualifiedIdentifier("x.repeated_nested_message")); + + source = "x.single_nested_message.bb"; + runTestWithUnknowns(ImmutableMap.of(), patternX); + runTestWithUnknowns( + ImmutableMap.of(), + CelAttributePattern.fromQualifiedIdentifier("x.single_nested_message.bb")); + + source = "{1: x.single_int32}"; + runTestWithUnknowns(ImmutableMap.of(), patternX); + runTestWithUnknowns( + ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); + + source = "[1, x.single_int32]"; + runTestWithUnknowns(ImmutableMap.of(), patternX); + runTestWithUnknowns( + 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(); + Map 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"; + runTestWithUnknowns(variables, unknownInt32); + + source = "x.single_int32 == 1 && false"; + runTestWithUnknowns(variables, unknownInt32); + + source = "x.single_int32 == 1 && x.single_int64 == 1"; + runTestWithUnknowns(variables, unknownInt32, unknownInt64); + + source = "true && x.single_int32 == 1"; + runTestWithUnknowns(variables, unknownInt32); + + source = "false && x.single_int32 == 1"; + runTestWithUnknowns(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_string == \"test\""; + runTestWithUnknowns(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_string != \"test\""; + runTestWithUnknowns(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_int64 == 1"; + runTestWithUnknowns(variables, unknownInt32, unknownInt64); + + source = "true || x.single_int32 == 1"; + runTestWithUnknowns(variables, unknownInt32); + + source = "false || x.single_int32 == 1"; + runTestWithUnknowns(variables, unknownInt32); + + // dispatch test + declareFunction( + "f", + memberOverload( + "f", java.util.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, java.util.Objects::equals)) + .setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())) + .build(); + + source = "x.single_int32.f(1)"; + runTestWithUnknowns(variables, unknownInt32); + + source = "1.f(x.single_int32)"; + runTestWithUnknowns(variables, unknownInt32); + + source = "x.single_int64.f(x.single_int32)"; + runTestWithUnknowns(variables, unknownInt32, unknownInt64); + + source = "[0, 2, 4].exists(z, z == 2 || z == x.single_int32)"; + runTestWithUnknowns(variables, unknownInt32); + + source = "[0, 2, 4].exists(z, z == x.single_int32)"; + runTestWithUnknowns(variables, unknownInt32); + + source = + "[0, 2, 4].exists_one(z, z == 0 || (z == 2 && z == x.single_int32) " + + "|| (z == 4 && z == x.single_int64))"; + runTestWithUnknowns(variables, unknownInt32, unknownInt64); + + source = "[0, 2].all(z, z == 2 || z == x.single_int32)"; + runTestWithUnknowns(variables, unknownInt32); + + source = + "[0, 2, 4].filter(z, z == 0 || (z == 2 && z == x.single_int32) " + + "|| (z == 4 && z == x.single_int64))"; + runTestWithUnknowns(variables, unknownInt32, unknownInt64); + + source = + "[0, 2, 4].map(z, z == 0 || (z == 2 && z == x.single_int32) " + + "|| (z == 4 && z == x.single_int64))"; + runTestWithUnknowns(variables, unknownInt32, unknownInt64); + + source = "x.single_int32 == 1 ? 1 : 2"; + runTestWithUnknowns(variables, unknownInt32); + + source = "true ? x.single_int32 : 2"; + runTestWithUnknowns(variables, unknownInt32); + + source = "true ? 1 : x.single_int32"; + runTestWithUnknowns(variables, unknownInt32); + + source = "false ? x.single_int32 : 2"; + runTestWithUnknowns(variables, unknownInt32); + + source = "false ? 1 : x.single_int32"; + runTestWithUnknowns(variables, unknownInt32); + + source = "x.single_int64 == 1 ? x.single_int32 : x.single_int32"; + runTestWithUnknowns(variables, unknownInt32, unknownInt64); + + source = "{x.single_int32: 2, 3: 4}"; + runTestWithUnknowns(variables, unknownInt32); + + source = "{1: x.single_int32, 3: 4}"; + runTestWithUnknowns(variables, unknownInt32); + + source = "{1: x.single_int32, x.single_int64: 4}"; + runTestWithUnknowns(variables, unknownInt32, unknownInt64); + + source = "[1, x.single_int32, 3, 4]"; + runTestWithUnknowns(variables, unknownInt32); + + source = "[1, x.single_int32, x.single_int64, 4]"; + runTestWithUnknowns(variables, unknownInt32, unknownInt64); + + source = "TestAllTypes{single_int32: x.single_int32}.single_int32 == 2"; + runTestWithUnknowns(variables, unknownInt32); + + source = "TestAllTypes{single_int32: x.single_int32, single_int64: x.single_int64}"; + runTestWithUnknowns(variables, unknownInt32, unknownInt64); + } + + @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(); + Map variables = ImmutableMap.of("x", message); + CelAttributePattern unknownInt32 = + CelAttributePattern.fromQualifiedIdentifier("x.single_int32"); + + source = "x.single_int32 == 1 && x.single_timestamp <= timestamp(\"bad timestamp string\")"; + runTestWithUnknowns(variables, unknownInt32); + + source = "x.single_timestamp <= timestamp(\"bad timestamp string\") && x.single_int32 == 1"; + runTestWithUnknowns(variables, unknownInt32); + + source = + "x.single_timestamp <= timestamp(\"bad timestamp string\") " + + "&& x.single_timestamp > timestamp(\"another bad timestamp string\")"; + runTestWithUnknowns(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_timestamp <= timestamp(\"bad timestamp string\")"; + runTestWithUnknowns(variables, unknownInt32); + + source = "x.single_timestamp <= timestamp(\"bad timestamp string\") || x.single_int32 == 1"; + runTestWithUnknowns(variables, unknownInt32); + + source = + "x.single_timestamp <= timestamp(\"bad timestamp string\") " + + "|| x.single_timestamp > timestamp(\"another bad timestamp string\")"; + runTestWithUnknowns(variables, unknownInt32); + + source = "x"; + runTestWithUnknowns(ImmutableMap.of("y", message), unknownInt32); + + source = "x"; + runTestWithUnknowns(ImmutableMap.of(), unknownInt32); // For ident is unknown ==> unknown } } diff --git a/runtime/src/test/resources/planner_unknownField.baseline b/runtime/src/test/resources/planner_unknownField.baseline new file mode 100644 index 000000000..cc45b5ce3 --- /dev/null +++ b/runtime/src/test/resources/planner_unknownField.baseline @@ -0,0 +1,95 @@ +Source: x.single_int32 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {} +result: CelUnknownSet{attributes=[x], unknownExprIds=[]} + +Source: x.single_int32 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +Source: x.map_int32_int64[22] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {} +result: CelUnknownSet{attributes=[x], unknownExprIds=[]} + +Source: x.map_int32_int64[22] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {} +result: CelUnknownSet{attributes=[x.map_int32_int64], unknownExprIds=[]} + +Source: x.repeated_nested_message[1] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {} +result: CelUnknownSet{attributes=[x], unknownExprIds=[]} + +Source: x.repeated_nested_message[1] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {} +result: CelUnknownSet{attributes=[x.repeated_nested_message], unknownExprIds=[]} + +Source: x.single_nested_message.bb +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {} +result: CelUnknownSet{attributes=[x], unknownExprIds=[]} + +Source: x.single_nested_message.bb +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {} +result: CelUnknownSet{attributes=[x.single_nested_message.bb], unknownExprIds=[]} + +Source: {1: x.single_int32} +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {} +result: CelUnknownSet{attributes=[x], unknownExprIds=[]} + +Source: {1: x.single_int32} +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +Source: [1, x.single_int32] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {} +result: CelUnknownSet{attributes=[x], unknownExprIds=[]} + +Source: [1, x.single_int32] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} \ No newline at end of file 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..e17bc5189 --- /dev/null +++ b/runtime/src/test/resources/planner_unknownResultSet_errors.baseline @@ -0,0 +1,95 @@ +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +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 +} +} +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +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 +} +} +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: {y=single_string: "test" +single_timestamp { + seconds: 15 +} +} +error: evaluation error at test_location:0: No such attribute(s): x +error_code: ATTRIBUTE_NOT_FOUND + +Source: x +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {} +error: evaluation error at test_location:0: No such attribute(s): x +error_code: ATTRIBUTE_NOT_FOUND \ 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..0945171e9 --- /dev/null +++ b/runtime/src/test/resources/planner_unknownResultSet_success.baseline @@ -0,0 +1,449 @@ +Source: x.single_int32 == 1 && true +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +Source: x.single_int32 == 1 && false +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +} +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[]} + +Source: true && x.single_int32 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +Source: false && x.single_int32 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +} +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 +} +} +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[]} + +Source: true || x.single_int32 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +} +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int64, x.single_int32], unknownExprIds=[]} + +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 +} +} +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int64], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int64], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +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 +} +} +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 +} +} +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int64], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[]} + +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 +} +} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[]} \ 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 144ada5a8..e9f48c767 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,13 +195,23 @@ private Object runTest(Map input, CelLateFunctionBindings lateFunctio return runTestInternal(input, Optional.of(lateFunctionBindings)); } + protected Object runTestWithUnknowns(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]); + } + + private Object runTestInternal( + Object input, + Optional lateFunctionBindings, + CelAttributePattern... patterns) { CelAbstractSyntaxTree ast = compileTestCase(); if (ast == null) { // Usually indicates test was not setup correctly @@ -210,7 +222,18 @@ private Object runTestInternal( 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); + + if (lateFunctionBindings.isPresent()) { + result = program.eval(partialVars, lateFunctionBindings.get()); + } else { + result = program.eval(partialVars); + } + } else if (lateFunctionBindings.isPresent()) { if (input instanceof Map) { Map map = ((Map) input); CelVariableResolver variableResolver = (name) -> Optional.ofNullable(map.get(name)); @@ -233,6 +256,12 @@ private Object runTestInternal( } catch (CelEvaluationException e) { println("error: " + e.getMessage()); println("error_code: " + e.getErrorCode()); + } catch (RuntimeException e) { + if (patterns.length > 0) { + println("error: " + e.getMessage()); + } else { + throw e; + } } println(""); return result;