Skip to content

Commit 04b5547

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 04b5547

5 files changed

Lines changed: 166 additions & 1 deletion

File tree

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: 12 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

@@ -924,6 +925,11 @@ private IntermediateResult evalComprehension(
924925
accuValue = IntermediateResult.create(new LazyExpression(compre.accuInit()));
925926
} else {
926927
accuValue = evalNonstrictly(frame, compre.accuInit());
928+
if (accuValue.value() instanceof List) {
929+
ConcatenatedListView<Object> lv =
930+
new ConcatenatedListView<>((List<Object>) accuValue.value());
931+
accuValue = IntermediateResult.create(lv);
932+
}
927933
}
928934
int i = 0;
929935
for (Object elem : iterRange) {
@@ -950,6 +956,12 @@ private IntermediateResult evalComprehension(
950956
frame.popScope();
951957
}
952958

959+
// Materialize view back into an immutable list to facilitate O(1) lookups.
960+
if (accuValue.value() instanceof ConcatenatedListView) {
961+
accuValue =
962+
IntermediateResult.create(ImmutableList.copyOf((Collection<?>) accuValue.value()));
963+
}
964+
953965
frame.pushScope(Collections.singletonMap(accuVar, accuValue));
954966
IntermediateResult result;
955967
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);

runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414

1515
package dev.cel.runtime;
1616

17+
import static com.google.common.collect.ImmutableList.toImmutableList;
1718
import static com.google.common.truth.Truth.assertThat;
1819
import static org.junit.Assert.assertThrows;
1920

2021
import com.google.api.expr.v1alpha1.Constant;
2122
import com.google.api.expr.v1alpha1.Expr;
2223
import com.google.api.expr.v1alpha1.Type.PrimitiveType;
24+
import com.google.common.base.Stopwatch;
2325
import com.google.common.collect.ImmutableList;
2426
import com.google.common.collect.ImmutableMap;
2527
import com.google.protobuf.Any;
@@ -41,6 +43,7 @@
4143
import dev.cel.common.ast.CelExpr;
4244
import dev.cel.common.ast.CelExpr.ExprKind.Kind;
4345
import dev.cel.common.types.CelV1AlphaTypes;
46+
import dev.cel.common.types.ListType;
4447
import dev.cel.common.types.SimpleType;
4548
import dev.cel.common.types.StructTypeReference;
4649
import dev.cel.compiler.CelCompiler;
@@ -51,7 +54,9 @@
5154
import java.util.List;
5255
import java.util.Map;
5356
import java.util.Optional;
57+
import java.util.concurrent.TimeUnit;
5458
import java.util.concurrent.atomic.AtomicReference;
59+
import java.util.stream.LongStream;
5560
import org.junit.Test;
5661
import org.junit.runner.RunWith;
5762

@@ -685,4 +690,34 @@ public void standardEnvironmentDisabledForRuntime_throws() throws Exception {
685690
.hasMessageThat()
686691
.contains("No matching overload for function 'size'. Overload candidates: size_string");
687692
}
693+
694+
@Test
695+
@TestParameters("{run: 400000}")
696+
@TestParameters("{run: 800000}")
697+
@TestParameters("{run: 1200000}")
698+
@TestParameters("{run: 1600000}")
699+
@TestParameters("{run: 2000000}")
700+
@SuppressWarnings("unchecked") // Test only
701+
public void smokeTest(long run) throws Exception {
702+
CelCompiler celCompiler =
703+
CelCompilerFactory.standardCelCompilerBuilder()
704+
.addVar("list", ListType.create(SimpleType.INT))
705+
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
706+
.build();
707+
CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build();
708+
ImmutableList<Long> tooLongList =
709+
LongStream.range(0L, run + 1).boxed().collect(toImmutableList());
710+
CelAbstractSyntaxTree ast = celCompiler.compile("list.map(x, x)").getAst();
711+
712+
// Wake cache up
713+
List<Long> result =
714+
(List<Long>) celRuntime.createProgram(ast).eval(ImmutableMap.of("list", tooLongList));
715+
Stopwatch sw = Stopwatch.createStarted();
716+
result = (List<Long>) celRuntime.createProgram(ast).eval(ImmutableMap.of("list", tooLongList));
717+
sw.stop();
718+
719+
System.err.println("Elapsed: " + sw.elapsed(TimeUnit.MILLISECONDS));
720+
721+
assertThat(result).isEqualTo(tooLongList);
722+
}
688723
}

0 commit comments

Comments
 (0)