3838import dev .cel .common .ast .CelMutableExpr .CelMutableCall ;
3939import dev .cel .common .ast .CelMutableExpr .CelMutableList ;
4040import dev .cel .common .ast .CelMutableExpr .CelMutableMap ;
41+ import dev .cel .common .ast .CelMutableExpr .CelMutableSelect ;
4142import dev .cel .common .ast .CelMutableExpr .CelMutableStruct ;
4243import dev .cel .common .ast .CelMutableExprConverter ;
4344import dev .cel .common .internal .DateTimeHelpers ;
5556import dev .cel .common .values .StructValue ;
5657import dev .cel .extensions .CelOptionalLibrary .Function ;
5758import dev .cel .optimizer .AstMutator ;
58- import dev .cel .optimizer .AstMutator .SubtreeReplacement ;
5959import dev .cel .optimizer .CelAstOptimizer ;
6060import dev .cel .optimizer .CelOptimizationException ;
6161import dev .cel .runtime .CelAttributePattern ;
7373import java .util .Map ;
7474import java .util .Objects ;
7575import java .util .Optional ;
76+ import java .util .Set ;
7677import org .jspecify .annotations .Nullable ;
7778
7879/**
@@ -141,7 +142,7 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel)
141142 ImmutableMap <String , CelType > identTypes = precomputeIdentTypes (initialMutableAst );
142143
143144 CelMutableAst mutableAst =
144- foldConstants (optimizerEnv , valueProvider , identTypes , initialMutableAst );
145+ foldConstants (cel , optimizerEnv , valueProvider , identTypes , initialMutableAst );
145146 mutableAst = pruneOptionalElements (mutableAst );
146147
147148 if (mutableAst == initialMutableAst ) {
@@ -172,23 +173,25 @@ private static ImmutableMap<String, CelType> precomputeIdentTypes(CelMutableAst
172173 }
173174
174175 private CelMutableAst foldConstants (
176+ Cel cel ,
175177 Cel optimizerEnv ,
176178 @ Nullable CelValueProvider valueProvider ,
177179 ImmutableMap <String , CelType > identTypes ,
178180 CelMutableAst mutableAst )
179181 throws CelOptimizationException {
180182 for (int iterCount = 0 ; iterCount < constantFoldingOptions .maxIterationLimit (); iterCount ++) {
181- Optional <SubtreeReplacement > replacement =
182- findNextFoldableSubtree (optimizerEnv , valueProvider , identTypes , mutableAst );
183- if (!replacement .isPresent ()) {
183+ Optional <CelMutableAst > nextAst =
184+ findNextFoldableSubtree (cel , optimizerEnv , valueProvider , identTypes , mutableAst );
185+ if (!nextAst .isPresent ()) {
184186 return mutableAst ;
185187 }
186- mutableAst = astMutator . replaceSubtree ( mutableAst , replacement . get () );
188+ mutableAst = nextAst . get ();
187189 }
188190 throw new IllegalStateException ("Max iteration count reached." );
189191 }
190192
191- private Optional <SubtreeReplacement > findNextFoldableSubtree (
193+ private Optional <CelMutableAst > findNextFoldableSubtree (
194+ Cel cel ,
192195 Cel optimizerEnv ,
193196 @ Nullable CelValueProvider valueProvider ,
194197 ImmutableMap <String , CelType > identTypes ,
@@ -199,19 +202,40 @@ private Optional<SubtreeReplacement> findNextFoldableSubtree(
199202 navAst .getRoot ().allNodes (TraversalOrder .PRE_ORDER ).filter (this ::canFold ).iterator ();
200203 while (foldableExprs .hasNext ()) {
201204 CelNavigableMutableExpr foldableExpr = foldableExprs .next ();
202- Optional <CelMutableExpr > pruned =
205+ Optional <CelMutableExpr > candidate =
203206 maybePruneBranches (mutableAst , identTypes , foldableExpr .expr ());
204- if (pruned .isPresent ()) {
205- return Optional . of ( SubtreeReplacement . of ( foldableExpr . id (), pruned . get ()) );
207+ if (! candidate .isPresent ()) {
208+ candidate = maybeFold ( optimizerEnv , valueProvider , foldableExpr );
206209 }
207- Optional <CelMutableExpr > folded = maybeFold (optimizerEnv , valueProvider , foldableExpr );
208- if (folded .isPresent ()) {
209- return Optional .of (SubtreeReplacement .of (foldableExpr .id (), folded .get ()));
210+ if (candidate .isPresent ()) {
211+ Optional <CelMutableAst > replacedAst =
212+ tryReplaceSubtree (cel , optimizerEnv , mutableAst , foldableExpr , candidate .get ());
213+ if (replacedAst .isPresent ()) {
214+ return replacedAst ;
215+ }
210216 }
211217 }
212218 return Optional .empty ();
213219 }
214220
221+ private Optional <CelMutableAst > tryReplaceSubtree (
222+ Cel cel ,
223+ Cel optimizerEnv ,
224+ CelMutableAst mutableAst ,
225+ CelNavigableMutableExpr foldableExpr ,
226+ CelMutableExpr candidate ) {
227+ // Folding an expression containing a dynamic identifier can narrow its static type from dyn to
228+ // a concrete type (e.g. `false || [3, x][0]` -> `false || 3`), which may cause the enclosing
229+ // expression or expected result type to fail type-checking.
230+ CelMutableAst candidateAst =
231+ astMutator .replaceSubtree (
232+ CelMutableAst .fromCelAst (mutableAst .toParsedAst ()), candidate , foldableExpr .id ());
233+ Cel envToCheck = foldableExpr .parent ().isPresent () ? optimizerEnv : cel ;
234+ return envToCheck .check (candidateAst .toParsedAst ()).hasError ()
235+ ? Optional .empty ()
236+ : Optional .of (candidateAst );
237+ }
238+
215239 private boolean canFold (CelNavigableMutableExpr navigableExpr ) {
216240 switch (navigableExpr .getKind ()) {
217241 case CALL :
@@ -269,19 +293,55 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) {
269293 return !CelNavigableExprUtil .hasComprehensionVariable (navigableExpr );
270294 }
271295
272- // Default case: all call arguments must be constants. If the argument is a container (ex:
273- // list, map), then its arguments must be a constant.
274- return areChildrenArgConstant (navigableExpr );
296+ return areChildrenArgConstant (navigableExpr )
297+ || maybePruneContainerLookup (navigableExpr .expr ()).isPresent ();
275298 case SELECT :
276299 CelNavigableMutableExpr operand = navigableExpr .children ().collect (onlyElement ());
277- return areChildrenArgConstant (operand );
300+ return areChildrenArgConstant (operand )
301+ || maybePruneContainerLookup (navigableExpr .expr ()).isPresent ();
278302 case COMPREHENSION :
279303 return !isNestedComprehension (navigableExpr ) && containsFoldableFunctionOnly (navigableExpr );
280304 default :
281305 return false ;
282306 }
283307 }
284308
309+ private static boolean isErrorFree (CelMutableExpr expr ) {
310+ switch (expr .getKind ()) {
311+ case CONSTANT :
312+ case IDENT :
313+ return true ;
314+ case LIST :
315+ CelMutableList list = expr .list ();
316+ return list .optionalIndices ().isEmpty ()
317+ && list .elements ().stream ().allMatch (ConstantFoldingOptimizer ::isErrorFree );
318+ case MAP :
319+ return isErrorFreeMap (expr .map ());
320+ default :
321+ return false ;
322+ }
323+ }
324+
325+ private static boolean isErrorFreeMap (CelMutableMap map ) {
326+ Set <CelConstant > keysSeen = new HashSet <>();
327+ for (CelMutableMap .Entry entry : map .entries ()) {
328+ if (entry .optionalEntry ()
329+ || !entry .key ().getKind ().equals (Kind .CONSTANT )
330+ || !isErrorFree (entry .value ())
331+ || !keysSeen .add (normalizeMapKey (entry .key ().constant ()))) {
332+ return false ;
333+ }
334+ }
335+ return true ;
336+ }
337+
338+ private static CelConstant normalizeMapKey (CelConstant key ) {
339+ if (key .getKind ().equals (CelConstant .Kind .UINT64_VALUE ) && key .uint64Value ().longValue () >= 0 ) {
340+ return CelConstant .ofValue (key .uint64Value ().longValue ());
341+ }
342+ return key ;
343+ }
344+
285345 private boolean containsFoldableFunctionOnly (CelNavigableMutableExpr navigableExpr ) {
286346 return navigableExpr
287347 .allNodes ()
@@ -334,7 +394,13 @@ private Optional<CelMutableExpr> maybeFold(
334394 Object result ;
335395 try {
336396 result = evaluateExpr (cel , node );
337- } catch (CelEvaluationException | CelValidationException e ) {
397+ } catch (CelValidationException | CelEvaluationException e ) {
398+ if (node .getKind ().equals (Kind .COMPREHENSION )) {
399+ // Comprehension may fail to evaluate due to other unknowns the expression references. We'll
400+ // simply opt to not fold the subtree and move on.
401+ return Optional .empty ();
402+ }
403+
338404 throw new CelOptimizationException (
339405 "Constant folding failure. Failed to evaluate subtree due to: " + e .getMessage (), e );
340406 }
@@ -479,7 +545,7 @@ private static boolean isCallToFunction(CelMutableExpr expr, String functionName
479545 private Optional <CelMutableExpr > maybePruneBranches (
480546 CelMutableAst mutableAst , Map <String , CelType > identTypes , CelMutableExpr expr ) {
481547 if (!expr .getKind ().equals (Kind .CALL )) {
482- return Optional . empty ( );
548+ return maybePruneContainerLookup ( expr );
483549 }
484550
485551 CelMutableCall call = expr .call ();
@@ -575,6 +641,74 @@ private Optional<CelMutableExpr> maybePruneBranches(
575641 return replacementExpr ;
576642 }
577643
644+ return maybePruneContainerLookup (expr );
645+ }
646+
647+ private static Optional <CelMutableExpr > maybePruneContainerLookup (CelMutableExpr expr ) {
648+ if (expr .getKind ().equals (Kind .SELECT )) {
649+ CelMutableSelect select = expr .select ();
650+ if (select .operand ().getKind ().equals (Kind .MAP ) && isErrorFreeMap (select .operand ().map ())) {
651+ Optional <CelMutableExpr > val =
652+ findMapEntryByConstantKey (select .operand ().map (), CelConstant .ofValue (select .field ()));
653+ return select .testOnly ()
654+ ? Optional .of (CelMutableExpr .ofConstant (CelConstant .ofValue (val .isPresent ())))
655+ : val ;
656+ }
657+ return Optional .empty ();
658+ }
659+ if (!expr .getKind ().equals (Kind .CALL )) {
660+ return Optional .empty ();
661+ }
662+ CelMutableCall call = expr .call ();
663+ String function = call .function ();
664+ if (function .equals (Operator .INDEX .getFunction ())) {
665+ return findContainerElement (call .args ().get (0 ), call .args ().get (1 ), /* optional= */ false );
666+ }
667+ if (function .equals (Operator .OPTIONAL_INDEX .getFunction ())
668+ || function .equals (Operator .OPTIONAL_SELECT .getFunction ())) {
669+ return findContainerElement (call .args ().get (0 ), call .args ().get (1 ), /* optional= */ true );
670+ }
671+ return Optional .empty ();
672+ }
673+
674+ private static Optional <CelMutableExpr > findContainerElement (
675+ CelMutableExpr target , CelMutableExpr key , boolean optional ) {
676+ Optional <CelMutableExpr > elem ;
677+ if (target .getKind ().equals (Kind .LIST )
678+ && isErrorFree (target )
679+ && isExprConstantOfKind (key , CelConstant .Kind .INT64_VALUE )) {
680+ long idx = key .constant ().int64Value ();
681+ List <CelMutableExpr > elements = target .list ().elements ();
682+ elem =
683+ idx >= 0 && idx < elements .size ()
684+ ? Optional .of (elements .get ((int ) idx ))
685+ : Optional .empty ();
686+ } else if (target .getKind ().equals (Kind .MAP )
687+ && isErrorFreeMap (target .map ())
688+ && key .getKind ().equals (Kind .CONSTANT )) {
689+ elem = findMapEntryByConstantKey (target .map (), key .constant ());
690+ } else {
691+ return Optional .empty ();
692+ }
693+ if (!optional ) {
694+ return elem ;
695+ }
696+ return Optional .of (
697+ elem .map (
698+ val ->
699+ CelMutableExpr .ofCall (
700+ CelMutableCall .create (Function .OPTIONAL_OF .getFunction (), val )))
701+ .orElseGet (ConstantFoldingOptimizer ::newOptionalNoneExpr ));
702+ }
703+
704+ private static Optional <CelMutableExpr > findMapEntryByConstantKey (
705+ CelMutableMap map , CelConstant targetKey ) {
706+ CelConstant normalizedTarget = normalizeMapKey (targetKey );
707+ for (CelMutableMap .Entry entry : map .entries ()) {
708+ if (normalizeMapKey (entry .key ().constant ()).equals (normalizedTarget )) {
709+ return Optional .of (entry .value ());
710+ }
711+ }
578712 return Optional .empty ();
579713 }
580714
0 commit comments