2121
2222import com .google .auto .value .AutoValue ;
2323import com .google .common .collect .ImmutableList ;
24+ import com .google .common .collect .ImmutableMap ;
2425import com .google .common .collect .ImmutableSet ;
2526import com .google .errorprone .annotations .CanIgnoreReturnValue ;
2627import dev .cel .bundle .Cel ;
6263import java .util .ArrayList ;
6364import java .util .Arrays ;
6465import java .util .Collection ;
66+ import java .util .HashMap ;
6567import java .util .HashSet ;
68+ import java .util .Iterator ;
6669import java .util .List ;
6770import java .util .Map ;
71+ import java .util .Objects ;
6872import java .util .Optional ;
6973import org .jspecify .annotations .Nullable ;
7074
7377 * calls and select statements with their evaluated result.
7478 */
7579public final class ConstantFoldingOptimizer implements CelAstOptimizer {
80+ private static final ImmutableSet <String > BOOLEAN_RETURN_OPERATORS =
81+ ImmutableSet .of (
82+ Operator .LOGICAL_AND .getFunction (),
83+ Operator .LOGICAL_OR .getFunction (),
84+ Operator .LOGICAL_NOT .getFunction (),
85+ Operator .EQUALS .getFunction (),
86+ Operator .NOT_EQUALS .getFunction (),
87+ Operator .LESS .getFunction (),
88+ Operator .LESS_EQUALS .getFunction (),
89+ Operator .GREATER .getFunction (),
90+ Operator .GREATER_EQUALS .getFunction (),
91+ Operator .IN .getFunction ());
92+
7693 private static final ConstantFoldingOptimizer INSTANCE =
7794 new ConstantFoldingOptimizer (ConstantFoldingOptions .newBuilder ().build ());
7895
@@ -115,6 +132,25 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
115132 Cel optimizerEnv = builder .setResultType (SimpleType .DYN ).build ();
116133
117134 CelMutableAst mutableAst = CelMutableAst .fromCelAst (ast );
135+
136+ // HACK: The AstMutator strips type metadata during intermediate folds due to ID renumbering.
137+ // We pre-compute identifier types from the unmutated AST to safely evaluate boolean conditions
138+ // later.
139+ // TODO: Improve AstMutator to retain type metadata when possible.
140+ Map <String , CelType > mutableIdentTypes = new HashMap <>();
141+ Iterator <CelNavigableMutableExpr > identNodes =
142+ CelNavigableMutableAst .fromAst (mutableAst )
143+ .getRoot ()
144+ .allNodes ()
145+ .filter (node -> node .getKind ().equals (Kind .IDENT ))
146+ .iterator ();
147+ while (identNodes .hasNext ()) {
148+ CelNavigableMutableExpr node = identNodes .next ();
149+ Optional <CelType > type = mutableAst .getType (node .id ());
150+ type .ifPresent (celType -> mutableIdentTypes .put (node .expr ().ident ().name (), celType ));
151+ }
152+ ImmutableMap <String , CelType > identTypes = ImmutableMap .copyOf (mutableIdentTypes );
153+
118154 int iterCount = 0 ;
119155 boolean continueFolding = true ;
120156 while (continueFolding ) {
@@ -123,7 +159,6 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
123159 }
124160 iterCount ++;
125161 continueFolding = false ;
126-
127162 ImmutableList <CelNavigableMutableExpr > foldableExprs =
128163 CelNavigableMutableAst .fromAst (mutableAst )
129164 .getRoot ()
@@ -135,7 +170,7 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
135170
136171 Optional <CelMutableAst > mutatedResult ;
137172 // Attempt to prune if it is a non-strict call
138- mutatedResult = maybePruneBranches (mutableAst , foldableExpr .expr ());
173+ mutatedResult = maybePruneBranches (mutableAst , identTypes , foldableExpr .expr ());
139174 if (!mutatedResult .isPresent ()) {
140175 // Evaluate the call then fold
141176 try {
@@ -210,6 +245,9 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {
210245
211246 if (functionName .equals (Operator .EQUALS .getFunction ())
212247 || functionName .equals (Operator .NOT_EQUALS .getFunction ())) {
248+ if (hasComprehensionVar (navigableExpr )) {
249+ return false ;
250+ }
213251 if (mutableCall .args ().stream ()
214252 .anyMatch (node -> isExprConstantOfKind (node , CelConstant .Kind .BOOLEAN_VALUE ))
215253 || mutableCall .args ().stream ()
@@ -219,7 +257,7 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {
219257 }
220258
221259 if (functionName .equals (Operator .IN .getFunction ())) {
222- return canFoldInOperator (navigableExpr );
260+ return ! hasComprehensionVar (navigableExpr );
223261 }
224262
225263 // Default case: all call arguments must be constants. If the argument is a container (ex:
@@ -248,32 +286,31 @@ private static boolean isCallTimestampOrDuration(CelMutableCall call) {
248286 || call .function ().equals (DURATION .functionName ());
249287 }
250288
251- private static boolean canFoldInOperator (CelNavigableMutableExpr navigableExpr ) {
252- ImmutableList <CelNavigableMutableExpr > allIdents =
253- navigableExpr
254- .allNodes ()
255- .filter (node -> node .getKind ().equals (Kind .IDENT ))
256- .collect (toImmutableList ());
257- for (CelNavigableMutableExpr identNode : allIdents ) {
258- CelNavigableMutableExpr parent = identNode .parent ().orElse (null );
259- while (parent != null ) {
260- if (parent .getKind ().equals (Kind .COMPREHENSION )) {
261- String identName = identNode .expr ().ident ().name ();
262- CelMutableComprehension parentComprehension = parent .expr ().comprehension ();
263- if (parentComprehension .accuVar ().equals (identName )
264- || parentComprehension .iterVar ().equals (identName )
265- || parentComprehension .iterVar2 ().equals (identName )) {
266- // Prevent folding a subexpression if it contains a variable declared by a
267- // comprehension. The subexpression cannot be compiled without the full context of the
268- // surrounding comprehension.
269- return false ;
270- }
271- }
272- parent = parent .parent ().orElse (null );
273- }
274- }
275-
276- return true ;
289+ private static boolean hasComprehensionVar (CelNavigableMutableExpr expr ) {
290+ return expr .allNodes ()
291+ .filter (node -> node .getKind ().equals (Kind .IDENT ))
292+ .anyMatch (
293+ identNode -> {
294+ String identName = identNode .expr ().ident ().name ();
295+ CelNavigableMutableExpr curr = identNode ;
296+ Optional <CelNavigableMutableExpr > maybeParent = curr .parent ();
297+ while (maybeParent .isPresent ()) {
298+ CelNavigableMutableExpr parent = maybeParent .get ();
299+ if (parent .getKind ().equals (Kind .COMPREHENSION )) {
300+ CelMutableComprehension compre = parent .expr ().comprehension ();
301+ if ((compre .accuVar ().equals (identName )
302+ || compre .iterVar ().equals (identName )
303+ || compre .iterVar2 ().equals (identName ))
304+ && curr .id () != compre .iterRange ().id ()
305+ && curr .id () != compre .accuInit ().id ()) {
306+ return true ;
307+ }
308+ }
309+ curr = parent ;
310+ maybeParent = parent .parent ();
311+ }
312+ return false ;
313+ });
277314 }
278315
279316 private static boolean areChildrenArgConstant (CelNavigableMutableExpr expr ) {
@@ -311,6 +348,9 @@ private Optional<CelMutableAst> maybeFold(
311348 CelMutableAst mutableAst ,
312349 CelNavigableMutableExpr node )
313350 throws CelOptimizationException , CelEvaluationException {
351+ if (!node .getKind ().equals (Kind .COMPREHENSION ) && hasComprehensionVar (node )) {
352+ return Optional .empty ();
353+ }
314354 Object result ;
315355 try {
316356 result = evaluateExpr (cel , node );
@@ -465,7 +505,7 @@ private static boolean isCallToFunction(CelMutableExpr expr, String functionName
465505
466506 /** Inspects the non-strict calls to determine whether a branch can be removed. */
467507 private Optional <CelMutableAst > maybePruneBranches (
468- CelMutableAst mutableAst , CelMutableExpr expr ) {
508+ CelMutableAst mutableAst , Map < String , CelType > identTypes , CelMutableExpr expr ) {
469509 if (!expr .getKind ().equals (Kind .CALL )) {
470510 return Optional .empty ();
471511 }
@@ -474,7 +514,7 @@ private Optional<CelMutableAst> maybePruneBranches(
474514 String function = call .function ();
475515 if (function .equals (Operator .LOGICAL_AND .getFunction ())
476516 || function .equals (Operator .LOGICAL_OR .getFunction ())) {
477- return maybeShortCircuitCall (mutableAst , expr );
517+ return maybeShortCircuitCall (mutableAst , identTypes , expr );
478518 } else if (function .equals (Operator .CONDITIONAL .getFunction ())) {
479519 CelMutableExpr cond = call .args ().get (0 );
480520 CelMutableExpr truthy = call .args ().get (1 );
@@ -518,24 +558,28 @@ private Optional<CelMutableAst> maybePruneBranches(
518558 || function .equals (Operator .NOT_EQUALS .getFunction ())) {
519559 CelMutableExpr lhs = call .args ().get (0 );
520560 CelMutableExpr rhs = call .args ().get (1 );
521- boolean lhsIsBoolean = isExprConstantOfKind (lhs , CelConstant .Kind .BOOLEAN_VALUE );
522- boolean rhsIsBoolean = isExprConstantOfKind (rhs , CelConstant .Kind .BOOLEAN_VALUE );
561+ boolean lhsIsBooleanConstant = isExprConstantOfKind (lhs , CelConstant .Kind .BOOLEAN_VALUE );
562+ boolean rhsIsBooleanConstant = isExprConstantOfKind (rhs , CelConstant .Kind .BOOLEAN_VALUE );
523563 boolean invertCondition = function .equals (Operator .NOT_EQUALS .getFunction ());
524564 Optional <CelMutableExpr > replacementExpr = Optional .empty ();
525565
526566 if (lhs .getKind ().equals (Kind .CONSTANT ) && rhs .getKind ().equals (Kind .CONSTANT )) {
527567 // If both args are const, don't prune any branches and let maybeFold method evaluate this
528568 // subExpr
529569 return Optional .empty ();
530- } else if (lhsIsBoolean ) {
570+ } else if (lhsIsBooleanConstant
571+ && (!constantFoldingOptions .enableSafeLogicalOptimization ()
572+ || evaluatesToBoolean (mutableAst , identTypes , rhs ))) {
531573 boolean cond = invertCondition != lhs .constant ().booleanValue ();
532574 replacementExpr =
533575 Optional .of (
534576 cond
535577 ? rhs
536578 : CelMutableExpr .ofCall (
537579 CelMutableCall .create (Operator .LOGICAL_NOT .getFunction (), rhs )));
538- } else if (rhsIsBoolean ) {
580+ } else if (rhsIsBooleanConstant
581+ && (!constantFoldingOptions .enableSafeLogicalOptimization ()
582+ || evaluatesToBoolean (mutableAst , identTypes , lhs ))) {
539583 boolean cond = invertCondition != rhs .constant ().booleanValue ();
540584 replacementExpr =
541585 Optional .of (
@@ -552,7 +596,7 @@ private Optional<CelMutableAst> maybePruneBranches(
552596 }
553597
554598 private Optional <CelMutableAst > maybeShortCircuitCall (
555- CelMutableAst mutableAst , CelMutableExpr expr ) {
599+ CelMutableAst mutableAst , Map < String , CelType > identTypes , CelMutableExpr expr ) {
556600 CelMutableCall call = expr .call ();
557601 boolean shortCircuit = false ;
558602 boolean skip = true ;
@@ -583,14 +627,39 @@ private Optional<CelMutableAst> maybeShortCircuitCall(
583627 return Optional .of (astMutator .replaceSubtree (mutableAst , shortCircuitTarget , expr .id ()));
584628 }
585629 if (newArgs .size () == 1 ) {
586- return Optional .of (astMutator .replaceSubtree (mutableAst , newArgs .get (0 ), expr .id ()));
630+ CelMutableExpr remainingArg = newArgs .get (0 );
631+ if (!constantFoldingOptions .enableSafeLogicalOptimization ()
632+ || evaluatesToBoolean (mutableAst , identTypes , remainingArg )) {
633+ return Optional .of (astMutator .replaceSubtree (mutableAst , remainingArg , expr .id ()));
634+ }
635+ return Optional .empty ();
587636 }
588637
589638 // TODO: Support folding variadic AND/ORs.
590639 throw new UnsupportedOperationException (
591640 "Folding variadic logical operator is not supported yet." );
592641 }
593642
643+ private boolean evaluatesToBoolean (
644+ CelMutableAst mutableAst , Map <String , CelType > identTypes , CelMutableExpr expr ) {
645+ if (isExprConstantOfKind (expr , CelConstant .Kind .BOOLEAN_VALUE )) {
646+ return true ;
647+ }
648+ // The AST's type map relies on the type-checker having explicitly populated the type for a
649+ // given node. However, during the optimization pipeline, mutated intermediate nodes might
650+ // temporarily lack type metadata. Standard CEL operators like &&, ||, and == inherently
651+ // always return a boolean, so checking the function name provides a reliable fallback when
652+ // the type map is incomplete.
653+ if (expr .getKind ().equals (Kind .CALL )
654+ && BOOLEAN_RETURN_OPERATORS .contains (expr .call ().function ())) {
655+ return true ;
656+ }
657+ if (expr .getKind ().equals (Kind .IDENT )) {
658+ return Objects .equals (identTypes .get (expr .ident ().name ()), SimpleType .BOOL );
659+ }
660+ return mutableAst .getType (expr .id ()).map (SimpleType .BOOL ::equals ).orElse (false );
661+ }
662+
594663 private boolean isFoldedAggregateLiteral (CelMutableExpr expr ) {
595664 if (expr .getKind ().equals (Kind .CONSTANT )) {
596665 return true ;
@@ -811,6 +880,13 @@ public abstract static class ConstantFoldingOptions {
811880
812881 public abstract ImmutableSet <String > foldableFunctions ();
813882
883+ /**
884+ * Returns true if safe logical optimization is enabled. When enabled, logical (&&, ||) and
885+ * equality (==, !=) expression optimizations strictly verify that pruned sub-expressions
886+ * evaluate to booleans.
887+ */
888+ public abstract boolean enableSafeLogicalOptimization ();
889+
814890 /** Builder for configuring the {@link ConstantFoldingOptions}. */
815891 @ AutoValue .Builder
816892 public abstract static class Builder {
@@ -823,6 +899,17 @@ public abstract static class Builder {
823899 */
824900 public abstract Builder maxIterationLimit (int value );
825901
902+ /**
903+ * Enables or disables safe logical optimization. When enabled (default: {@code true}),
904+ * constant folding on logical (&&, ||) and equality (==, !=) operators strictly checks
905+ * whether sub-expressions evaluate to booleans before pruning them. Disabling this flag
906+ * restores legacy aggressive folding behavior.
907+ *
908+ * <p>Note: Disabling this flag should only be done temporarily for migration purposes, with
909+ * the goal of eventually enabling it for safety.
910+ */
911+ public abstract Builder enableSafeLogicalOptimization (boolean value );
912+
826913 /**
827914 * Adds a collection of custom functions that will be a candidate for constant folding. By
828915 * default, standard functions are foldable.
@@ -850,7 +937,8 @@ public Builder addFoldableFunctions(String... functions) {
850937 /** Returns a new options builder with recommended defaults pre-configured. */
851938 public static Builder newBuilder () {
852939 return new AutoValue_ConstantFoldingOptimizer_ConstantFoldingOptions .Builder ()
853- .maxIterationLimit (400 );
940+ .maxIterationLimit (400 )
941+ .enableSafeLogicalOptimization (true );
854942 }
855943
856944 ConstantFoldingOptions () {}
0 commit comments