Skip to content

Commit 75dbc4f

Browse files
l46kokcopybara-github
authored andcommitted
Allow cel.bind to be lazily evaluated
PiperOrigin-RevId: 601239413
1 parent b7823ba commit 75dbc4f

5 files changed

Lines changed: 236 additions & 26 deletions

File tree

common/src/main/java/dev/cel/common/CelOptions.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ public abstract class CelOptions {
8989

9090
public abstract boolean enableCelValue();
9191

92+
public abstract boolean enableComprehensionLazyEval();
93+
9294
public abstract int comprehensionMaxIterations();
9395

9496
public abstract Builder toBuilder();
@@ -179,6 +181,7 @@ public static Builder newBuilder() {
179181
.resolveTypeDependencies(true)
180182
.enableUnknownTracking(false)
181183
.enableCelValue(false)
184+
.enableComprehensionLazyEval(false)
182185
.comprehensionMaxIterations(-1);
183186
}
184187

@@ -452,6 +455,12 @@ public abstract static class Builder {
452455
*/
453456
public abstract Builder comprehensionMaxIterations(int value);
454457

458+
/**
459+
* Enables certain comprehension expressions to be lazily evaluated where safe. Currently, this
460+
* only works for cel.bind.
461+
*/
462+
public abstract Builder enableComprehensionLazyEval(boolean value);
463+
455464
public abstract CelOptions build();
456465
}
457466
}

extensions/src/test/java/dev/cel/extensions/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ java_library(
1515
"//common/resources/testdata/proto2:messages_extensions_proto2_java_proto",
1616
"//common/resources/testdata/proto2:messages_proto2_java_proto",
1717
"//common/resources/testdata/proto2:test_all_types_java_proto",
18+
"//common/resources/testdata/proto3:test_all_types_java_proto",
1819
"//common/types",
1920
"//common/types:type_providers",
2021
"//compiler",

extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java

Lines changed: 153 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,27 @@
1717
import static com.google.common.truth.Truth.assertThat;
1818
import static org.junit.Assert.assertThrows;
1919

20+
import com.google.common.collect.ImmutableList;
21+
import com.google.common.collect.ImmutableMap;
22+
import com.google.testing.junit.testparameterinjector.TestParameter;
2023
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
2124
import com.google.testing.junit.testparameterinjector.TestParameters;
2225
import dev.cel.common.CelAbstractSyntaxTree;
2326
import dev.cel.common.CelFunctionDecl;
27+
import dev.cel.common.CelOptions;
2428
import dev.cel.common.CelOverloadDecl;
2529
import dev.cel.common.CelValidationException;
2630
import dev.cel.common.types.SimpleType;
31+
import dev.cel.common.types.StructTypeReference;
2732
import dev.cel.compiler.CelCompiler;
2833
import dev.cel.compiler.CelCompilerFactory;
2934
import dev.cel.parser.CelStandardMacro;
3035
import dev.cel.runtime.CelRuntime;
3136
import dev.cel.runtime.CelRuntime.CelFunctionBinding;
3237
import dev.cel.runtime.CelRuntimeFactory;
38+
import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes;
3339
import java.util.Arrays;
40+
import java.util.concurrent.atomic.AtomicInteger;
3441
import org.junit.Test;
3542
import org.junit.runner.RunWith;
3643

@@ -45,25 +52,46 @@ public final class CelBindingsExtensionsTest {
4552

4653
private static final CelRuntime RUNTIME = CelRuntimeFactory.standardCelRuntimeBuilder().build();
4754

55+
private enum BindingTestCase {
56+
BOOL_LITERAL("cel.bind(t, true, t)"),
57+
STRING_CONCAT("cel.bind(msg, \"hello\", msg + msg + msg) == \"hellohellohello\""),
58+
NESTED_BINDS("cel.bind(t1, true, cel.bind(t2, true, t1 && t2))"),
59+
NESTED_BINDS_SPECIFIER_ONLY(
60+
"cel.bind(x, cel.bind(x, \"a\", x + x), x + \":\" + x) == \"aa:aa\""),
61+
NESTED_BINDS_SPECIFIER_AND_VALUE(
62+
"cel.bind(x, cel.bind(x, \"a\", x + x), cel.bind(y, x + x, y + \":\" + y)) =="
63+
+ " \"aaaa:aaaa\""),
64+
BIND_WITH_EXISTS_TRUE(
65+
"cel.bind(valid_elems, [1, 2, 3], [3, 4, 5].exists(e, e in valid_elems))"),
66+
BIND_WITH_EXISTS_FALSE("cel.bind(valid_elems, [1, 2, 3], ![4, 5].exists(e, e in valid_elems))");
67+
68+
private final String source;
69+
70+
BindingTestCase(String source) {
71+
this.source = source;
72+
}
73+
}
74+
4875
@Test
49-
@TestParameters("{expr: 'cel.bind(t, true, t)', expectedResult: true}")
50-
@TestParameters(
51-
"{expr: 'cel.bind(msg, \"hello\", msg + msg + msg) == \"hellohellohello\"',"
52-
+ " expectedResult: true}")
53-
@TestParameters(
54-
"{expr: 'cel.bind(t1, true, cel.bind(t2, true, t1 && t2))', expectedResult: true}")
55-
@TestParameters(
56-
"{expr: 'cel.bind(valid_elems, [1, 2, 3], [3, 4, 5]"
57-
+ ".exists(e, e in valid_elems))', expectedResult: true}")
58-
@TestParameters(
59-
"{expr: 'cel.bind(valid_elems, [1, 2, 3], ![4, 5].exists(e, e in valid_elems))',"
60-
+ " expectedResult: true}")
61-
public void binding_success(String expr, boolean expectedResult) throws Exception {
62-
CelAbstractSyntaxTree ast = COMPILER.compile(expr).getAst();
76+
public void binding_success(@TestParameter BindingTestCase testCase) throws Exception {
77+
CelAbstractSyntaxTree ast = COMPILER.compile(testCase.source).getAst();
6378
CelRuntime.Program program = RUNTIME.createProgram(ast);
64-
Object evaluatedResult = program.eval();
79+
boolean evaluatedResult = (boolean) program.eval();
80+
81+
assertThat(evaluatedResult).isTrue();
82+
}
83+
84+
@Test
85+
public void binding_lazyEval_success(@TestParameter BindingTestCase testCase) throws Exception {
86+
CelAbstractSyntaxTree ast = COMPILER.compile(testCase.source).getAst();
87+
CelRuntime.Program program =
88+
CelRuntimeFactory.standardCelRuntimeBuilder()
89+
.setOptions(CelOptions.current().enableComprehensionLazyEval(true).build())
90+
.build()
91+
.createProgram(ast);
92+
boolean evaluatedResult = (boolean) program.eval();
6593

66-
assertThat(evaluatedResult).isEqualTo(expectedResult);
94+
assertThat(evaluatedResult).isTrue();
6795
}
6896

6997
@Test
@@ -105,4 +133,113 @@ public void binding_throwsCompilationException(String expr) throws Exception {
105133

106134
assertThat(e).hasMessageThat().contains("cel.bind() variable name must be a simple identifier");
107135
}
136+
137+
@Test
138+
@SuppressWarnings("Immutable") // Test only
139+
public void lazyBinding_bindingVarNeverReferenced() throws Exception {
140+
CelCompiler celCompiler =
141+
CelCompilerFactory.standardCelCompilerBuilder()
142+
.setStandardMacros(CelStandardMacro.HAS)
143+
.addMessageTypes(TestAllTypes.getDescriptor())
144+
.addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName()))
145+
.addLibraries(CelExtensions.bindings())
146+
.addFunctionDeclarations(
147+
CelFunctionDecl.newFunctionDeclaration(
148+
"get_true",
149+
CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL)))
150+
.build();
151+
AtomicInteger invocation = new AtomicInteger();
152+
CelRuntime celRuntime =
153+
CelRuntimeFactory.standardCelRuntimeBuilder()
154+
.addMessageTypes(TestAllTypes.getDescriptor())
155+
.setOptions(CelOptions.current().enableComprehensionLazyEval(true).build())
156+
.addFunctionBindings(
157+
CelFunctionBinding.from(
158+
"get_true_overload",
159+
ImmutableList.of(),
160+
arg -> {
161+
invocation.getAndIncrement();
162+
return true;
163+
}))
164+
.build();
165+
CelAbstractSyntaxTree ast =
166+
celCompiler.compile("cel.bind(t, get_true(), has(msg.single_int64) ? t : false)").getAst();
167+
168+
boolean result =
169+
(boolean)
170+
celRuntime
171+
.createProgram(ast)
172+
.eval(ImmutableMap.of("msg", TestAllTypes.getDefaultInstance()));
173+
174+
assertThat(result).isFalse();
175+
assertThat(invocation.get()).isEqualTo(0);
176+
}
177+
178+
@Test
179+
@SuppressWarnings("Immutable") // Test only
180+
public void lazyBinding_accuInitEvaluatedOnce() throws Exception {
181+
CelCompiler celCompiler =
182+
CelCompilerFactory.standardCelCompilerBuilder()
183+
.addLibraries(CelExtensions.bindings())
184+
.addFunctionDeclarations(
185+
CelFunctionDecl.newFunctionDeclaration(
186+
"get_true",
187+
CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL)))
188+
.build();
189+
AtomicInteger invocation = new AtomicInteger();
190+
CelRuntime celRuntime =
191+
CelRuntimeFactory.standardCelRuntimeBuilder()
192+
.setOptions(CelOptions.current().enableComprehensionLazyEval(true).build())
193+
.addFunctionBindings(
194+
CelFunctionBinding.from(
195+
"get_true_overload",
196+
ImmutableList.of(),
197+
arg -> {
198+
invocation.getAndIncrement();
199+
return true;
200+
}))
201+
.build();
202+
CelAbstractSyntaxTree ast =
203+
celCompiler.compile("cel.bind(t, get_true(), t && t && t && t)").getAst();
204+
205+
boolean result = (boolean) celRuntime.createProgram(ast).eval();
206+
207+
assertThat(result).isTrue();
208+
assertThat(invocation.get()).isEqualTo(1);
209+
}
210+
211+
@Test
212+
@SuppressWarnings("Immutable") // Test only
213+
public void lazyBinding_withNestedBinds() throws Exception {
214+
CelCompiler celCompiler =
215+
CelCompilerFactory.standardCelCompilerBuilder()
216+
.addLibraries(CelExtensions.bindings())
217+
.addFunctionDeclarations(
218+
CelFunctionDecl.newFunctionDeclaration(
219+
"get_true",
220+
CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL)))
221+
.build();
222+
AtomicInteger invocation = new AtomicInteger();
223+
CelRuntime celRuntime =
224+
CelRuntimeFactory.standardCelRuntimeBuilder()
225+
.setOptions(CelOptions.current().enableComprehensionLazyEval(true).build())
226+
.addFunctionBindings(
227+
CelFunctionBinding.from(
228+
"get_true_overload",
229+
ImmutableList.of(),
230+
arg -> {
231+
invocation.getAndIncrement();
232+
return true;
233+
}))
234+
.build();
235+
CelAbstractSyntaxTree ast =
236+
celCompiler
237+
.compile("cel.bind(t1, get_true(), cel.bind(t2, get_true(), t1 && t2 && t1 && t2))")
238+
.getAst();
239+
240+
boolean result = (boolean) celRuntime.createProgram(ast).eval();
241+
242+
assertThat(result).isTrue();
243+
assertThat(invocation.get()).isEqualTo(2);
244+
}
108245
}

optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,11 @@ private static CelBuilder newCelBuilder() {
8686
.setContainer("dev.cel.testing.testdata.proto3")
8787
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
8888
.setOptions(
89-
CelOptions.current().enableTimestampEpoch(true).populateMacroCalls(true).build())
89+
CelOptions.current()
90+
.enableTimestampEpoch(true)
91+
.enableComprehensionLazyEval(true)
92+
.populateMacroCalls(true)
93+
.build())
9094
.addCompilerLibraries(CelOptionalLibrary.INSTANCE, CelExtensions.bindings())
9195
.addRuntimeLibraries(CelOptionalLibrary.INSTANCE)
9296
.addFunctionDeclarations(

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

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -275,11 +275,27 @@ private IntermediateResult resolveIdent(ExecutionFrame frame, CelExpr expr, Stri
275275
return IntermediateResult.create(typeValue);
276276
}
277277

278+
IntermediateResult cachedResult = frame.lookupLazilyEvaluatedResult(name).orElse(null);
279+
if (cachedResult != null) {
280+
return cachedResult;
281+
}
282+
278283
IntermediateResult rawResult = frame.resolveSimpleName(name, expr.id());
284+
Object value = rawResult.value();
285+
boolean isLazyExpression = value instanceof LazyExpression;
286+
if (isLazyExpression) {
287+
value = evalInternal(frame, ((LazyExpression) value).celExpr).value();
288+
}
279289

280290
// Value resolved from Binding, it could be Message, PartialMessage or unbound(null)
281-
Object value = InterpreterUtil.strict(typeProvider.adapt(rawResult.value()));
282-
return IntermediateResult.create(rawResult.attribute(), value);
291+
value = InterpreterUtil.strict(typeProvider.adapt(value));
292+
IntermediateResult result = IntermediateResult.create(rawResult.attribute(), value);
293+
294+
if (isLazyExpression) {
295+
frame.cacheLazilyEvaluatedResult(name, result);
296+
}
297+
298+
return result;
283299
}
284300

285301
private IntermediateResult evalSelect(ExecutionFrame frame, CelExpr expr, CelSelect selectExpr)
@@ -404,7 +420,7 @@ private IntermediateResult evalCall(ExecutionFrame frame, CelExpr expr, CelCall
404420
return IntermediateResult.create(attr, unknowns.get());
405421
}
406422

407-
Object[] argArray = Arrays.stream(argResults).map(v -> v.value()).toArray();
423+
Object[] argArray = Arrays.stream(argResults).map(IntermediateResult::value).toArray();
408424

409425
return IntermediateResult.create(
410426
attr,
@@ -754,11 +770,12 @@ private IntermediateResult evalStruct(
754770
fields.put(entry.fieldKey(), value);
755771
}
756772

757-
Optional<Object> unknowns = argChecker.maybeUnknowns();
758-
if (unknowns.isPresent()) {
759-
return IntermediateResult.create(unknowns.get());
760-
}
761-
return IntermediateResult.create(typeProvider.createMessage(reference.name(), fields));
773+
return argChecker
774+
.maybeUnknowns()
775+
.map(IntermediateResult::create)
776+
.orElseGet(
777+
() ->
778+
IntermediateResult.create(typeProvider.createMessage(reference.name(), fields)));
762779
}
763780

764781
// Evaluates the expression and returns a value-or-throwable.
@@ -796,7 +813,12 @@ private IntermediateResult evalComprehension(
796813
.setLocation(metadata, compre.iterRange().id())
797814
.build();
798815
}
799-
IntermediateResult accuValue = evalNonstrictly(frame, compre.accuInit());
816+
IntermediateResult accuValue;
817+
if (celOptions.enableComprehensionLazyEval() && LazyExpression.isLazilyEvaluable(compre)) {
818+
accuValue = IntermediateResult.create(new LazyExpression(compre.accuInit()));
819+
} else {
820+
accuValue = evalNonstrictly(frame, compre.accuInit());
821+
}
800822
int i = 0;
801823
for (Object elem : iterRange) {
802824
frame.incrementIterations();
@@ -831,11 +853,39 @@ private IntermediateResult evalComprehension(
831853
}
832854
}
833855

856+
/** Contains a CelExpr that is to be lazily evaluated. */
857+
private static class LazyExpression {
858+
private final CelExpr celExpr;
859+
860+
/**
861+
* Checks whether the provided expression can be evaluated lazily then cached. For example, the
862+
* accumulator initializer in `cel.bind` macro is a good candidate because it never needs to be
863+
* updated after being evaluated once.
864+
*/
865+
private static boolean isLazilyEvaluable(CelComprehension comprehension) {
866+
// For now, just handle cel.bind. cel.block will be a future addition.
867+
return comprehension
868+
.loopCondition()
869+
.constantOrDefault()
870+
.getKind()
871+
.equals(CelConstant.Kind.BOOLEAN_VALUE)
872+
&& !comprehension.loopCondition().constant().booleanValue()
873+
&& comprehension.iterVar().equals("#unused")
874+
&& comprehension.iterRange().exprKind().getKind().equals(ExprKind.Kind.CREATE_LIST)
875+
&& comprehension.iterRange().createList().elements().isEmpty();
876+
}
877+
878+
private LazyExpression(CelExpr celExpr) {
879+
this.celExpr = celExpr;
880+
}
881+
}
882+
834883
/** This class tracks the state meaningful to a single evaluation pass. */
835884
private static class ExecutionFrame {
836885
private final CelEvaluationListener evaluationListener;
837886
private final int maxIterations;
838887
private final ArrayDeque<RuntimeUnknownResolver> resolvers;
888+
private final Map<String, IntermediateResult> lazyEvalResultCache;
839889
private RuntimeUnknownResolver currentResolver;
840890
private int iterations;
841891

@@ -848,6 +898,7 @@ private ExecutionFrame(
848898
this.resolvers.add(resolver);
849899
this.currentResolver = resolver;
850900
this.maxIterations = maxIterations;
901+
this.lazyEvalResultCache = new HashMap<>();
851902
}
852903

853904
private CelEvaluationListener getEvaluationListener() {
@@ -878,6 +929,14 @@ private Optional<Object> resolveAttribute(CelAttribute attr) {
878929
return currentResolver.resolveAttribute(attr);
879930
}
880931

932+
private Optional<IntermediateResult> lookupLazilyEvaluatedResult(String name) {
933+
return Optional.ofNullable(lazyEvalResultCache.get(name));
934+
}
935+
936+
private void cacheLazilyEvaluatedResult(String name, IntermediateResult result) {
937+
lazyEvalResultCache.put(name, result);
938+
}
939+
881940
private void pushScope(ImmutableMap<String, IntermediateResult> scope) {
882941
RuntimeUnknownResolver scopedResolver = currentResolver.withScope(scope);
883942
currentResolver = scopedResolver;

0 commit comments

Comments
 (0)