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