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 ee8987886..110fe468c 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -14,11 +14,13 @@ java_library( ], deps = [ ":attribute", + ":eval_and", ":eval_attribute", ":eval_const", ":eval_create_list", ":eval_create_map", ":eval_create_struct", + ":eval_or", ":eval_unary", ":eval_var_args_call", ":eval_zero_arity", @@ -26,6 +28,7 @@ java_library( "//:auto_value", "//common:cel_ast", "//common:container", + "//common:operator", "//common/annotations", "//common/ast", "//common/types", @@ -48,6 +51,8 @@ java_library( srcs = ["PlannedProgram.java"], deps = [ "//:auto_value", + "//common:runtime_exception", + "//common/values", "//runtime:activation", "//runtime:evaluation_exception", "//runtime:evaluation_exception_builder", @@ -137,6 +142,32 @@ java_library( ], ) +java_library( + name = "eval_or", + srcs = ["EvalOr.java"], + deps = [ + ":eval_helpers", + "//common/values", + "//runtime:evaluation_listener", + "//runtime:function_resolver", + "//runtime:interpretable", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_and", + srcs = ["EvalAnd.java"], + deps = [ + ":eval_helpers", + "//common/values", + "//runtime:evaluation_listener", + "//runtime:function_resolver", + "//runtime:interpretable", + "@maven//:com_google_guava_guava", + ], +) + java_library( name = "eval_create_struct", srcs = ["EvalCreateStruct.java"], @@ -178,3 +209,12 @@ java_library( "@maven//:com_google_guava_guava", ], ) + +java_library( + name = "eval_helpers", + srcs = ["EvalHelpers.java"], + deps = [ + "//common/values", + "//runtime:interpretable", + ], +) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java new file mode 100644 index 000000000..4bf9af517 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java @@ -0,0 +1,86 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; + +import com.google.common.base.Preconditions; +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.CelEvaluationListener; +import dev.cel.runtime.CelFunctionResolver; +import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.Interpretable; + +final class EvalAnd implements Interpretable { + + @SuppressWarnings("Immutable") + private final Interpretable[] args; + + @Override + public Object eval(GlobalResolver resolver) { + ErrorValue errorValue = null; + for (Interpretable arg : args) { + Object argVal = evalNonstrictly(arg, resolver); + if (argVal instanceof Boolean) { + // Short-circuit on false + if (!((boolean) argVal)) { + return false; + } + } else if (argVal instanceof ErrorValue) { + errorValue = (ErrorValue) argVal; + } else { + // TODO: Handle unknowns + throw new IllegalArgumentException( + String.format("Expected boolean value, found: %s", argVal)); + } + } + + if (errorValue != null) { + return errorValue; + } + + return true; + } + + @Override + public Object eval(GlobalResolver resolver, CelEvaluationListener listener) { + // TODO: Implement support + throw new UnsupportedOperationException("Not yet supported"); + } + + @Override + public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { + // TODO: Implement support + throw new UnsupportedOperationException("Not yet supported"); + } + + @Override + public Object eval( + GlobalResolver resolver, + CelFunctionResolver lateBoundFunctionResolver, + CelEvaluationListener listener) { + // TODO: Implement support + throw new UnsupportedOperationException("Not yet supported"); + } + + static EvalAnd create(Interpretable[] args) { + return new EvalAnd(args); + } + + private EvalAnd(Interpretable[] args) { + Preconditions.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 new file mode 100644 index 000000000..82bd6124a --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java @@ -0,0 +1,32 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.Interpretable; + +final class EvalHelpers { + + static Object evalNonstrictly(Interpretable interpretable, GlobalResolver resolver) { + try { + return interpretable.eval(resolver); + } catch (Exception e) { + return ErrorValue.create(e); + } + } + + private EvalHelpers() {} +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java new file mode 100644 index 000000000..afa02dfb8 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java @@ -0,0 +1,86 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; + +import com.google.common.base.Preconditions; +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.CelEvaluationListener; +import dev.cel.runtime.CelFunctionResolver; +import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.Interpretable; + +final class EvalOr implements Interpretable { + + @SuppressWarnings("Immutable") + private final Interpretable[] args; + + @Override + public Object eval(GlobalResolver resolver) { + ErrorValue errorValue = null; + for (Interpretable arg : args) { + Object argVal = evalNonstrictly(arg, resolver); + if (argVal instanceof Boolean) { + // Short-circuit on true + if (((boolean) argVal)) { + return true; + } + } else if (argVal instanceof ErrorValue) { + errorValue = (ErrorValue) argVal; + } else { + // TODO: Handle unknowns + throw new IllegalArgumentException( + String.format("Expected boolean value, found: %s", argVal)); + } + } + + if (errorValue != null) { + return errorValue; + } + + return false; + } + + @Override + public Object eval(GlobalResolver resolver, CelEvaluationListener listener) { + // TODO: Implement support + throw new UnsupportedOperationException("Not yet supported"); + } + + @Override + public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { + // TODO: Implement support + throw new UnsupportedOperationException("Not yet supported"); + } + + @Override + public Object eval( + GlobalResolver resolver, + CelFunctionResolver lateBoundFunctionResolver, + CelEvaluationListener listener) { + // TODO: Implement support + throw new UnsupportedOperationException("Not yet supported"); + } + + static EvalOr create(Interpretable[] args) { + return new EvalOr(args); + } + + private EvalOr(Interpretable[] args) { + Preconditions.checkArgument(args.length == 2); + this.args = args; + } +} 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 34aac58a9..2c0d402c2 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java @@ -16,6 +16,8 @@ import com.google.auto.value.AutoValue; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.CelRuntimeException; +import dev.cel.common.values.ErrorValue; import dev.cel.runtime.Activation; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelEvaluationExceptionBuilder; @@ -49,14 +51,28 @@ public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctio private Object evalOrThrow(Interpretable interpretable, GlobalResolver resolver) throws CelEvaluationException { try { - return interpretable.eval(resolver); + Object evalResult = interpretable.eval(resolver); + if (evalResult instanceof ErrorValue) { + ErrorValue errorValue = (ErrorValue) evalResult; + throw newCelEvaluationException(errorValue.value()); + } + + return evalResult; } catch (RuntimeException e) { throw newCelEvaluationException(e); } } private static CelEvaluationException newCelEvaluationException(Exception e) { - return CelEvaluationExceptionBuilder.newBuilder(e.getMessage()).setCause(e).build(); + CelEvaluationExceptionBuilder builder; + if (e instanceof CelRuntimeException) { + // Preserve detailed error, including error codes if one exists. + builder = CelEvaluationExceptionBuilder.newBuilder((CelRuntimeException) e); + } else { + builder = CelEvaluationExceptionBuilder.newBuilder(e.getMessage()).setCause(e); + } + + return builder.build(); } static Program create(Interpretable interpretable) { 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 805be193c..09b79617c 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -21,6 +21,7 @@ import javax.annotation.concurrent.ThreadSafe; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; +import dev.cel.common.Operator; import dev.cel.common.annotations.Internal; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; @@ -169,6 +170,17 @@ private Interpretable planCall(CelExpr expr, PlannerContext ctx) { // TODO: Handle all specialized calls (logical operators, conditionals, equals etc) String functionName = resolvedFunction.functionName(); + Operator operator = Operator.findReverse(functionName).orElse(null); + if (operator != null) { + switch (operator) { + case LOGICAL_OR: + return EvalOr.create(evaluatedArgs); + case LOGICAL_AND: + return EvalAnd.create(evaluatedArgs); + default: + // fall-through + } + } CelResolvedOverload resolvedOverload = null; if (resolvedFunction.overloadId().isPresent()) { diff --git a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel index 94a427685..8c8c66369 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel @@ -18,6 +18,7 @@ java_library( "//common:cel_descriptor_util", "//common:cel_source", "//common:compiler_common", + "//common:error_codes", "//common:operator", "//common:options", "//common/ast", 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 56bc1810a..3681583d8 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -29,8 +29,10 @@ import com.google.common.primitives.UnsignedLong; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelDescriptorUtil; +import dev.cel.common.CelErrorCode; import dev.cel.common.CelOptions; import dev.cel.common.CelSource; import dev.cel.common.Operator; @@ -460,6 +462,70 @@ public void plan_call_noMatchingOverload_throws() throws Exception { assertThat(e).hasMessageThat().contains("No matching overload for function: concat"); } + @Test + @TestParameters("{expression: 'true || true', expectedResult: true}") + @TestParameters("{expression: 'true || false', expectedResult: true}") + @TestParameters("{expression: 'false || true', expectedResult: true}") + @TestParameters("{expression: 'false || false', expectedResult: false}") + @TestParameters("{expression: 'true || (1 / 0 > 2)', expectedResult: true}") + @TestParameters("{expression: '(1 / 0 > 2) || true', expectedResult: true}") + public void plan_call_logicalOr_shortCircuit(String expression, boolean expectedResult) + throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expression: '(1 / 0 > 2) || (1 / 0 > 2)'}") + @TestParameters("{expression: 'false || (1 / 0 > 2)'}") + @TestParameters("{expression: '(1 / 0 > 2) || false'}") + public void plan_call_logicalOr_throws(String expression) throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + // TODO: Tag metadata (source loc) + assertThat(e).hasMessageThat().isEqualTo("evaluation error: / by zero"); + assertThat(e).hasCauseThat().isInstanceOf(ArithmeticException.class); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.DIVIDE_BY_ZERO); + } + + @Test + @TestParameters("{expression: 'true && true', expectedResult: true}") + @TestParameters("{expression: 'true && false', expectedResult: false}") + @TestParameters("{expression: 'false && true', expectedResult: false}") + @TestParameters("{expression: 'false && false', expectedResult: false}") + @TestParameters("{expression: 'false && (1 / 0 > 2)', expectedResult: false}") + @TestParameters("{expression: '(1 / 0 > 2) && false', expectedResult: false}") + public void plan_call_logicalAnd_shortCircuit(String expression, boolean expectedResult) + throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + boolean result = (boolean) program.eval(); + + assertThat(result).isEqualTo(expectedResult); + } + + @Test + @TestParameters("{expression: '(1 / 0 > 2) && (1 / 0 > 2)'}") + @TestParameters("{expression: 'true && (1 / 0 > 2)'}") + @TestParameters("{expression: '(1 / 0 > 2) && true'}") + public void plan_call_logicalAnd_throws(String expression) throws Exception { + CelAbstractSyntaxTree ast = compile(expression); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + // TODO: Tag metadata (source loc) + assertThat(e).hasMessageThat().isEqualTo("evaluation error: / by zero"); + assertThat(e).hasCauseThat().isInstanceOf(ArithmeticException.class); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.DIVIDE_BY_ZERO); + } + private CelAbstractSyntaxTree compile(String expression) throws Exception { CelAbstractSyntaxTree ast = CEL_COMPILER.parse(expression).getAst(); if (isParseOnly) {