diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java index 35d181905..ccfedc6f3 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java @@ -276,6 +276,37 @@ private static boolean areChildrenArgConstant(CelNavigableMutableExpr expr) { return false; } + /** + * Checks whether the provided identifier node is a loop variable bound by a surrounding + * comprehension. Identifiers existing strictly within loop initialization bounds (e.g. iterRange) + * are not considered loop variables. + */ + private static boolean isComprehensionBoundLoopVariable(CelNavigableMutableExpr identNode) { + CelNavigableMutableExpr child = identNode; + CelNavigableMutableExpr parent = identNode.parent().orElse(null); + + while (parent != null) { + if (parent.getKind().equals(Kind.COMPREHENSION)) { + CelMutableComprehension comprehension = parent.expr().comprehension(); + boolean isInitializationNode = + child.expr().equals(comprehension.iterRange()) + || child.expr().equals(comprehension.accuInit()); + + if (!isInitializationNode) { + String identName = identNode.expr().ident().name(); + if (identName.equals(comprehension.accuVar()) + || identName.equals(comprehension.iterVar()) + || identName.equals(comprehension.iterVar2())) { + return true; + } + } + } + child = parent; + parent = parent.parent().orElse(null); + } + return false; + } + private static boolean isNestedComprehension(CelNavigableMutableExpr expr) { Optional maybeParent = expr.parent(); while (maybeParent.isPresent()) { @@ -733,6 +764,7 @@ private static Object evaluateExpr(Cel cel, CelNavigableMutableExpr navigableMut navigableMutableExpr .allNodes() .filter(node -> node.getKind().equals(Kind.IDENT)) + .filter(node -> !isComprehensionBoundLoopVariable(node)) .map(node -> node.expr().ident().name()) .map(CelAttributePattern::fromQualifiedIdentifier) .collect(toImmutableList()); diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java index ee62c5ed1..dffeaefe0 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java @@ -94,6 +94,7 @@ private static Cel setupEnv(CelBuilder celBuilder) { .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .setOptions(CEL_OPTIONS) .addCompilerLibraries( + CelExtensions.comprehensions(), CelExtensions.bindings(), CelOptionalLibrary.INSTANCE, CelExtensions.math(CEL_OPTIONS), @@ -228,6 +229,22 @@ private static Cel setupEnv(CelBuilder celBuilder) { @TestParameters("{source: 'true == false', expected: 'false'}") @TestParameters("{source: 'true == true', expected: 'true'}") @TestParameters("{source: 'false == true', expected: 'false'}") + @TestParameters("{source: '[1, 2, 3].map(item, item * 2)', expected: '[2, 4, 6]'}") + @TestParameters("{source: '[1, 2, 3].filter(item, item > 1)', expected: '[2, 3]'}") + @TestParameters("{source: '[1, 2, 3].exists(item, item > 1)', expected: 'true'}") + @TestParameters("{source: '[1, 2, 3].all(item, item > 1)', expected: 'false'}") + @TestParameters("{source: '{\"a\": 1}.all(k, v, k == \"a\" || v > 0)', expected: 'true'}") + @TestParameters( + "{source: '{\"a\": 1, \"b\": x}.all(k, v, k == \"a\" || v > 0)', expected: '{\"a\": 1, \"b\":" + + " x}.all(k, v, k == \"a\" || v > 0)'}") + @TestParameters("{source: '[1, 2].map(x, [3, 4].map(y, x + y))', expected: '[[4, 5], [5, 6]]'}") + @TestParameters( + "{source: '[1, 2, x].map(item, item * 2)', expected: '[1, 2, x].map(item, item * 2)'}") + @TestParameters("{source: '[[1, 2], [3, 4]].map(x, x[0] == 1)', expected: '[true, false]'}") + @TestParameters("{source: '[[1, 2]].map(list_var, list_var[0])', expected: '[1]'}") + @TestParameters("{source: '[{\"a\": 1}].map(x, x.a == 1)', expected: '[true]'}") + @TestParameters( + "{source: '[1, 2, x].map(item, item * 2)', expected: '[1, 2, x].map(item, item * 2)'}") @TestParameters("{source: 'false == false', expected: 'true'}") @TestParameters("{source: '10 == 42', expected: 'false'}") @TestParameters("{source: '42 == 42', expected: 'true'}") @@ -561,10 +578,7 @@ public void iterationLimitReached_throws() throws Exception { Cel cel = runtimeFlavor .builder() - .setOptions( - CelOptions.current() - .enableHeterogeneousNumericComparisons(true) - .build()) + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) .build(); CelAbstractSyntaxTree ast = cel.compile("1 + 1").getAst(); CelOptimizer optimizer =