Skip to content

Commit a31cb05

Browse files
l46kokcopybara-github
authored andcommitted
Implement exhaustive eval for planner
PiperOrigin-RevId: 903345007
1 parent 1cd2806 commit a31cb05

11 files changed

Lines changed: 485 additions & 31 deletions

File tree

runtime/src/main/java/dev/cel/runtime/CelRuntime.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ Object trace(
9090
CelEvaluationListener listener)
9191
throws CelEvaluationException;
9292

93+
/**
94+
* Trace evaluates a compiled program using {@code partialVars} as the source of input variables
95+
* and unknown attribute patterns. The listener is invoked as evaluation progresses through the
96+
* AST.
97+
*/
98+
Object trace(PartialVars partialVars, CelEvaluationListener listener)
99+
throws CelEvaluationException;
100+
93101
/**
94102
* Advance evaluation based on the current unknown context.
95103
*

runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,17 @@ public Object trace(
222222
.trace(Activation.copyOf(mapValue), lateBoundFunctionResolver, null, listener);
223223
}
224224

225+
@Override
226+
public Object trace(PartialVars partialVars, CelEvaluationListener listener)
227+
throws CelEvaluationException {
228+
return ((PlannedProgram) program)
229+
.trace(
230+
(name) -> partialVars.resolver().find(name).orElse(null),
231+
EMPTY_FUNCTION_RESOLVER,
232+
partialVars,
233+
listener);
234+
}
235+
225236
@Override
226237
public Object advanceEvaluation(UnknownContext context) throws CelEvaluationException {
227238
throw new UnsupportedOperationException("Unsupported operation.");

runtime/src/main/java/dev/cel/runtime/ProgramImpl.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,15 @@ public Object trace(
110110
return evalInternal(Activation.copyOf(mapValue), lateBoundFunctionResolver, listener);
111111
}
112112

113+
@Override
114+
public Object trace(PartialVars partialVars, CelEvaluationListener listener)
115+
throws CelEvaluationException {
116+
return evalInternal(
117+
UnknownContext.create(partialVars.resolver(), partialVars.unknowns()),
118+
/* lateBoundFunctionResolver= */ Optional.empty(),
119+
Optional.of(listener));
120+
}
121+
113122
@Override
114123
public Object advanceEvaluation(UnknownContext context) throws CelEvaluationException {
115124
return evalInternal(context, Optional.empty(), Optional.empty());

runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ java_library(
2424
":eval_create_list",
2525
":eval_create_map",
2626
":eval_create_struct",
27+
":eval_exhaustive_and",
28+
":eval_exhaustive_conditional",
29+
":eval_exhaustive_or",
2730
":eval_fold",
2831
":eval_late_bound_call",
2932
":eval_optional_or",
@@ -446,6 +449,7 @@ java_library(
446449
"//runtime:evaluation_listener",
447450
"//runtime:function_resolver",
448451
"//runtime:interpretable",
452+
"//runtime:interpreter_util",
449453
"//runtime:partial_vars",
450454
"//runtime:resolved_overload",
451455
"@maven//:com_google_errorprone_error_prone_annotations",
@@ -498,6 +502,48 @@ java_library(
498502
],
499503
)
500504

505+
java_library(
506+
name = "eval_exhaustive_and",
507+
srcs = ["EvalExhaustiveAnd.java"],
508+
deps = [
509+
":eval_helpers",
510+
":planned_interpretable",
511+
"//common/ast",
512+
"//common/values",
513+
"//runtime:accumulated_unknowns",
514+
"//runtime:interpretable",
515+
"@maven//:com_google_errorprone_error_prone_annotations",
516+
],
517+
)
518+
519+
java_library(
520+
name = "eval_exhaustive_or",
521+
srcs = ["EvalExhaustiveOr.java"],
522+
deps = [
523+
":eval_helpers",
524+
":planned_interpretable",
525+
"//common/ast",
526+
"//common/values",
527+
"//runtime:accumulated_unknowns",
528+
"//runtime:interpretable",
529+
"@maven//:com_google_errorprone_error_prone_annotations",
530+
],
531+
)
532+
533+
java_library(
534+
name = "eval_exhaustive_conditional",
535+
srcs = ["EvalExhaustiveConditional.java"],
536+
deps = [
537+
":eval_helpers",
538+
":planned_interpretable",
539+
"//common/ast",
540+
"//runtime:accumulated_unknowns",
541+
"//runtime:evaluation_exception",
542+
"//runtime:interpretable",
543+
"@maven//:com_google_errorprone_error_prone_annotations",
544+
],
545+
)
546+
501547
java_library(
502548
name = "eval_block",
503549
srcs = ["EvalBlock.java"],
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.runtime.planner;
16+
17+
import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly;
18+
19+
import com.google.errorprone.annotations.Immutable;
20+
import dev.cel.common.ast.CelExpr;
21+
import dev.cel.common.values.ErrorValue;
22+
import dev.cel.runtime.AccumulatedUnknowns;
23+
import dev.cel.runtime.GlobalResolver;
24+
25+
/**
26+
* Implementation of logical AND with exhaustive evaluation (non-short-circuiting).
27+
*
28+
* <p>It evaluates all arguments, but prioritizes a false result over unknowns and errors to
29+
* maintain semantic consistency with short-circuiting evaluation.
30+
*/
31+
@Immutable
32+
final class EvalExhaustiveAnd extends PlannedInterpretable {
33+
34+
@SuppressWarnings("Immutable")
35+
private final PlannedInterpretable[] args;
36+
37+
@Override
38+
Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) {
39+
AccumulatedUnknowns accumulatedUnknowns = null;
40+
ErrorValue errorValue = null;
41+
boolean hasFalse = false;
42+
43+
for (PlannedInterpretable arg : args) {
44+
Object argVal = evalNonstrictly(arg, resolver, frame);
45+
if (argVal instanceof Boolean) {
46+
if (!((boolean) argVal)) {
47+
hasFalse = true;
48+
}
49+
}
50+
51+
// If we already encountered a false, we do not need to accumulate unknowns or errors
52+
// from subsequent terms because the final result will be false anyway.
53+
if (hasFalse) {
54+
continue;
55+
}
56+
57+
if (argVal instanceof AccumulatedUnknowns) {
58+
accumulatedUnknowns =
59+
accumulatedUnknowns == null
60+
? (AccumulatedUnknowns) argVal
61+
: accumulatedUnknowns.merge((AccumulatedUnknowns) argVal);
62+
} else if (argVal instanceof ErrorValue) {
63+
if (errorValue == null) {
64+
errorValue = (ErrorValue) argVal;
65+
}
66+
}
67+
}
68+
69+
if (hasFalse) {
70+
return false;
71+
}
72+
73+
if (accumulatedUnknowns != null) {
74+
return accumulatedUnknowns;
75+
}
76+
77+
if (errorValue != null) {
78+
return errorValue;
79+
}
80+
81+
return true;
82+
}
83+
84+
static EvalExhaustiveAnd create(CelExpr expr, PlannedInterpretable[] args) {
85+
return new EvalExhaustiveAnd(expr, args);
86+
}
87+
88+
private EvalExhaustiveAnd(CelExpr expr, PlannedInterpretable[] args) {
89+
super(expr);
90+
this.args = args;
91+
}
92+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.runtime.planner;
16+
17+
import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly;
18+
19+
import com.google.errorprone.annotations.Immutable;
20+
import dev.cel.common.ast.CelExpr;
21+
import dev.cel.runtime.AccumulatedUnknowns;
22+
import dev.cel.runtime.CelEvaluationException;
23+
import dev.cel.runtime.GlobalResolver;
24+
25+
/**
26+
* Implementation of conditional operator (ternary) with exhaustive evaluation
27+
* (non-short-circuiting).
28+
*
29+
* <p>It evaluates all three arguments (condition, truthy, and falsy branches) but returns the
30+
* result based on the condition, maintaining semantic consistency with short-circuiting evaluation.
31+
*/
32+
@Immutable
33+
final class EvalExhaustiveConditional extends PlannedInterpretable {
34+
35+
@SuppressWarnings("Immutable")
36+
private final PlannedInterpretable[] args;
37+
38+
@Override
39+
Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException {
40+
PlannedInterpretable condition = args[0];
41+
PlannedInterpretable truthy = args[1];
42+
PlannedInterpretable falsy = args[2];
43+
44+
Object condResult = condition.eval(resolver, frame);
45+
Object truthyVal = evalNonstrictly(truthy, resolver, frame);
46+
Object falsyVal = evalNonstrictly(falsy, resolver, frame);
47+
48+
if (condResult instanceof AccumulatedUnknowns) {
49+
return condResult;
50+
}
51+
52+
if (!(condResult instanceof Boolean)) {
53+
throw new IllegalArgumentException(
54+
String.format("Expected boolean value, found :%s", condResult));
55+
}
56+
57+
return (boolean) condResult ? truthyVal : falsyVal;
58+
}
59+
60+
static EvalExhaustiveConditional create(CelExpr expr, PlannedInterpretable[] args) {
61+
return new EvalExhaustiveConditional(expr, args);
62+
}
63+
64+
private EvalExhaustiveConditional(CelExpr expr, PlannedInterpretable[] args) {
65+
super(expr);
66+
this.args = args;
67+
}
68+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.runtime.planner;
16+
17+
import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly;
18+
19+
import com.google.errorprone.annotations.Immutable;
20+
import dev.cel.common.ast.CelExpr;
21+
import dev.cel.common.values.ErrorValue;
22+
import dev.cel.runtime.AccumulatedUnknowns;
23+
import dev.cel.runtime.GlobalResolver;
24+
25+
/**
26+
* Implementation of logical OR with exhaustive evaluation (non-short-circuiting).
27+
*
28+
* <p>It evaluates all arguments, but prioritizes a true result over unknowns and errors to maintain
29+
* semantic consistency with short-circuiting evaluation.
30+
*/
31+
@Immutable
32+
final class EvalExhaustiveOr extends PlannedInterpretable {
33+
34+
@SuppressWarnings("Immutable")
35+
private final PlannedInterpretable[] args;
36+
37+
@Override
38+
Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) {
39+
AccumulatedUnknowns accumulatedUnknowns = null;
40+
ErrorValue errorValue = null;
41+
boolean hasTrue = false;
42+
43+
for (PlannedInterpretable arg : args) {
44+
Object argVal = evalNonstrictly(arg, resolver, frame);
45+
if (argVal instanceof Boolean) {
46+
if ((boolean) argVal) {
47+
hasTrue = true;
48+
}
49+
}
50+
51+
// If we already encountered a true, we do not need to accumulate unknowns or errors
52+
// from subsequent terms because the final result will be true anyway.
53+
if (hasTrue) {
54+
continue;
55+
}
56+
57+
if (argVal instanceof AccumulatedUnknowns) {
58+
accumulatedUnknowns =
59+
accumulatedUnknowns == null
60+
? (AccumulatedUnknowns) argVal
61+
: accumulatedUnknowns.merge((AccumulatedUnknowns) argVal);
62+
} else if (argVal instanceof ErrorValue) {
63+
if (errorValue == null) {
64+
errorValue = (ErrorValue) argVal;
65+
}
66+
}
67+
}
68+
69+
if (hasTrue) {
70+
return true;
71+
}
72+
73+
if (accumulatedUnknowns != null) {
74+
return accumulatedUnknowns;
75+
}
76+
77+
if (errorValue != null) {
78+
return errorValue;
79+
}
80+
81+
return false;
82+
}
83+
84+
static EvalExhaustiveOr create(CelExpr expr, PlannedInterpretable[] args) {
85+
return new EvalExhaustiveOr(expr, args);
86+
}
87+
88+
private EvalExhaustiveOr(CelExpr expr, PlannedInterpretable[] args) {
89+
super(expr);
90+
this.args = args;
91+
}
92+
}

runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import dev.cel.runtime.CelEvaluationException;
2020
import dev.cel.runtime.CelEvaluationListener;
2121
import dev.cel.runtime.GlobalResolver;
22+
import dev.cel.runtime.InterpreterUtil;
2223

2324
@Immutable
2425
abstract class PlannedInterpretable {
@@ -29,7 +30,7 @@ final Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvalu
2930
Object result = evalInternal(resolver, frame);
3031
CelEvaluationListener listener = frame.getListener();
3132
if (listener != null) {
32-
listener.callback(expr, result);
33+
listener.callback(expr, InterpreterUtil.maybeAdaptToCelUnknownSet(result));
3334
}
3435
return result;
3536
}

0 commit comments

Comments
 (0)