diff --git a/common/src/main/java/dev/cel/common/values/ErrorValue.java b/common/src/main/java/dev/cel/common/values/ErrorValue.java index 6bc04cda4..940e1bef9 100644 --- a/common/src/main/java/dev/cel/common/values/ErrorValue.java +++ b/common/src/main/java/dev/cel/common/values/ErrorValue.java @@ -36,7 +36,7 @@ public abstract class ErrorValue extends CelValue { public abstract long exprId(); @Override - public abstract Exception value(); + public abstract RuntimeException value(); @Override public boolean isZeroValue() { @@ -48,7 +48,9 @@ public CelType celType() { return SimpleType.ERROR; } - public static ErrorValue create(long exprId, Exception value) { + public static ErrorValue create(long exprId, RuntimeException value) { return new AutoValue_ErrorValue(exprId, value); } + + ErrorValue() {} } 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 a8882f539..f17f63572 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -336,6 +336,7 @@ java_library( name = "eval_conditional", srcs = ["EvalConditional.java"], deps = [ + ":eval_helpers", ":planned_interpretable", "//common/ast", "//runtime:accumulated_unknowns", @@ -373,6 +374,7 @@ java_library( "//runtime:accumulated_unknowns", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", ], ) @@ -383,10 +385,11 @@ java_library( ":eval_helpers", ":planned_interpretable", "//common/ast", + "//common/values", "//runtime:accumulated_unknowns", - "//runtime:evaluation_exception", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", ], ) @@ -401,6 +404,7 @@ java_library( "//runtime:accumulated_unknowns", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", ], ) @@ -942,6 +946,7 @@ cel_android_library( name = "eval_conditional_android", srcs = ["EvalConditional.java"], deps = [ + ":eval_helpers_android", ":planned_interpretable_android", "//common/ast:ast_android", "//runtime:evaluation_exception", @@ -979,6 +984,7 @@ cel_android_library( "//runtime:interpretable_android", "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", ], ) @@ -989,10 +995,11 @@ cel_android_library( ":eval_helpers_android", ":planned_interpretable_android", "//common/ast:ast_android", - "//runtime:evaluation_exception", + "//common/values:values_android", "//runtime:interpretable_android", "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", ], ) @@ -1007,6 +1014,7 @@ cel_android_library( "//runtime:interpretable_android", "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//: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 11da26a50..d75571e0e 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java @@ -14,7 +14,7 @@ package dev.cel.runtime.planner; -import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; +import static dev.cel.runtime.planner.EvalHelpers.evalBooleanNonstrictly; import com.google.common.base.Preconditions; import dev.cel.common.ast.CelExpr; @@ -32,7 +32,7 @@ Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { ErrorValue errorValue = null; AccumulatedUnknowns unknowns = null; for (PlannedInterpretable arg : args) { - Object argVal = evalNonstrictly(arg, resolver, frame); + Object argVal = evalBooleanNonstrictly(arg, resolver, frame); if (argVal instanceof Boolean) { // Short-circuit on false if (!((boolean) argVal)) { @@ -45,12 +45,6 @@ Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { } } else if (argVal instanceof AccumulatedUnknowns) { unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVal); - } else { - errorValue = - ErrorValue.create( - arg.expr().id(), - new IllegalArgumentException( - String.format("Expected boolean value, found: %s", argVal))); } } 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 c2d730cdf..e3489e409 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java @@ -14,7 +14,9 @@ package dev.cel.runtime.planner; -import com.google.common.base.Preconditions; +import static com.google.common.base.Preconditions.checkArgument; +import static dev.cel.runtime.planner.EvalHelpers.evalStrictly; + import dev.cel.common.ast.CelExpr; import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; @@ -30,16 +32,15 @@ Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEva PlannedInterpretable condition = args[0]; PlannedInterpretable truthy = args[1]; PlannedInterpretable falsy = args[2]; - Object condResult = condition.eval(resolver, frame); + Object condResult = evalStrictly(condition, resolver, frame); if (condResult instanceof AccumulatedUnknowns) { return condResult; } if (!(condResult instanceof Boolean)) { throw new IllegalArgumentException( - String.format("Expected boolean value, found :%s", condResult)); + String.format("Expected boolean value, found: %s", condResult)); } - // TODO: Handle exhaustive eval if ((boolean) condResult) { return truthy.eval(resolver, frame); } @@ -53,7 +54,7 @@ static EvalConditional create(CelExpr expr, PlannedInterpretable[] args) { private EvalConditional(CelExpr expr, PlannedInterpretable[] args) { super(expr); - Preconditions.checkArgument(args.length == 3); + checkArgument(args.length == 3); this.args = args; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveAnd.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveAnd.java index ac3d07200..4ef464976 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveAnd.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveAnd.java @@ -14,7 +14,8 @@ package dev.cel.runtime.planner; -import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; +import static com.google.common.base.Preconditions.checkArgument; +import static dev.cel.runtime.planner.EvalHelpers.evalBooleanNonstrictly; import com.google.errorprone.annotations.Immutable; import dev.cel.common.ast.CelExpr; @@ -41,11 +42,9 @@ Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { boolean hasFalse = false; for (PlannedInterpretable arg : args) { - Object argVal = evalNonstrictly(arg, resolver, frame); - if (argVal instanceof Boolean) { - if (!((boolean) argVal)) { - hasFalse = true; - } + Object argVal = evalBooleanNonstrictly(arg, resolver, frame); + if (argVal instanceof Boolean && !(boolean) argVal) { + hasFalse = true; } // If we already encountered a false, we do not need to accumulate unknowns or errors @@ -55,10 +54,7 @@ Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { } if (argVal instanceof AccumulatedUnknowns) { - accumulatedUnknowns = - accumulatedUnknowns == null - ? (AccumulatedUnknowns) argVal - : accumulatedUnknowns.merge((AccumulatedUnknowns) argVal); + accumulatedUnknowns = AccumulatedUnknowns.maybeMerge(accumulatedUnknowns, argVal); } else if (argVal instanceof ErrorValue) { if (errorValue == null) { errorValue = (ErrorValue) argVal; @@ -87,6 +83,7 @@ static EvalExhaustiveAnd create(CelExpr expr, PlannedInterpretable[] args) { private EvalExhaustiveAnd(CelExpr expr, PlannedInterpretable[] args) { super(expr); + checkArgument(args.length == 2); 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 index 01e242c0f..3f4e19b7b 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveConditional.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveConditional.java @@ -14,12 +14,14 @@ package dev.cel.runtime.planner; +import static com.google.common.base.Preconditions.checkArgument; +import static dev.cel.runtime.planner.EvalHelpers.evalBooleanNonstrictly; 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.CelEvaluationException; import dev.cel.runtime.GlobalResolver; /** @@ -36,24 +38,19 @@ final class EvalExhaustiveConditional extends PlannedInterpretable { private final PlannedInterpretable[] args; @Override - Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { PlannedInterpretable condition = args[0]; PlannedInterpretable truthy = args[1]; PlannedInterpretable falsy = args[2]; - Object condResult = condition.eval(resolver, frame); + Object condResult = evalBooleanNonstrictly(condition, resolver, frame); Object truthyVal = evalNonstrictly(truthy, resolver, frame); Object falsyVal = evalNonstrictly(falsy, resolver, frame); - if (condResult instanceof AccumulatedUnknowns) { + if (condResult instanceof AccumulatedUnknowns || condResult instanceof ErrorValue) { return condResult; } - if (!(condResult instanceof Boolean)) { - throw new IllegalArgumentException( - String.format("Expected boolean value, found :%s", condResult)); - } - return (boolean) condResult ? truthyVal : falsyVal; } @@ -63,6 +60,7 @@ static EvalExhaustiveConditional create(CelExpr expr, PlannedInterpretable[] arg private EvalExhaustiveConditional(CelExpr expr, PlannedInterpretable[] args) { super(expr); + checkArgument(args.length == 3); 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 index 07164f8c7..0d11f5268 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveOr.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveOr.java @@ -14,7 +14,8 @@ package dev.cel.runtime.planner; -import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; +import static com.google.common.base.Preconditions.checkArgument; +import static dev.cel.runtime.planner.EvalHelpers.evalBooleanNonstrictly; import com.google.errorprone.annotations.Immutable; import dev.cel.common.ast.CelExpr; @@ -41,11 +42,9 @@ Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { boolean hasTrue = false; for (PlannedInterpretable arg : args) { - Object argVal = evalNonstrictly(arg, resolver, frame); - if (argVal instanceof Boolean) { - if ((boolean) argVal) { - hasTrue = true; - } + Object argVal = evalBooleanNonstrictly(arg, resolver, frame); + if (argVal instanceof Boolean && (boolean) argVal) { + hasTrue = true; } // If we already encountered a true, we do not need to accumulate unknowns or errors @@ -55,10 +54,7 @@ Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { } if (argVal instanceof AccumulatedUnknowns) { - accumulatedUnknowns = - accumulatedUnknowns == null - ? (AccumulatedUnknowns) argVal - : accumulatedUnknowns.merge((AccumulatedUnknowns) argVal); + accumulatedUnknowns = AccumulatedUnknowns.maybeMerge(accumulatedUnknowns, argVal); } else if (argVal instanceof ErrorValue) { if (errorValue == null) { errorValue = (ErrorValue) argVal; @@ -87,6 +83,7 @@ static EvalExhaustiveOr create(CelExpr expr, PlannedInterpretable[] args) { private EvalExhaustiveOr(CelExpr expr, PlannedInterpretable[] args) { super(expr); + checkArgument(args.length == 2); this.args = args; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java index 1b8d61234..9e7caa9a3 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java @@ -31,20 +31,33 @@ final class EvalHelpers { static Object evalNonstrictly( PlannedInterpretable interpretable, GlobalResolver resolver, ExecutionFrame frame) { try { - return interpretable.eval(resolver, frame); + return evalStrictly(interpretable, resolver, frame); + } catch (LocalizedEvaluationException e) { + return ErrorValue.create(e.exprId(), e); + } + } + + static Object evalBooleanNonstrictly( + PlannedInterpretable interpretable, GlobalResolver resolver, ExecutionFrame frame) { + try { + Object val = evalStrictly(interpretable, resolver, frame); + if (!(val instanceof Boolean) && !(val instanceof AccumulatedUnknowns)) { + throw new LocalizedEvaluationException( + new IllegalArgumentException(String.format("Expected boolean value, found: %s", val)), + CelErrorCode.INTERNAL_ERROR, + interpretable.expr().id()); + } + return val; } catch (LocalizedEvaluationException e) { - // Intercept the localized exception to get a more specific expr ID for error reporting - // Example: foo [1] && strict_err [2] -> ID 2 is propagated. return ErrorValue.create(e.exprId(), e); - } catch (Exception e) { - return ErrorValue.create(interpretable.expr().id(), e); } } static Object evalStrictly( PlannedInterpretable interpretable, GlobalResolver resolver, ExecutionFrame frame) { + Object val; try { - return interpretable.eval(resolver, frame); + val = interpretable.eval(resolver, frame); } catch (LocalizedEvaluationException e) { // Already localized - propagate as-is to preserve inner expression ID throw e; @@ -56,6 +69,10 @@ static Object evalStrictly( throw new LocalizedEvaluationException( e, CelErrorCode.INTERNAL_ERROR, interpretable.expr().id()); } + if (val instanceof ErrorValue) { + throw ((ErrorValue) val).value(); + } + return val; } static Object dispatch( 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 849b6e7b4..3980572a6 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java @@ -14,7 +14,7 @@ package dev.cel.runtime.planner; -import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; +import static dev.cel.runtime.planner.EvalHelpers.evalBooleanNonstrictly; import com.google.common.base.Preconditions; import dev.cel.common.ast.CelExpr; @@ -32,7 +32,7 @@ Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { ErrorValue errorValue = null; AccumulatedUnknowns unknowns = null; for (PlannedInterpretable arg : args) { - Object argVal = evalNonstrictly(arg, resolver, frame); + Object argVal = evalBooleanNonstrictly(arg, resolver, frame); if (argVal instanceof Boolean) { // Short-circuit on true if (((boolean) argVal)) { @@ -45,12 +45,6 @@ Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { } } else if (argVal instanceof AccumulatedUnknowns) { unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVal); - } else { - errorValue = - ErrorValue.create( - arg.expr().id(), - new IllegalArgumentException( - String.format("Expected boolean value, found: %s", argVal))); } } 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 2f007923e..458e8f4e0 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java @@ -175,7 +175,6 @@ public Object evalOrThrow( ErrorValue errorValue = (ErrorValue) evalResult; throw newCelEvaluationException(errorValue.exprId(), errorValue.value()); } - return InterpreterUtil.maybeAdaptToCelUnknownSet(evalResult); } catch (RuntimeException e) { throw newCelEvaluationException(interpretable().expr().id(), e); @@ -192,21 +191,17 @@ public Object trace( } private CelEvaluationException newCelEvaluationException(long exprId, Throwable e) { - if (e instanceof CelEvaluationException) { - return (CelEvaluationException) e; - } - CelEvaluationExceptionBuilder builder; if (e instanceof LocalizedEvaluationException) { // Use the localized expr ID (most specific error location) LocalizedEvaluationException localized = (LocalizedEvaluationException) e; exprId = localized.exprId(); - Throwable cause = localized.getCause(); - if (cause instanceof CelRuntimeException) { - builder = CelEvaluationExceptionBuilder.newBuilder((CelRuntimeException) cause); - } else { - builder = CelEvaluationExceptionBuilder.newBuilder(cause.getMessage()).setCause(cause); - } - } else if (e instanceof CelRuntimeException) { + e = localized.getCause(); + } + if (e instanceof CelEvaluationException) { + return (CelEvaluationException) e; + } + CelEvaluationExceptionBuilder builder; + if (e instanceof CelRuntimeException) { builder = CelEvaluationExceptionBuilder.newBuilder((CelRuntimeException) e); } else { // Unhandled function dispatch failures wraps the original exception with a descriptive diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index ebf8e1cdb..2c592a93f 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -1688,4 +1688,181 @@ private enum PresenceTestCase { this.expected = expected; } } + + @Test + public void plan_exhaustiveLogicalOr_shortCircuitDominatesError( + @TestParameter({"true || (1 / 0 == 0)", "(1 / 0 == 0) || true"}) String expression) + throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + ProgramPlanner planner = + newPlannerWithOptions( + CelOptions.current() + .enableHeterogeneousNumericComparisons(true) + .enableShortCircuiting(false) + .build()); + Program program = planner.plan(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo(true); + } + + @Test + public void plan_exhaustiveLogicalAnd_shortCircuitDominatesError( + @TestParameter({"false && (1 / 0 == 0)", "(1 / 0 == 0) && false"}) String expression) + throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + ProgramPlanner planner = + newPlannerWithOptions( + CelOptions.current() + .enableHeterogeneousNumericComparisons(true) + .enableShortCircuiting(false) + .build()); + Program program = planner.plan(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo(false); + } + + @Test + public void plan_exhaustiveConditional_nonStrictConditionError() throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile("(1 / 0 == 0) ? 'a' : 'b'").getAst(); + ProgramPlanner planner = + newPlannerWithOptions(CelOptions.current().enableShortCircuiting(false).build()); + Program program = planner.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.DIVIDE_BY_ZERO); + } + + @Test + public void plan_exhaustiveLogicalOps_invalidNonBooleanTypeThrows( + @TestParameter({"true && x", "x && true", "false || x", "x || false"}) String expression) + throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder().addVar("x", SimpleType.DYN).build(); + CelAbstractSyntaxTree ast = compiler.compile(expression).getAst(); + ProgramPlanner planner = + newPlannerWithOptions(CelOptions.current().enableShortCircuiting(false).build()); + Program program = planner.plan(ast); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> program.eval(ImmutableMap.of("x", 1L))); + + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.INTERNAL_ERROR); + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(e).hasCauseThat().hasMessageThat().contains("Expected boolean value, found: 1"); + } + + @Test + public void plan_exhaustiveLogicalOr_shortCircuitValueDominatesInvalidNonBooleanType( + @TestParameter({"true || x", "x || true"}) String expression) throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder().addVar("x", SimpleType.DYN).build(); + CelAbstractSyntaxTree ast = compiler.compile(expression).getAst(); + ProgramPlanner planner = + newPlannerWithOptions(CelOptions.current().enableShortCircuiting(false).build()); + Program program = planner.plan(ast); + + Object result = program.eval(ImmutableMap.of("x", 1L)); + + assertThat(result).isEqualTo(true); + } + + @Test + public void plan_exhaustiveLogicalAnd_shortCircuitValueDominatesInvalidNonBooleanType( + @TestParameter({"false && x", "x && false"}) String expression) throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder().addVar("x", SimpleType.DYN).build(); + CelAbstractSyntaxTree ast = compiler.compile(expression).getAst(); + ProgramPlanner planner = + newPlannerWithOptions(CelOptions.current().enableShortCircuiting(false).build()); + Program program = planner.plan(ast); + + Object result = program.eval(ImmutableMap.of("x", 1L)); + + assertThat(result).isEqualTo(false); + } + + @Test + public void plan_conditional_unwrapsErrorValue() throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile("(1 / 0 == 0) ? 'a' : 'b'").getAst(); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.DIVIDE_BY_ZERO); + } + + @Test + @SuppressWarnings("Immutable") // Test only + public void plan_functionThrowsWithCause_unwrapsOriginalCause() throws Exception { + IllegalArgumentException rootCause = new IllegalArgumentException("nested root cause"); + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addFunctionDeclarations( + newFunctionDeclaration( + "throw", newGlobalOverload("throw_int", SimpleType.INT, SimpleType.INT))) + .build(); + DefaultDispatcher.Builder builder = DefaultDispatcher.newBuilder(); + addBindingsToDispatcher( + builder, + CelFunctionBinding.fromOverloads( + "throw", + CelFunctionBinding.from( + "throw_int", + Long.class, + (Long arg) -> { + throw new RuntimeException("outer wrapper", rootCause); + }))); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + VALUE_PROVIDER, + builder.build(), + CEL_VALUE_CONVERTER, + CEL_CONTAINER, + CEL_OPTIONS, + ImmutableSet.of(), + CelAsyncEvaluationOptions.defaultOptions(), + /* asyncExecutor= */ null); + Program program = planner.plan(compiler.compile("throw(1)").getAst()); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + + assertThat(e).hasCauseThat().hasCauseThat().isSameInstanceAs(rootCause); + } + + @Test + public void plan_exhaustiveConditional_untakenBranchError_evaluatesSuccessfully( + @TestParameter({"true ? 42 : (1 / 0)", "false ? (1 / 0) : 42"}) String expression) + throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + ProgramPlanner planner = + newPlannerWithOptions( + CelOptions.current() + .enableHeterogeneousNumericComparisons(true) + .enableShortCircuiting(false) + .build()); + Program program = planner.plan(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo(42L); + } + + private static ProgramPlanner newPlannerWithOptions(CelOptions options) { + return ProgramPlanner.newPlanner( + TYPE_PROVIDER, + VALUE_PROVIDER, + newDispatcher(), + CEL_VALUE_CONVERTER, + CEL_CONTAINER, + options, + ImmutableSet.of("late_bound_func"), + CelAsyncEvaluationOptions.defaultOptions(), + /* asyncExecutor= */ null); + } }