Skip to content

Commit d2fe8b7

Browse files
l46kokcopybara-github
authored andcommitted
Add capability to evaluate cel.block calls in the runtime
PiperOrigin-RevId: 602536654
1 parent 00d7726 commit d2fe8b7

5 files changed

Lines changed: 292 additions & 3 deletions

File tree

optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@ java_library(
4242
"//bundle:cel",
4343
"//checker:checker_legacy_environment",
4444
"//common",
45+
"//common:compiler_common",
4546
"//common/ast",
4647
"//common/navigation",
48+
"//common/types",
49+
"//common/types:type_providers",
4750
"//optimizer:ast_optimizer",
4851
"//optimizer:mutable_ast",
4952
"//parser:operator",

optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,25 @@
1919
import static java.util.Arrays.stream;
2020

2121
import com.google.auto.value.AutoValue;
22+
import com.google.common.annotations.VisibleForTesting;
2223
import com.google.common.collect.ImmutableList;
2324
import com.google.common.collect.ImmutableSet;
2425
import com.google.common.collect.Streams;
2526
import dev.cel.bundle.Cel;
2627
import dev.cel.checker.Standard;
2728
import dev.cel.common.CelAbstractSyntaxTree;
29+
import dev.cel.common.CelFunctionDecl;
30+
import dev.cel.common.CelOverloadDecl;
2831
import dev.cel.common.CelSource;
2932
import dev.cel.common.ast.CelExpr;
3033
import dev.cel.common.ast.CelExpr.CelIdent;
3134
import dev.cel.common.ast.CelExpr.ExprKind.Kind;
3235
import dev.cel.common.navigation.CelNavigableAst;
3336
import dev.cel.common.navigation.CelNavigableExpr;
3437
import dev.cel.common.navigation.CelNavigableExpr.TraversalOrder;
38+
import dev.cel.common.types.CelType;
39+
import dev.cel.common.types.ListType;
40+
import dev.cel.common.types.SimpleType;
3541
import dev.cel.optimizer.CelAstOptimizer;
3642
import dev.cel.optimizer.MutableAst;
3743
import dev.cel.parser.Operator;
@@ -64,6 +70,7 @@ public class SubexpressionOptimizer implements CelAstOptimizer {
6470
new SubexpressionOptimizer(SubexpressionOptimizerOptions.newBuilder().build());
6571
private static final String BIND_IDENTIFIER_PREFIX = "@r";
6672
private static final String MANGLED_COMPREHENSION_IDENTIFIER_PREFIX = "@c";
73+
private static final String CEL_BLOCK_FUNCTION = "cel.@block";
6774
private static final ImmutableSet<String> CSE_ALLOWED_FUNCTIONS =
6875
Streams.concat(
6976
stream(Operator.values()).map(Operator::getFunction),
@@ -325,6 +332,14 @@ private CelExpr normalizeForEquality(CelExpr celExpr) {
325332
return mutableAst.clearExprIds(celExpr);
326333
}
327334

335+
@VisibleForTesting
336+
static CelFunctionDecl newCelBlockFunctionDecl(CelType resultType) {
337+
return CelFunctionDecl.newFunctionDeclaration(
338+
CEL_BLOCK_FUNCTION,
339+
CelOverloadDecl.newGlobalOverload(
340+
"cel_block_list", resultType, ListType.create(SimpleType.DYN), resultType));
341+
}
342+
328343
/** Options to configure how Common Subexpression Elimination behave. */
329344
@AutoValue
330345
public abstract static class SubexpressionOptimizerOptions {

optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,21 @@ java_library(
1313
"//common:compiler_common",
1414
"//common:options",
1515
"//common/ast",
16+
"//common/navigation",
1617
"//common/resources/testdata/proto3:test_all_types_java_proto",
1718
"//common/types",
1819
"//extensions",
1920
"//extensions:optional_library",
2021
"//optimizer",
22+
"//optimizer:mutable_ast",
2123
"//optimizer:optimization_exception",
2224
"//optimizer:optimizer_builder",
2325
"//optimizer/optimizers:common_subexpression_elimination",
2426
"//optimizer/optimizers:constant_folding",
2527
"//parser:macro",
2628
"//parser:operator",
2729
"//parser:unparser",
30+
"//runtime",
2831
"@maven//:com_google_guava_guava",
2932
"@maven//:com_google_testparameterinjector_test_parameter_injector",
3033
"@maven//:junit_junit",

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

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,15 @@
2929
import dev.cel.common.CelAbstractSyntaxTree;
3030
import dev.cel.common.CelFunctionDecl;
3131
import dev.cel.common.CelOptions;
32+
import dev.cel.common.CelOverloadDecl;
33+
import dev.cel.common.CelValidationException;
34+
import dev.cel.common.CelVarDecl;
3235
import dev.cel.common.ast.CelConstant;
3336
import dev.cel.common.ast.CelExpr;
37+
import dev.cel.common.ast.CelExpr.ExprKind.Kind;
38+
import dev.cel.common.navigation.CelNavigableAst;
39+
import dev.cel.common.navigation.CelNavigableExpr;
40+
import dev.cel.common.types.ListType;
3441
import dev.cel.common.types.OptionalType;
3542
import dev.cel.common.types.SimpleType;
3643
import dev.cel.common.types.StructTypeReference;
@@ -39,14 +46,19 @@
3946
import dev.cel.optimizer.CelOptimizationException;
4047
import dev.cel.optimizer.CelOptimizer;
4148
import dev.cel.optimizer.CelOptimizerFactory;
49+
import dev.cel.optimizer.MutableAst;
4250
import dev.cel.optimizer.optimizers.SubexpressionOptimizer.SubexpressionOptimizerOptions;
4351
import dev.cel.parser.CelStandardMacro;
4452
import dev.cel.parser.CelUnparser;
4553
import dev.cel.parser.CelUnparserFactory;
4654
import dev.cel.parser.Operator;
55+
import dev.cel.runtime.CelRuntime;
56+
import dev.cel.runtime.CelRuntime.CelFunctionBinding;
57+
import dev.cel.runtime.CelRuntimeFactory;
4758
import dev.cel.testing.testdata.proto3.TestAllTypesProto.NestedTestAllTypes;
4859
import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes;
4960
import java.util.Optional;
61+
import java.util.concurrent.atomic.AtomicInteger;
5062
import org.junit.Test;
5163
import org.junit.runner.RunWith;
5264

@@ -55,6 +67,37 @@ public class SubexpressionOptimizerTest {
5567

5668
private static final Cel CEL = newCelBuilder().build();
5769

70+
private static final Cel CEL_FOR_EVALUATING_BLOCK =
71+
CelFactory.standardCelBuilder()
72+
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
73+
.addFunctionDeclarations(
74+
// These are test only declarations, as the actual function is made internal using @
75+
// symbol.
76+
// If the main function declaration needs updating, be sure to update the test
77+
// declaration as well.
78+
CelFunctionDecl.newFunctionDeclaration(
79+
"cel.block",
80+
CelOverloadDecl.newGlobalOverload(
81+
"block_test_only_overload",
82+
SimpleType.DYN,
83+
ListType.create(SimpleType.DYN),
84+
SimpleType.DYN)),
85+
SubexpressionOptimizer.newCelBlockFunctionDecl(SimpleType.DYN),
86+
CelFunctionDecl.newFunctionDeclaration(
87+
"get_true",
88+
CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL)))
89+
// Similarly, this is a test only decl (index0 -> @index0)
90+
.addVarDeclarations(
91+
CelVarDecl.newVarDeclaration("index0", SimpleType.DYN),
92+
CelVarDecl.newVarDeclaration("index1", SimpleType.DYN),
93+
CelVarDecl.newVarDeclaration("index2", SimpleType.DYN),
94+
CelVarDecl.newVarDeclaration("@index0", SimpleType.DYN),
95+
CelVarDecl.newVarDeclaration("@index1", SimpleType.DYN),
96+
CelVarDecl.newVarDeclaration("@index2", SimpleType.DYN))
97+
.addMessageTypes(TestAllTypes.getDescriptor())
98+
.addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName()))
99+
.build();
100+
58101
private static final CelOptimizer CEL_OPTIMIZER =
59102
CelOptimizerFactory.standardCelOptimizerBuilder(CEL)
60103
.addAstOptimizers(
@@ -659,4 +702,213 @@ public void iterationLimitReached_throws() throws Exception {
659702
.optimize(ast));
660703
assertThat(e).hasMessageThat().isEqualTo("Optimization failure: Max iteration count reached.");
661704
}
705+
706+
private enum BlockTestCase {
707+
BOOL_LITERAL("cel.block([true, false], index0 || index1)"),
708+
STRING_CONCAT("cel.block(['a' + 'b', index0 + 'c'], index1 + 'd') == 'abcd'"),
709+
710+
BLOCK_WITH_EXISTS_TRUE("cel.block([[1, 2, 3], [3, 4, 5].exists(e, e in index0)], index1)"),
711+
BLOCK_WITH_EXISTS_FALSE("cel.block([[1, 2, 3], ![4, 5].exists(e, e in index0)], index1)"),
712+
;
713+
714+
private final String source;
715+
716+
BlockTestCase(String source) {
717+
this.source = source;
718+
}
719+
}
720+
721+
@Test
722+
public void block_success(@TestParameter BlockTestCase testCase) throws Exception {
723+
CelAbstractSyntaxTree ast = compileUsingInternalFunctions(testCase.source);
724+
725+
Object evaluatedResult = CEL_FOR_EVALUATING_BLOCK.createProgram(ast).eval();
726+
727+
assertThat(evaluatedResult).isNotNull();
728+
}
729+
730+
@Test
731+
@SuppressWarnings("Immutable") // Test only
732+
public void lazyEval_blockIndexNeverReferenced() throws Exception {
733+
AtomicInteger invocation = new AtomicInteger();
734+
CelRuntime celRuntime =
735+
CelRuntimeFactory.standardCelRuntimeBuilder()
736+
.addMessageTypes(TestAllTypes.getDescriptor())
737+
.addFunctionBindings(
738+
CelFunctionBinding.from(
739+
"get_true_overload",
740+
ImmutableList.of(),
741+
arg -> {
742+
invocation.getAndIncrement();
743+
return true;
744+
}))
745+
.build();
746+
CelAbstractSyntaxTree ast =
747+
compileUsingInternalFunctions(
748+
"cel.block([get_true()], has(msg.single_int64) ? index0 : false)");
749+
750+
boolean result =
751+
(boolean)
752+
celRuntime
753+
.createProgram(ast)
754+
.eval(ImmutableMap.of("msg", TestAllTypes.getDefaultInstance()));
755+
756+
assertThat(result).isFalse();
757+
assertThat(invocation.get()).isEqualTo(0);
758+
}
759+
760+
@Test
761+
@SuppressWarnings("Immutable") // Test only
762+
public void lazyEval_blockIndexEvaluatedOnlyOnce() throws Exception {
763+
AtomicInteger invocation = new AtomicInteger();
764+
CelRuntime celRuntime =
765+
CelRuntimeFactory.standardCelRuntimeBuilder()
766+
.addMessageTypes(TestAllTypes.getDescriptor())
767+
.addFunctionBindings(
768+
CelFunctionBinding.from(
769+
"get_true_overload",
770+
ImmutableList.of(),
771+
arg -> {
772+
invocation.getAndIncrement();
773+
return true;
774+
}))
775+
.build();
776+
CelAbstractSyntaxTree ast =
777+
compileUsingInternalFunctions("cel.block([get_true()], index0 && index0 && index0)");
778+
779+
boolean result = (boolean) celRuntime.createProgram(ast).eval();
780+
781+
assertThat(result).isTrue();
782+
assertThat(invocation.get()).isEqualTo(1);
783+
}
784+
785+
@Test
786+
@SuppressWarnings("Immutable") // Test only
787+
public void lazyEval_multipleBlockIndices_inResultExpr() throws Exception {
788+
AtomicInteger invocation = new AtomicInteger();
789+
CelRuntime celRuntime =
790+
CelRuntimeFactory.standardCelRuntimeBuilder()
791+
.addMessageTypes(TestAllTypes.getDescriptor())
792+
.addFunctionBindings(
793+
CelFunctionBinding.from(
794+
"get_true_overload",
795+
ImmutableList.of(),
796+
arg -> {
797+
invocation.getAndIncrement();
798+
return true;
799+
}))
800+
.build();
801+
CelAbstractSyntaxTree ast =
802+
compileUsingInternalFunctions(
803+
"cel.block([get_true(), get_true(), get_true()], index0 && index0 && index1 && index1"
804+
+ " && index2 && index2)");
805+
806+
boolean result = (boolean) celRuntime.createProgram(ast).eval();
807+
808+
assertThat(result).isTrue();
809+
assertThat(invocation.get()).isEqualTo(3);
810+
}
811+
812+
@Test
813+
@SuppressWarnings("Immutable") // Test only
814+
public void lazyEval_multipleBlockIndices_cascaded() throws Exception {
815+
AtomicInteger invocation = new AtomicInteger();
816+
CelRuntime celRuntime =
817+
CelRuntimeFactory.standardCelRuntimeBuilder()
818+
.addMessageTypes(TestAllTypes.getDescriptor())
819+
.addFunctionBindings(
820+
CelFunctionBinding.from(
821+
"get_true_overload",
822+
ImmutableList.of(),
823+
arg -> {
824+
invocation.getAndIncrement();
825+
return true;
826+
}))
827+
.build();
828+
CelAbstractSyntaxTree ast =
829+
compileUsingInternalFunctions("cel.block([get_true(), index0, index1], index2)");
830+
831+
boolean result = (boolean) celRuntime.createProgram(ast).eval();
832+
833+
assertThat(result).isTrue();
834+
assertThat(invocation.get()).isEqualTo(1);
835+
}
836+
837+
@Test
838+
@TestParameters("{source: 'cel.block([])'}")
839+
@TestParameters("{source: 'cel.block([1])'}")
840+
@TestParameters("{source: 'cel.block(1, 2)'}")
841+
@TestParameters("{source: 'cel.block(1, [1])'}")
842+
public void block_invalidArguments_throws(String source) {
843+
CelValidationException e =
844+
assertThrows(CelValidationException.class, () -> compileUsingInternalFunctions(source));
845+
846+
assertThat(e).hasMessageThat().contains("found no matching overload for 'cel.block'");
847+
}
848+
849+
@Test
850+
public void blockIndex_invalidArgument_throws() {
851+
CelValidationException e =
852+
assertThrows(
853+
CelValidationException.class,
854+
() -> compileUsingInternalFunctions("cel.block([1], index)"));
855+
856+
assertThat(e).hasMessageThat().contains("undeclared reference");
857+
}
858+
859+
/**
860+
* Converts AST containing cel.block related test functions to internal functions (e.g: cel.block
861+
* -> cel.@block)
862+
*/
863+
private static CelAbstractSyntaxTree compileUsingInternalFunctions(String expression)
864+
throws CelValidationException {
865+
MutableAst mutableAst = MutableAst.newInstance(1000);
866+
CelAbstractSyntaxTree astToModify = CEL_FOR_EVALUATING_BLOCK.compile(expression).getAst();
867+
while (true) {
868+
CelExpr celExpr =
869+
CelNavigableAst.fromAst(astToModify)
870+
.getRoot()
871+
.allNodes()
872+
.filter(node -> node.getKind().equals(Kind.CALL))
873+
.map(CelNavigableExpr::expr)
874+
.filter(expr -> expr.call().function().equals("cel.block"))
875+
.findAny()
876+
.orElse(null);
877+
if (celExpr == null) {
878+
break;
879+
}
880+
astToModify =
881+
mutableAst.replaceSubtree(
882+
astToModify,
883+
celExpr.toBuilder()
884+
.setCall(celExpr.call().toBuilder().setFunction("cel.@block").build())
885+
.build(),
886+
celExpr.id());
887+
}
888+
889+
while (true) {
890+
CelExpr celExpr =
891+
CelNavigableAst.fromAst(astToModify)
892+
.getRoot()
893+
.allNodes()
894+
.filter(node -> node.getKind().equals(Kind.IDENT))
895+
.map(CelNavigableExpr::expr)
896+
.filter(expr -> expr.ident().name().startsWith("index"))
897+
.findAny()
898+
.orElse(null);
899+
if (celExpr == null) {
900+
break;
901+
}
902+
String internalIdentName = "@" + celExpr.ident().name();
903+
astToModify =
904+
mutableAst.replaceSubtree(
905+
astToModify,
906+
celExpr.toBuilder()
907+
.setIdent(celExpr.ident().toBuilder().setName(internalIdentName).build())
908+
.build(),
909+
celExpr.id());
910+
}
911+
912+
return CEL_FOR_EVALUATING_BLOCK.check(astToModify).getAst();
913+
}
662914
}

0 commit comments

Comments
 (0)