Skip to content

Commit ddfcbc8

Browse files
l46kokcopybara-github
authored andcommitted
Fix filter/map macro to be linear in time and space complexity
PiperOrigin-RevId: 781245388
1 parent 97667f5 commit ddfcbc8

File tree

4 files changed

+151
-1
lines changed

4 files changed

+151
-1
lines changed

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ java_library(
272272
exports = [":base"],
273273
deps = [
274274
":base",
275+
":concatenated_list_view",
275276
":dispatcher",
276277
":evaluation_exception",
277278
":evaluation_exception_builder",
@@ -306,6 +307,7 @@ cel_android_library(
306307
visibility = ["//visibility:private"],
307308
deps = [
308309
":base_android",
310+
":concatenated_list_view",
309311
":dispatcher_android",
310312
":evaluation_exception",
311313
":evaluation_exception_builder",
@@ -398,6 +400,7 @@ cel_android_library(
398400
tags = [
399401
],
400402
deps = [
403+
":concatenated_list_view",
401404
"//common:error_codes",
402405
"//common:options",
403406
"//common:runtime_exception",
@@ -419,6 +422,7 @@ java_library(
419422
tags = [
420423
],
421424
deps = [
425+
":concatenated_list_view",
422426
"//common:error_codes",
423427
"//common:options",
424428
"//common:runtime_exception",
@@ -1094,3 +1098,10 @@ cel_android_library(
10941098
"@maven_android//:com_google_guava_guava",
10951099
],
10961100
)
1101+
1102+
java_library(
1103+
name = "concatenated_list_view",
1104+
srcs = ["ConcatenatedListView.java"],
1105+
# used_by_android
1106+
visibility = ["//visibility:private"],
1107+
)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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 aj
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;
16+
17+
import java.util.AbstractList;
18+
import java.util.ArrayList;
19+
import java.util.Collection;
20+
import java.util.Iterator;
21+
import java.util.List;
22+
import java.util.NoSuchElementException;
23+
24+
/**
25+
* A custom list view implementation that allows O(1) concatenation of two lists. Its primary
26+
* purpose is to facilitate efficient accumulation of lists for later materialization. (ex:
27+
* comprehensions that dispatch `add_list` to concat N lists together).
28+
*
29+
* <p>This does not support any of the standard list operations from {@link java.util.List}.
30+
*/
31+
final class ConcatenatedListView<E> extends AbstractList<E> {
32+
private final List<List<? extends E>> sourceLists;
33+
private int totalSize = 0;
34+
35+
ConcatenatedListView() {
36+
this.sourceLists = new ArrayList<>();
37+
}
38+
39+
ConcatenatedListView(Collection<? extends E> collection) {
40+
this();
41+
addAll(collection);
42+
}
43+
44+
@Override
45+
public boolean addAll(Collection<? extends E> collection) {
46+
if (!(collection instanceof List)) {
47+
// size() is O(1) iff it's a list
48+
throw new IllegalStateException("addAll must be called with lists, not collections");
49+
}
50+
51+
sourceLists.add((List<? extends E>) collection);
52+
totalSize += collection.size();
53+
return true;
54+
}
55+
56+
@Override
57+
public E get(int index) {
58+
throw new UnsupportedOperationException("get method not supported.");
59+
}
60+
61+
@Override
62+
public int size() {
63+
return totalSize;
64+
}
65+
66+
@Override
67+
public Iterator<E> iterator() {
68+
return new ConcatenatingIterator();
69+
}
70+
71+
/** Custom iterator to provide a flat view of all concatenated collections */
72+
private class ConcatenatingIterator implements Iterator<E> {
73+
private int index = 0;
74+
private Iterator<? extends E> iterator = null;
75+
76+
@Override
77+
public boolean hasNext() {
78+
while (iterator == null || !iterator.hasNext()) {
79+
if (index < sourceLists.size()) {
80+
iterator = sourceLists.get(index).iterator();
81+
index++;
82+
} else {
83+
return false;
84+
}
85+
}
86+
return true;
87+
}
88+
89+
@Override
90+
public E next() {
91+
if (!hasNext()) {
92+
throw new NoSuchElementException();
93+
}
94+
return iterator.next();
95+
}
96+
97+
@Override
98+
public void remove() {
99+
throw new UnsupportedOperationException("remove method not supported");
100+
}
101+
}
102+
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ public Object evalTrackingUnknowns(
173173
throws CelEvaluationException {
174174
ExecutionFrame frame = newExecutionFrame(resolver, functionResolver, listener);
175175
IntermediateResult internalResult = evalInternal(frame, ast.getExpr());
176+
176177
return internalResult.value();
177178
}
178179

@@ -896,6 +897,32 @@ private IntermediateResult evalNonstrictly(ExecutionFrame frame, CelExpr expr) {
896897
}
897898
}
898899

900+
@SuppressWarnings("unchecked") // All type-erased elements are object compatible
901+
private IntermediateResult maybeAdaptToListView(IntermediateResult accuValue) {
902+
// ListView uses a mutable reference internally. Macros such as `filter` uses conditionals
903+
// under the hood. In situations where short circuiting is disabled, we don't want to evaluate
904+
// both LHS and RHS, as evaluating LHS can mutate the accu value, which also affects RHS.
905+
if (!(accuValue.value() instanceof List) || !celOptions.enableShortCircuiting()) {
906+
return accuValue;
907+
}
908+
909+
ConcatenatedListView<Object> lv =
910+
new ConcatenatedListView<>((List<Object>) accuValue.value());
911+
return IntermediateResult.create(lv);
912+
}
913+
914+
@SuppressWarnings("unchecked") // All type-erased elements are object compatible
915+
private IntermediateResult maybeAdaptViewToList(IntermediateResult accuValue) {
916+
if ((accuValue.value() instanceof ConcatenatedListView)) {
917+
// Materialize view back into a list to facilitate O(1) lookups.
918+
List<Object> copiedList = new ArrayList<>((List<Object>) accuValue.value());
919+
920+
accuValue = IntermediateResult.create(copiedList);
921+
}
922+
923+
return accuValue;
924+
}
925+
899926
@SuppressWarnings("unchecked")
900927
private IntermediateResult evalComprehension(
901928
ExecutionFrame frame, CelExpr unusedExpr, CelComprehension compre)
@@ -924,6 +951,9 @@ private IntermediateResult evalComprehension(
924951
accuValue = IntermediateResult.create(new LazyExpression(compre.accuInit()));
925952
} else {
926953
accuValue = evalNonstrictly(frame, compre.accuInit());
954+
// This ensures macros such as filter/map that uses "add_list" functions under the hood
955+
// remain linear in time complexity
956+
accuValue = maybeAdaptToListView(accuValue);
927957
}
928958
int i = 0;
929959
for (Object elem : iterRange) {
@@ -950,6 +980,8 @@ private IntermediateResult evalComprehension(
950980
frame.popScope();
951981
}
952982

983+
accuValue = maybeAdaptViewToList(accuValue);
984+
953985
frame.pushScope(Collections.singletonMap(accuVar, accuValue));
954986
IntermediateResult result;
955987
try {

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,12 @@ public static boolean matches(String string, String regexp, CelOptions celOption
9191

9292
/** Concatenates two lists into a new list. */
9393
public static <E> List<E> concat(List<E> first, List<E> second) {
94-
// TODO: return a view instead of an actual copy.
94+
if (first instanceof ConcatenatedListView) {
95+
// Comprehensions use a more efficient list view for performing O(1) concatenation
96+
first.addAll(second);
97+
return first;
98+
}
99+
95100
List<E> result = new ArrayList<>(first.size() + second.size());
96101
result.addAll(first);
97102
result.addAll(second);

0 commit comments

Comments
 (0)