Skip to content

Commit a11aa1e

Browse files
l46kokcopybara-github
authored andcommitted
Plan Comprehensions
PiperOrigin-RevId: 848026572
1 parent a0de42c commit a11aa1e

7 files changed

Lines changed: 299 additions & 14 deletions

File tree

runtime/BUILD.bazel

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,11 @@ java_library(
255255
visibility = ["//:internal"],
256256
exports = ["//runtime/src/main/java/dev/cel/runtime:metadata"],
257257
)
258+
259+
java_library(
260+
name = "concatenated_list_view",
261+
visibility = ["//:internal"],
262+
exports = [
263+
"//runtime/src/main/java/dev/cel/runtime:concatenated_list_view",
264+
],
265+
)

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1142,7 +1142,9 @@ java_library(
11421142
name = "concatenated_list_view",
11431143
srcs = ["ConcatenatedListView.java"],
11441144
# used_by_android
1145-
visibility = ["//visibility:private"],
1145+
tags = [
1146+
],
1147+
deps = ["//common/annotations"],
11461148
)
11471149

11481150
java_library(

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
5-
// You may obtain a copy of the License aj
5+
// You may obtain a copy of the License at
66
//
77
// https://www.apache.org/licenses/LICENSE-2.0
88
//
@@ -14,6 +14,7 @@
1414

1515
package dev.cel.runtime;
1616

17+
import dev.cel.common.annotations.Internal;
1718
import java.util.AbstractList;
1819
import java.util.ArrayList;
1920
import java.util.Collection;
@@ -27,16 +28,20 @@
2728
* comprehensions that dispatch `add_list` to concat N lists together).
2829
*
2930
* <p>This does not support any of the standard list operations from {@link java.util.List}.
31+
*
32+
33+
* <p>CEL Library Internals. Do Not Use.
3034
*/
31-
final class ConcatenatedListView<E> extends AbstractList<E> {
35+
@Internal
36+
public final class ConcatenatedListView<E> extends AbstractList<E> {
3237
private final List<List<? extends E>> sourceLists;
3338
private int totalSize = 0;
3439

3540
ConcatenatedListView() {
3641
this.sourceLists = new ArrayList<>();
3742
}
3843

39-
ConcatenatedListView(Collection<? extends E> collection) {
44+
public ConcatenatedListView(Collection<? extends E> collection) {
4045
this();
4146
addAll(collection);
4247
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ java_library(
2222
":eval_create_list",
2323
":eval_create_map",
2424
":eval_create_struct",
25+
":eval_fold",
2526
":eval_or",
2627
":eval_test_only",
2728
":eval_unary",
@@ -309,6 +310,22 @@ java_library(
309310
],
310311
)
311312

313+
java_library(
314+
name = "eval_fold",
315+
srcs = ["EvalFold.java"],
316+
deps = [
317+
":planned_interpretable",
318+
"//runtime:concatenated_list_view",
319+
"//runtime:evaluation_exception",
320+
"//runtime:evaluation_listener",
321+
"//runtime:function_resolver",
322+
"//runtime:interpretable",
323+
"@maven//:com_google_errorprone_error_prone_annotations",
324+
"@maven//:com_google_guava_guava",
325+
"@maven//:org_jspecify_jspecify",
326+
],
327+
)
328+
312329
java_library(
313330
name = "eval_helpers",
314331
srcs = ["EvalHelpers.java"],
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
// Copyright 2025 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 com.google.common.collect.ImmutableList;
18+
import com.google.errorprone.annotations.Immutable;
19+
import dev.cel.runtime.CelEvaluationException;
20+
import dev.cel.runtime.CelEvaluationListener;
21+
import dev.cel.runtime.CelFunctionResolver;
22+
import dev.cel.runtime.ConcatenatedListView;
23+
import dev.cel.runtime.GlobalResolver;
24+
import java.util.Collection;
25+
import java.util.Map;
26+
import org.jspecify.annotations.Nullable;
27+
28+
@Immutable
29+
final class EvalFold extends PlannedInterpretable {
30+
31+
private final String accuVar;
32+
private final PlannedInterpretable accuInit;
33+
private final String iterVar;
34+
private final String iterVar2;
35+
private final PlannedInterpretable iterRange;
36+
private final PlannedInterpretable condition;
37+
private final PlannedInterpretable loopStep;
38+
private final PlannedInterpretable result;
39+
40+
static EvalFold create(
41+
long exprId,
42+
String accuVar,
43+
PlannedInterpretable accuInit,
44+
String iterVar,
45+
String iterVar2,
46+
PlannedInterpretable iterRange,
47+
PlannedInterpretable loopCondition,
48+
PlannedInterpretable loopStep,
49+
PlannedInterpretable result) {
50+
return new EvalFold(
51+
exprId, accuVar, accuInit, iterVar, iterVar2, iterRange, loopCondition, loopStep, result);
52+
}
53+
54+
private EvalFold(
55+
long exprId,
56+
String accuVar,
57+
PlannedInterpretable accuInit,
58+
String iterVar,
59+
String iterVar2,
60+
PlannedInterpretable iterRange,
61+
PlannedInterpretable condition,
62+
PlannedInterpretable loopStep,
63+
PlannedInterpretable result) {
64+
super(exprId);
65+
this.accuVar = accuVar;
66+
this.accuInit = accuInit;
67+
this.iterVar = iterVar;
68+
this.iterVar2 = iterVar2;
69+
this.iterRange = iterRange;
70+
this.condition = condition;
71+
this.loopStep = loopStep;
72+
this.result = result;
73+
}
74+
75+
@Override
76+
public Object eval(GlobalResolver resolver) throws CelEvaluationException {
77+
Object iterRangeRaw = iterRange.eval(resolver);
78+
Folder folder = new Folder(resolver, accuVar, iterVar, iterVar2);
79+
folder.accuVal = maybeWrapAccumulator(accuInit.eval(folder));
80+
81+
Object result;
82+
if (iterRangeRaw instanceof Map) {
83+
result = evalMap((Map<?, ?>) iterRangeRaw, folder);
84+
} else if (iterRangeRaw instanceof Collection) {
85+
result = evalList((Collection<?>) iterRangeRaw, folder);
86+
} else {
87+
throw new IllegalArgumentException("Unexpected iter_range type: " + iterRangeRaw.getClass());
88+
}
89+
90+
return maybeUnwrapAccumulator(result);
91+
}
92+
93+
@Override
94+
public Object eval(GlobalResolver resolver, CelEvaluationListener listener) {
95+
// TODO: Implement support
96+
throw new UnsupportedOperationException("Not yet supported");
97+
}
98+
99+
@Override
100+
public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) {
101+
// TODO: Implement support
102+
throw new UnsupportedOperationException("Not yet supported");
103+
}
104+
105+
@Override
106+
public Object eval(
107+
GlobalResolver resolver,
108+
CelFunctionResolver lateBoundFunctionResolver,
109+
CelEvaluationListener listener) {
110+
// TODO: Implement support
111+
throw new UnsupportedOperationException("Not yet supported");
112+
}
113+
114+
private Object evalMap(Map<?, ?> iterRange, Folder folder) throws CelEvaluationException {
115+
for (Map.Entry<?, ?> entry : iterRange.entrySet()) {
116+
folder.iterVarVal = entry.getKey();
117+
if (!iterVar2.isEmpty()) {
118+
folder.iterVar2Val = entry.getValue();
119+
}
120+
121+
boolean cond = (boolean) condition.eval(folder);
122+
if (!cond) {
123+
return result.eval(folder);
124+
}
125+
126+
// TODO: Introduce comprehension safety controls, such as iteration limit.
127+
folder.accuVal = loopStep.eval(folder);
128+
}
129+
return result.eval(folder);
130+
}
131+
132+
private Object evalList(Collection<?> iterRange, Folder folder) throws CelEvaluationException {
133+
int index = 0;
134+
for (Object item : iterRange) {
135+
if (iterVar2.isEmpty()) {
136+
folder.iterVarVal = item;
137+
} else {
138+
folder.iterVarVal = (long) index;
139+
folder.iterVar2Val = item;
140+
}
141+
142+
boolean cond = (boolean) condition.eval(folder);
143+
if (!cond) {
144+
return result.eval(folder);
145+
}
146+
147+
folder.accuVal = loopStep.eval(folder);
148+
index++;
149+
}
150+
return result.eval(folder);
151+
}
152+
153+
private static Object maybeWrapAccumulator(Object val) {
154+
if (val instanceof Collection) {
155+
return new ConcatenatedListView<>((Collection<?>) val);
156+
}
157+
// TODO: Introduce mutable map support (for comp v2)
158+
return val;
159+
}
160+
161+
private static Object maybeUnwrapAccumulator(Object val) {
162+
if (val instanceof ConcatenatedListView) {
163+
return ImmutableList.copyOf((ConcatenatedListView<?>) val);
164+
}
165+
166+
// TODO: Introduce mutable map support (for comp v2)
167+
return val;
168+
}
169+
170+
private static class Folder implements GlobalResolver {
171+
private final GlobalResolver resolver;
172+
private final String accuVar;
173+
private final String iterVar;
174+
private final String iterVar2;
175+
176+
private Object iterVarVal;
177+
private Object iterVar2Val;
178+
private Object accuVal;
179+
180+
private Folder(GlobalResolver resolver, String accuVar, String iterVar, String iterVar2) {
181+
this.resolver = resolver;
182+
this.accuVar = accuVar;
183+
this.iterVar = iterVar;
184+
this.iterVar2 = iterVar2;
185+
}
186+
187+
@Override
188+
public @Nullable Object resolve(String name) {
189+
if (name.equals(accuVar)) {
190+
return accuVal;
191+
}
192+
193+
if (name.equals(iterVar)) {
194+
return this.iterVarVal;
195+
}
196+
197+
if (name.equals(iterVar2)) {
198+
return this.iterVar2Val;
199+
}
200+
201+
return resolver.resolve(name);
202+
}
203+
}
204+
}

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import dev.cel.common.ast.CelConstant;
2727
import dev.cel.common.ast.CelExpr;
2828
import dev.cel.common.ast.CelExpr.CelCall;
29+
import dev.cel.common.ast.CelExpr.CelComprehension;
2930
import dev.cel.common.ast.CelExpr.CelList;
3031
import dev.cel.common.ast.CelExpr.CelMap;
3132
import dev.cel.common.ast.CelExpr.CelSelect;
@@ -94,10 +95,12 @@ private PlannedInterpretable plan(CelExpr celExpr, PlannerContext ctx) {
9495
return planCreateStruct(celExpr, ctx);
9596
case MAP:
9697
return planCreateMap(celExpr, ctx);
98+
case COMPREHENSION:
99+
return planComprehension(celExpr, ctx);
97100
case NOT_SET:
98101
throw new UnsupportedOperationException("Unsupported kind: " + celExpr.getKind());
99102
default:
100-
throw new IllegalArgumentException("Not yet implemented kind: " + celExpr.getKind());
103+
throw new UnsupportedOperationException("Unexpected kind: " + celExpr.getKind());
101104
}
102105
}
103106

@@ -280,6 +283,27 @@ private PlannedInterpretable planCreateMap(CelExpr celExpr, PlannerContext ctx)
280283
return EvalCreateMap.create(celExpr.id(), keys, values);
281284
}
282285

286+
private PlannedInterpretable planComprehension(CelExpr expr, PlannerContext ctx) {
287+
CelComprehension comprehension = expr.comprehension();
288+
289+
PlannedInterpretable accuInit = plan(comprehension.accuInit(), ctx);
290+
PlannedInterpretable iterRange = plan(comprehension.iterRange(), ctx);
291+
PlannedInterpretable loopCondition = plan(comprehension.loopCondition(), ctx);
292+
PlannedInterpretable loopStep = plan(comprehension.loopStep(), ctx);
293+
PlannedInterpretable result = plan(comprehension.result(), ctx);
294+
295+
return EvalFold.create(
296+
expr.id(),
297+
comprehension.accuVar(),
298+
accuInit,
299+
comprehension.iterVar(),
300+
comprehension.iterVar2(),
301+
iterRange,
302+
loopCondition,
303+
loopStep,
304+
result);
305+
}
306+
283307
/**
284308
* resolveFunction determines the call target, function name, and overload name (when unambiguous)
285309
* from the given call expr.

0 commit comments

Comments
 (0)