diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntime.java b/runtime/src/main/java/dev/cel/runtime/CelRuntime.java index 416bca132..1e7fdcac8 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntime.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntime.java @@ -90,6 +90,14 @@ Object trace( CelEvaluationListener listener) throws CelEvaluationException; + /** + * Trace evaluates a compiled program using {@code partialVars} as the source of input variables + * and unknown attribute patterns. The listener is invoked as evaluation progresses through the + * AST. + */ + Object trace(PartialVars partialVars, CelEvaluationListener listener) + throws CelEvaluationException; + /** * Advance evaluation based on the current unknown context. * diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index 4cc738b6d..ed203d612 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -222,6 +222,17 @@ public Object trace( .trace(Activation.copyOf(mapValue), lateBoundFunctionResolver, null, listener); } + @Override + public Object trace(PartialVars partialVars, CelEvaluationListener listener) + throws CelEvaluationException { + return ((PlannedProgram) program) + .trace( + (name) -> partialVars.resolver().find(name).orElse(null), + EMPTY_FUNCTION_RESOLVER, + partialVars, + listener); + } + @Override public Object advanceEvaluation(UnknownContext context) throws CelEvaluationException { throw new UnsupportedOperationException("Unsupported operation."); diff --git a/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java b/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java index c9f4d083b..2543a9525 100644 --- a/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java @@ -110,6 +110,15 @@ public Object trace( return evalInternal(Activation.copyOf(mapValue), lateBoundFunctionResolver, listener); } + @Override + public Object trace(PartialVars partialVars, CelEvaluationListener listener) + throws CelEvaluationException { + return evalInternal( + UnknownContext.create(partialVars.resolver(), partialVars.unknowns()), + /* lateBoundFunctionResolver= */ Optional.empty(), + Optional.of(listener)); + } + @Override public Object advanceEvaluation(UnknownContext context) throws CelEvaluationException { return evalInternal(context, Optional.empty(), Optional.empty()); 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 c13d5857f..801e56d73 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -24,6 +24,9 @@ java_library( ":eval_create_list", ":eval_create_map", ":eval_create_struct", + ":eval_exhaustive_and", + ":eval_exhaustive_conditional", + ":eval_exhaustive_or", ":eval_fold", ":eval_late_bound_call", ":eval_optional_or", @@ -446,6 +449,7 @@ java_library( "//runtime:evaluation_listener", "//runtime:function_resolver", "//runtime:interpretable", + "//runtime:interpreter_util", "//runtime:partial_vars", "//runtime:resolved_overload", "@maven//:com_google_errorprone_error_prone_annotations", @@ -498,6 +502,48 @@ java_library( ], ) +java_library( + name = "eval_exhaustive_and", + srcs = ["EvalExhaustiveAnd.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "eval_exhaustive_or", + srcs = ["EvalExhaustiveOr.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "eval_exhaustive_conditional", + srcs = ["EvalExhaustiveConditional.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//runtime:accumulated_unknowns", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + java_library( name = "eval_block", srcs = ["EvalBlock.java"], diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveAnd.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveAnd.java new file mode 100644 index 000000000..ac3d07200 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveAnd.java @@ -0,0 +1,92 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.GlobalResolver; + +/** + * Implementation of logical AND with exhaustive evaluation (non-short-circuiting). + * + *

It evaluates all arguments, but prioritizes a false result over unknowns and errors to + * maintain semantic consistency with short-circuiting evaluation. + */ +@Immutable +final class EvalExhaustiveAnd extends PlannedInterpretable { + + @SuppressWarnings("Immutable") + private final PlannedInterpretable[] args; + + @Override + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { + AccumulatedUnknowns accumulatedUnknowns = null; + ErrorValue errorValue = null; + boolean hasFalse = false; + + for (PlannedInterpretable arg : args) { + Object argVal = evalNonstrictly(arg, resolver, frame); + if (argVal instanceof Boolean) { + if (!((boolean) argVal)) { + hasFalse = true; + } + } + + // If we already encountered a false, we do not need to accumulate unknowns or errors + // from subsequent terms because the final result will be false anyway. + if (hasFalse) { + continue; + } + + if (argVal instanceof AccumulatedUnknowns) { + accumulatedUnknowns = + accumulatedUnknowns == null + ? (AccumulatedUnknowns) argVal + : accumulatedUnknowns.merge((AccumulatedUnknowns) argVal); + } else if (argVal instanceof ErrorValue) { + if (errorValue == null) { + errorValue = (ErrorValue) argVal; + } + } + } + + if (hasFalse) { + return false; + } + + if (accumulatedUnknowns != null) { + return accumulatedUnknowns; + } + + if (errorValue != null) { + return errorValue; + } + + return true; + } + + static EvalExhaustiveAnd create(CelExpr expr, PlannedInterpretable[] args) { + return new EvalExhaustiveAnd(expr, args); + } + + private EvalExhaustiveAnd(CelExpr expr, PlannedInterpretable[] args) { + super(expr); + this.args = args; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveConditional.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveConditional.java new file mode 100644 index 000000000..01e242c0f --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveConditional.java @@ -0,0 +1,68 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; +import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.GlobalResolver; + +/** + * Implementation of conditional operator (ternary) with exhaustive evaluation + * (non-short-circuiting). + * + *

It evaluates all three arguments (condition, truthy, and falsy branches) but returns the + * result based on the condition, maintaining semantic consistency with short-circuiting evaluation. + */ +@Immutable +final class EvalExhaustiveConditional extends PlannedInterpretable { + + @SuppressWarnings("Immutable") + private final PlannedInterpretable[] args; + + @Override + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + PlannedInterpretable condition = args[0]; + PlannedInterpretable truthy = args[1]; + PlannedInterpretable falsy = args[2]; + + Object condResult = condition.eval(resolver, frame); + Object truthyVal = evalNonstrictly(truthy, resolver, frame); + Object falsyVal = evalNonstrictly(falsy, resolver, frame); + + if (condResult instanceof AccumulatedUnknowns) { + return condResult; + } + + if (!(condResult instanceof Boolean)) { + throw new IllegalArgumentException( + String.format("Expected boolean value, found :%s", condResult)); + } + + return (boolean) condResult ? truthyVal : falsyVal; + } + + static EvalExhaustiveConditional create(CelExpr expr, PlannedInterpretable[] args) { + return new EvalExhaustiveConditional(expr, args); + } + + private EvalExhaustiveConditional(CelExpr expr, PlannedInterpretable[] args) { + super(expr); + this.args = args; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveOr.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveOr.java new file mode 100644 index 000000000..07164f8c7 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveOr.java @@ -0,0 +1,92 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.GlobalResolver; + +/** + * Implementation of logical OR with exhaustive evaluation (non-short-circuiting). + * + *

It evaluates all arguments, but prioritizes a true result over unknowns and errors to maintain + * semantic consistency with short-circuiting evaluation. + */ +@Immutable +final class EvalExhaustiveOr extends PlannedInterpretable { + + @SuppressWarnings("Immutable") + private final PlannedInterpretable[] args; + + @Override + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { + AccumulatedUnknowns accumulatedUnknowns = null; + ErrorValue errorValue = null; + boolean hasTrue = false; + + for (PlannedInterpretable arg : args) { + Object argVal = evalNonstrictly(arg, resolver, frame); + if (argVal instanceof Boolean) { + if ((boolean) argVal) { + hasTrue = true; + } + } + + // If we already encountered a true, we do not need to accumulate unknowns or errors + // from subsequent terms because the final result will be true anyway. + if (hasTrue) { + continue; + } + + if (argVal instanceof AccumulatedUnknowns) { + accumulatedUnknowns = + accumulatedUnknowns == null + ? (AccumulatedUnknowns) argVal + : accumulatedUnknowns.merge((AccumulatedUnknowns) argVal); + } else if (argVal instanceof ErrorValue) { + if (errorValue == null) { + errorValue = (ErrorValue) argVal; + } + } + } + + if (hasTrue) { + return true; + } + + if (accumulatedUnknowns != null) { + return accumulatedUnknowns; + } + + if (errorValue != null) { + return errorValue; + } + + return false; + } + + static EvalExhaustiveOr create(CelExpr expr, PlannedInterpretable[] args) { + return new EvalExhaustiveOr(expr, args); + } + + private EvalExhaustiveOr(CelExpr expr, PlannedInterpretable[] args) { + super(expr); + this.args = args; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java b/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java index 8fa52db97..6bdeaf1df 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java @@ -19,6 +19,7 @@ import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelEvaluationListener; import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.InterpreterUtil; @Immutable abstract class PlannedInterpretable { @@ -29,7 +30,7 @@ final Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvalu Object result = evalInternal(resolver, frame); CelEvaluationListener listener = frame.getListener(); if (listener != null) { - listener.callback(expr, result); + listener.callback(expr, InterpreterUtil.maybeAdaptToCelUnknownSet(result)); } return result; } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index affe64381..e38d08f8f 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -259,11 +259,17 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { if (operator != null) { switch (operator) { case LOGICAL_OR: - return EvalOr.create(expr, evaluatedArgs); + return options.enableShortCircuiting() + ? EvalOr.create(expr, evaluatedArgs) + : EvalExhaustiveOr.create(expr, evaluatedArgs); case LOGICAL_AND: - return EvalAnd.create(expr, evaluatedArgs); + return options.enableShortCircuiting() + ? EvalAnd.create(expr, evaluatedArgs) + : EvalExhaustiveAnd.create(expr, evaluatedArgs); case CONDITIONAL: - return EvalConditional.create(expr, evaluatedArgs); + return options.enableShortCircuiting() + ? EvalConditional.create(expr, evaluatedArgs) + : EvalExhaustiveConditional.create(expr, evaluatedArgs); default: // fall-through } diff --git a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel index 9461e5e6a..3200a80e0 100644 --- a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel @@ -75,6 +75,7 @@ java_library( "//runtime:late_function_binding", "//runtime:lite_runtime", "//runtime:lite_runtime_factory", + "//runtime:partial_vars", "//runtime:proto_message_activation_factory", "//runtime:proto_message_runtime_equality", "//runtime:proto_message_runtime_helpers", diff --git a/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java b/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java index 3e29a00db..13d5dd550 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java @@ -16,8 +16,8 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; -import static org.junit.Assume.assumeTrue; +import com.google.api.expr.v1alpha1.CheckedExpr; import com.google.api.expr.v1alpha1.Constant; import com.google.api.expr.v1alpha1.Expr; import com.google.api.expr.v1alpha1.Type.PrimitiveType; @@ -36,7 +36,10 @@ import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; +import dev.cel.common.CelOverloadDecl; import dev.cel.common.CelProtoV1Alpha1AbstractSyntaxTree; import dev.cel.common.CelSource; import dev.cel.common.ast.CelConstant; @@ -104,8 +107,8 @@ public void evaluate_anyPackedEqualityUsingProtoDifferencer_success() throws Exc public void evaluate_v1alpha1CheckedExpr() throws Exception { // Note: v1alpha1 proto support exists only to help migrate existing consumers. // New users of CEL should use the canonical protos instead (I.E: dev.cel.expr) - com.google.api.expr.v1alpha1.CheckedExpr checkedExpr = - com.google.api.expr.v1alpha1.CheckedExpr.newBuilder() + CheckedExpr checkedExpr = + CheckedExpr.newBuilder() .setExpr( Expr.newBuilder() .setId(1) @@ -469,8 +472,6 @@ public void trace_withVariableResolver() throws Exception { public void trace_shortCircuitingDisabled_logicalAndAllBranchesVisited( @TestParameter boolean first, @TestParameter boolean second, @TestParameter boolean third) throws Exception { - // TODO: Implement exhaustive eval - assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); String expression = String.format("%s && %s && %s", first, second, third); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = @@ -516,8 +517,6 @@ public void trace_shortCircuitingDisabled_logicalAndAllBranchesVisited( @TestParameters("{source: 'x && false && false'}") public void trace_shortCircuitingDisabledWithUnknownsAndedToFalse_returnsFalse(String source) throws Exception { - // TODO: Implement exhaustive eval - assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -542,7 +541,8 @@ public void trace_shortCircuitingDisabledWithUnknownsAndedToFalse_returnsFalse(S .build(); CelAbstractSyntaxTree ast = cel.compile(source).getAst(); - boolean result = (boolean) cel.createProgram(ast).trace(listener); + PartialVars partialVars = PartialVars.of(CelAttributePattern.create("x")); + boolean result = (boolean) cel.createProgram(ast).trace(partialVars, listener); assertThat(result).isFalse(); assertThat(branchResults.build()).containsExactly(false, false, "x"); @@ -554,8 +554,6 @@ public void trace_shortCircuitingDisabledWithUnknownsAndedToFalse_returnsFalse(S @TestParameters("{source: 'x && true && true'}") public void trace_shortCircuitingDisabledWithUnknownAndedToTrue_returnsUnknown(String source) throws Exception { - // TODO: Implement exhaustive eval - assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -576,7 +574,8 @@ public void trace_shortCircuitingDisabledWithUnknownAndedToTrue_returnsUnknown(S .build(); CelAbstractSyntaxTree ast = cel.compile(source).getAst(); - Object unknownResult = cel.createProgram(ast).trace(listener); + PartialVars partialVars = PartialVars.of(CelAttributePattern.create("x")); + Object unknownResult = cel.createProgram(ast).trace(partialVars, listener); assertThat(InterpreterUtil.isUnknown(unknownResult)).isTrue(); assertThat(branchResults.build()).containsExactly(true, true, unknownResult); @@ -586,8 +585,6 @@ public void trace_shortCircuitingDisabledWithUnknownAndedToTrue_returnsUnknown(S public void trace_shortCircuitingDisabled_logicalOrAllBranchesVisited( @TestParameter boolean first, @TestParameter boolean second, @TestParameter boolean third) throws Exception { - // TODO: Implement exhaustive eval - assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); String expression = String.format("%s || %s || %s", first, second, third); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = @@ -633,8 +630,6 @@ public void trace_shortCircuitingDisabled_logicalOrAllBranchesVisited( @TestParameters("{source: 'x || false || false'}") public void trace_shortCircuitingDisabledWithUnknownsOredToFalse_returnsUnknown(String source) throws Exception { - // TODO: Implement exhaustive eval - assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -655,7 +650,8 @@ public void trace_shortCircuitingDisabledWithUnknownsOredToFalse_returnsUnknown( .build(); CelAbstractSyntaxTree ast = cel.compile(source).getAst(); - Object unknownResult = cel.createProgram(ast).trace(listener); + PartialVars partialVars = PartialVars.of(CelAttributePattern.create("x")); + Object unknownResult = cel.createProgram(ast).trace(partialVars, listener); assertThat(InterpreterUtil.isUnknown(unknownResult)).isTrue(); assertThat(branchResults.build()).containsExactly(false, false, unknownResult); @@ -667,8 +663,6 @@ public void trace_shortCircuitingDisabledWithUnknownsOredToFalse_returnsUnknown( @TestParameters("{source: 'x || true || true'}") public void trace_shortCircuitingDisabledWithUnknownOredToTrue_returnsTrue(String source) throws Exception { - // TODO: Implement exhaustive eval - assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -693,7 +687,8 @@ public void trace_shortCircuitingDisabledWithUnknownOredToTrue_returnsTrue(Strin .build(); CelAbstractSyntaxTree ast = cel.compile(source).getAst(); - boolean result = (boolean) cel.createProgram(ast).trace(listener); + PartialVars partialVars = PartialVars.of(CelAttributePattern.create("x")); + boolean result = (boolean) cel.createProgram(ast).trace(partialVars, listener); assertThat(result).isTrue(); assertThat(branchResults.build()).containsExactly(true, true, "x"); @@ -701,7 +696,6 @@ public void trace_shortCircuitingDisabledWithUnknownOredToTrue_returnsTrue(Strin @Test public void trace_shortCircuitingDisabled_ternaryAllBranchesVisited() throws Exception { - assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -731,8 +725,6 @@ public void trace_shortCircuitingDisabled_ternaryAllBranchesVisited() throws Exc @TestParameters("{source: 'true ? x : false'}") @TestParameters("{source: 'x ? true : false'}") public void trace_shortCircuitingDisabled_ternaryWithUnknowns(String source) throws Exception { - // TODO: Implement exhaustive eval - assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -753,7 +745,8 @@ public void trace_shortCircuitingDisabled_ternaryWithUnknowns(String source) thr .build(); CelAbstractSyntaxTree ast = cel.compile(source).getAst(); - Object unknownResult = cel.createProgram(ast).trace(listener); + PartialVars partialVars = PartialVars.of(CelAttributePattern.create("x")); + Object unknownResult = cel.createProgram(ast).trace(partialVars, listener); assertThat(InterpreterUtil.isUnknown(unknownResult)).isTrue(); assertThat(branchResults.build()).containsExactly(false, unknownResult, true); @@ -770,8 +763,6 @@ public void trace_shortCircuitingDisabled_ternaryWithUnknowns(String source) thr "{expression: 'true ? true : (1 / 0) > 2', firstVisited: true, secondVisited: true}") public void trace_shortCircuitingDisabled_ternaryWithError( String expression, boolean firstVisited, boolean secondVisited) throws Exception { - // TODO: Implement exhaustive eval - assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -810,6 +801,55 @@ public void trace_shortCircuitingDisabled_ternaryWithError( assertThat(branchResults.build()).containsExactly(firstVisited, secondVisited).inOrder(); } + @Test + public void trace_shortCircuitingDisabled_ternaryWithSelectedError() throws Exception { + Cel cel = + runtimeFlavor + .builder() + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile("true ? (1 / 0) : 2").getAst(); + CelRuntime.Program program = cel.createProgram(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> program.eval()); + assertThat(e).hasMessageThat().contains("evaluation error at :10: / by zero"); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.DIVIDE_BY_ZERO); + } + + @Test + public void trace_shortCircuitingDisabled_ternaryWithCustomError() throws Exception { + Cel cel = + runtimeFlavor + .builder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "error_func", + CelOverloadDecl.newGlobalOverload( + "error_func_overload", SimpleType.BOOL, ImmutableList.of()))) + .addFunctionBindings( + CelFunctionBinding.from( + "error_func_overload", + ImmutableList.of(), + args -> { + throw new IllegalArgumentException("custom error"); + })) + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile("true ? error_func() : false").getAst(); + CelRuntime.Program program = cel.createProgram(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> program.eval()); + assertThat(e).hasCauseThat().hasMessageThat().contains("custom error"); + } + @Test public void standardEnvironmentDisabledForRuntime_throws() throws Exception { CelCompiler celCompiler = @@ -817,11 +857,91 @@ public void standardEnvironmentDisabledForRuntime_throws() throws Exception { CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().setStandardEnvironmentEnabled(false).build(); CelAbstractSyntaxTree ast = celCompiler.compile("size('hello')").getAst(); + CelRuntime.Program program = celRuntime.createProgram(ast); - CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> celRuntime.createProgram(ast).eval()); + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> program.eval()); assertThat(e) .hasMessageThat() .contains("No matching overload for function 'size'. Overload candidates: size_string"); } + + @Test + public void trace_shortCircuitingDisabled_logicalAndPrefersFirstError() throws Exception { + Cel cel = + runtimeFlavor + .builder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "error_1", + CelOverloadDecl.newGlobalOverload( + "error_1_overload", SimpleType.BOOL, ImmutableList.of())), + CelFunctionDecl.newFunctionDeclaration( + "error_2", + CelOverloadDecl.newGlobalOverload( + "error_2_overload", SimpleType.BOOL, ImmutableList.of()))) + .addFunctionBindings( + CelFunctionBinding.from( + "error_1_overload", + ImmutableList.of(), + args -> { + throw new IllegalArgumentException("error 1"); + }), + CelFunctionBinding.from( + "error_2_overload", + ImmutableList.of(), + args -> { + throw new IllegalArgumentException("error 2"); + })) + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile("error_1() && error_2()").getAst(); + CelRuntime.Program program = cel.createProgram(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> program.eval()); + assertThat(e).hasCauseThat().hasMessageThat().contains("error 1"); + } + + @Test + public void trace_shortCircuitingDisabled_logicalOrPrefersFirstError() throws Exception { + Cel cel = + runtimeFlavor + .builder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "error_1", + CelOverloadDecl.newGlobalOverload( + "error_1_overload", SimpleType.BOOL, ImmutableList.of())), + CelFunctionDecl.newFunctionDeclaration( + "error_2", + CelOverloadDecl.newGlobalOverload( + "error_2_overload", SimpleType.BOOL, ImmutableList.of()))) + .addFunctionBindings( + CelFunctionBinding.from( + "error_1_overload", + ImmutableList.of(), + args -> { + throw new IllegalArgumentException("error 1"); + }), + CelFunctionBinding.from( + "error_2_overload", + ImmutableList.of(), + args -> { + throw new IllegalArgumentException("error 2"); + })) + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile("error_1() || error_2()").getAst(); + CelRuntime.Program program = cel.createProgram(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> program.eval()); + assertThat(e).hasCauseThat().hasMessageThat().contains("error 1"); + } }