From 307269343fee75d77bb72f08cee111dfd500fca2 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 4 Dec 2025 21:14:14 -0800 Subject: [PATCH] Persist lazily bound variables in the correct scoped resolver PiperOrigin-RevId: 840535220 --- .../extensions/CelBindingsExtensionsTest.java | 73 ++++ .../optimizers/SubexpressionOptimizer.java | 118 +++++- .../SubexpressionOptimizerTest.java | 32 +- ...ssion_ast_block_recursion_depth_4.baseline | 360 +++++++++--------- ...ssion_ast_block_recursion_depth_5.baseline | 360 +++++++++--------- ...ssion_ast_block_recursion_depth_6.baseline | 360 +++++++++--------- .../resources/subexpression_unparsed.baseline | 12 +- .../dev/cel/runtime/DefaultInterpreter.java | 23 +- .../cel/runtime/RuntimeUnknownResolver.java | 29 +- 9 files changed, 825 insertions(+), 542 deletions(-) diff --git a/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java index 33dd9db39..bc98c9816 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java @@ -38,6 +38,7 @@ import dev.cel.runtime.CelRuntime; import dev.cel.runtime.CelRuntimeFactory; import java.util.Arrays; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; import org.junit.runner.RunWith; @@ -243,4 +244,76 @@ public void lazyBinding_withNestedBinds() throws Exception { assertThat(result).isTrue(); assertThat(invocation.get()).isEqualTo(2); } + + @Test + @SuppressWarnings({"Immutable", "unchecked"}) // Test only + public void lazyBinding_boundAttributeInComprehension() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.MAP) + .addLibraries(CelExtensions.bindings()) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "get_true", + CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL))) + .build(); + AtomicInteger invocation = new AtomicInteger(); + CelRuntime celRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addFunctionBindings( + CelFunctionBinding.from( + "get_true_overload", + ImmutableList.of(), + arg -> { + invocation.getAndIncrement(); + return true; + })) + .build(); + + CelAbstractSyntaxTree ast = + celCompiler.compile("cel.bind(x, get_true(), [1,2,3].map(y, y < 0 || x))").getAst(); + + List result = (List) celRuntime.createProgram(ast).eval(); + + assertThat(result).containsExactly(true, true, true); + assertThat(invocation.get()).isEqualTo(1); + } + + @Test + @SuppressWarnings({"Immutable"}) // Test only + public void lazyBinding_boundAttributeInNestedComprehension() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.EXISTS) + .addLibraries(CelExtensions.bindings()) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "get_true", + CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL))) + .build(); + AtomicInteger invocation = new AtomicInteger(); + CelRuntime celRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addFunctionBindings( + CelFunctionBinding.from( + "get_true_overload", + ImmutableList.of(), + arg -> { + invocation.getAndIncrement(); + return true; + })) + .build(); + + CelAbstractSyntaxTree ast = + celCompiler + .compile( + "cel.bind(x, get_true(), [1,2,3].exists(unused, x && " + + "['a','b','c'].exists(unused_2, x)))") + .getAst(); + + boolean result = (boolean) celRuntime.createProgram(ast).eval(); + + assertThat(result).isTrue(); + assertThat(invocation.get()).isEqualTo(1); + } } diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java index eceb0bbe1..46a063a8d 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java @@ -16,11 +16,13 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableSet.toImmutableSet; import static java.util.stream.Collectors.toCollection; import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -41,8 +43,11 @@ import dev.cel.common.CelVarDecl; import dev.cel.common.ast.CelExpr; import dev.cel.common.ast.CelExpr.CelCall; +import dev.cel.common.ast.CelExpr.CelComprehension; +import dev.cel.common.ast.CelExpr.CelList; import dev.cel.common.ast.CelExpr.ExprKind.Kind; import dev.cel.common.ast.CelMutableExpr; +import dev.cel.common.ast.CelMutableExpr.CelMutableComprehension; import dev.cel.common.ast.CelMutableExprConverter; import dev.cel.common.navigation.CelNavigableExpr; import dev.cel.common.navigation.CelNavigableMutableAst; @@ -60,6 +65,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Stream; /** * Performs Common Subexpression Elimination. @@ -90,14 +96,15 @@ public class SubexpressionOptimizer implements CelAstOptimizer { private static final SubexpressionOptimizer INSTANCE = new SubexpressionOptimizer(SubexpressionOptimizerOptions.newBuilder().build()); private static final String BIND_IDENTIFIER_PREFIX = "@r"; - private static final String MANGLED_COMPREHENSION_ITER_VAR_PREFIX = "@it"; - private static final String MANGLED_COMPREHENSION_ITER_VAR2_PREFIX = "@it2"; - private static final String MANGLED_COMPREHENSION_ACCU_VAR_PREFIX = "@ac"; private static final String CEL_BLOCK_FUNCTION = "cel.@block"; private static final String BLOCK_INDEX_PREFIX = "@index"; private static final Extension CEL_BLOCK_AST_EXTENSION_TAG = Extension.create("cel_block", Version.of(1L, 1L), Component.COMPONENT_RUNTIME); + @VisibleForTesting static final String MANGLED_COMPREHENSION_ITER_VAR_PREFIX = "@it"; + @VisibleForTesting static final String MANGLED_COMPREHENSION_ITER_VAR2_PREFIX = "@it2"; + @VisibleForTesting static final String MANGLED_COMPREHENSION_ACCU_VAR_PREFIX = "@ac"; + private final SubexpressionOptimizerOptions cseOptions; private final AstMutator astMutator; private final ImmutableSet cseEliminableFunctions; @@ -269,6 +276,8 @@ static void verifyOptimizedAstCorrectness(CelAbstractSyntaxTree ast) { Verify.verify( resultHasAtLeastOneBlockIndex, "Expected at least one reference of index in cel.block result"); + + verifyNoInvalidScopedMangledVariables(celBlockExpr); } private static void verifyBlockIndex(CelExpr celExpr, int maxIndexValue) { @@ -289,6 +298,67 @@ private static void verifyBlockIndex(CelExpr celExpr, int maxIndexValue) { celExpr); } + private static void verifyNoInvalidScopedMangledVariables(CelExpr celExpr) { + CelCall celBlockCall = celExpr.call(); + CelExpr blockBody = celBlockCall.args().get(1); + + ImmutableSet allMangledVariablesInBlockBody = + CelNavigableExpr.fromExpr(blockBody) + .allNodes() + .map(CelNavigableExpr::expr) + .flatMap(SubexpressionOptimizer::extractMangledNames) + .collect(toImmutableSet()); + + CelList blockIndices = celBlockCall.args().get(0).list(); + for (CelExpr blockIndex : blockIndices.elements()) { + ImmutableSet indexDeclaredCompVariables = + CelNavigableExpr.fromExpr(blockIndex) + .allNodes() + .map(CelNavigableExpr::expr) + .filter(expr -> expr.getKind() == Kind.COMPREHENSION) + .map(CelExpr::comprehension) + .flatMap(comp -> Stream.of(comp.iterVar(), comp.iterVar2())) + .filter(iter -> !Strings.isNullOrEmpty(iter)) + .collect(toImmutableSet()); + + boolean containsIllegalDeclaration = + CelNavigableExpr.fromExpr(blockIndex) + .allNodes() + .map(CelNavigableExpr::expr) + .filter(expr -> expr.getKind() == Kind.IDENT) + .map(expr -> expr.ident().name()) + .filter(SubexpressionOptimizer::isMangled) + .anyMatch( + ident -> + !indexDeclaredCompVariables.contains(ident) + && allMangledVariablesInBlockBody.contains(ident)); + + Verify.verify( + !containsIllegalDeclaration, + "Illegal declared reference to a comprehension variable found in block indices. Expr: %s", + celExpr); + } + } + + private static Stream extractMangledNames(CelExpr expr) { + if (expr.getKind().equals(Kind.IDENT)) { + String name = expr.ident().name(); + return isMangled(name) ? Stream.of(name) : Stream.empty(); + } + if (expr.getKind().equals(Kind.COMPREHENSION)) { + CelComprehension comp = expr.comprehension(); + return Stream.of(comp.iterVar(), comp.iterVar2(), comp.accuVar()) + .filter(x -> !Strings.isNullOrEmpty(x)) + .filter(SubexpressionOptimizer::isMangled); + } + return Stream.empty(); + } + + private static boolean isMangled(String name) { + return name.startsWith(MANGLED_COMPREHENSION_ITER_VAR_PREFIX) + || name.startsWith(MANGLED_COMPREHENSION_ITER_VAR2_PREFIX); + } + private static CelAbstractSyntaxTree tagAstExtension(CelAbstractSyntaxTree ast) { // Tag the extension CelSource.Builder celSourceBuilder = @@ -355,8 +425,8 @@ private List getCseCandidatesWithRecursionDepth( navAst .getRoot() .descendants(TraversalOrder.PRE_ORDER) - .filter(node -> canEliminate(node, ineligibleExprs)) .filter(node -> node.height() <= recursionLimit) + .filter(node -> canEliminate(node, ineligibleExprs)) .sorted(Comparator.comparingInt(CelNavigableMutableExpr::height).reversed()) .collect(toImmutableList()); if (descendants.isEmpty()) { @@ -441,7 +511,45 @@ private boolean canEliminate( && navigableExpr.expr().list().elements().isEmpty()) && containsEliminableFunctionOnly(navigableExpr) && !ineligibleExprs.contains(navigableExpr.expr()) - && containsComprehensionIdentInSubexpr(navigableExpr); + && containsComprehensionIdentInSubexpr(navigableExpr) + && containsProperScopedComprehensionIdents(navigableExpr); + } + + private boolean containsProperScopedComprehensionIdents(CelNavigableMutableExpr navExpr) { + if (!navExpr.getKind().equals(Kind.COMPREHENSION)) { + return true; + } + + // For nested comprehensions of form [1].exists(x, [2].exists(y, x == y)), the inner + // comprehension [2].exists(y, x == y) + // should not be extracted out into a block index, as it causes issues with scoping. + ImmutableSet mangledIterVars = + navExpr + .descendants() + .filter(x -> x.getKind().equals(Kind.IDENT)) + .map(x -> x.expr().ident().name()) + .filter( + name -> + name.startsWith(MANGLED_COMPREHENSION_ITER_VAR_PREFIX) + || name.startsWith(MANGLED_COMPREHENSION_ITER_VAR2_PREFIX)) + .collect(toImmutableSet()); + + CelNavigableMutableExpr parent = navExpr.parent().orElse(null); + while (parent != null) { + if (parent.getKind().equals(Kind.COMPREHENSION)) { + CelMutableComprehension comp = parent.expr().comprehension(); + boolean containsParentIterReferences = + mangledIterVars.contains(comp.iterVar()) || mangledIterVars.contains(comp.iterVar2()); + + if (containsParentIterReferences) { + return false; + } + } + + parent = parent.parent().orElse(null); + } + + return true; } private boolean containsComprehensionIdentInSubexpr(CelNavigableMutableExpr navExpr) { diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java index c6999e46b..96bdf719e 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java @@ -55,6 +55,7 @@ import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelRuntime; import dev.cel.runtime.CelRuntimeFactory; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; import org.junit.runner.RunWith; @@ -381,6 +382,31 @@ public void lazyEval_blockIndexEvaluatedOnlyOnce() throws Exception { assertThat(invocation.get()).isEqualTo(1); } + @Test + @SuppressWarnings({"Immutable", "unchecked"}) // Test only + public void lazyEval_withinComprehension_blockIndexEvaluatedOnlyOnce() throws Exception { + AtomicInteger invocation = new AtomicInteger(); + CelRuntime celRuntime = + CelRuntimeFactory.standardCelRuntimeBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .addFunctionBindings( + CelFunctionBinding.from( + "get_true_overload", + ImmutableList.of(), + arg -> { + invocation.getAndIncrement(); + return true; + })) + .build(); + CelAbstractSyntaxTree ast = + compileUsingInternalFunctions("cel.block([get_true()], [1,2,3].map(x, x < 0 || index0))"); + + List result = (List) celRuntime.createProgram(ast).eval(); + + assertThat(result).containsExactly(true, true, true); + assertThat(invocation.get()).isEqualTo(1); + } + @Test @SuppressWarnings("Immutable") // Test only public void lazyEval_multipleBlockIndices_inResultExpr() throws Exception { @@ -452,9 +478,9 @@ public void lazyEval_nestedComprehension_indexReferencedInNestedScopes() throws // Equivalent of [true, false, true].map(c0, [c0].map(c1, [c0, c1, true])) CelAbstractSyntaxTree ast = compileUsingInternalFunctions( - "cel.block([c0, c1, get_true()], [index2, false, index2].map(c0, [c0].map(c1, [index0," - + " index1, index2]))) == [[[true, true, true]], [[false, false, true]], [[true," - + " true, true]]]"); + "cel.block([true, false, get_true()], [index2, false, index2].map(c0, [c0].map(c1, [c0," + + " c1, index2]))) == [[[true, true, true]], [[false, false, true]], [[true, true," + + " true]]]"); boolean result = (boolean) celRuntime.createProgram(ast).eval(); diff --git a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_4.baseline b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_4.baseline index c3de5d723..2159ae348 100644 --- a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_4.baseline +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_4.baseline @@ -2280,105 +2280,122 @@ CALL [1] { args: { LIST [2] { elements: { - COMPREHENSION [3] { - iter_var: @it:1:0 - iter_range: { + LIST [3] { + elements: { LIST [4] { elements: { CONSTANT [5] { value: 1 } - CONSTANT [6] { value: 2 } - CONSTANT [7] { value: 3 } } } - } - accu_var: @ac:1:0 - accu_init: { - LIST [8] { + LIST [6] { elements: { + CONSTANT [7] { value: 2 } } } } - loop_condition: { - CONSTANT [9] { value: true } - } - loop_step: { - CALL [10] { - function: _?_:_ - args: { - CALL [11] { - function: _==_ - args: { - IDENT [12] { - name: @it:1:0 - } - IDENT [13] { - name: @it:0:0 - } - } - } - CALL [14] { - function: _+_ - args: { - IDENT [15] { - name: @ac:1:0 - } - LIST [16] { - elements: { - IDENT [17] { - name: @it:1:0 - } - } - } - } - } - IDENT [18] { - name: @ac:1:0 - } - } - } + } + LIST [8] { + elements: { + CONSTANT [9] { value: 1 } + CONSTANT [10] { value: 2 } } - result: { - IDENT [19] { - name: @ac:1:0 - } + } + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } } } } } - CALL [20] { + CALL [15] { function: _==_ args: { - COMPREHENSION [21] { + COMPREHENSION [16] { iter_var: @it:0:0 iter_range: { - LIST [22] { - elements: { - CONSTANT [23] { value: 1 } - CONSTANT [24] { value: 2 } - } + IDENT [17] { + name: @index1 } } accu_var: @ac:0:0 accu_init: { - LIST [25] { + LIST [18] { elements: { } } } loop_condition: { - CONSTANT [26] { value: true } + CONSTANT [19] { value: true } } loop_step: { - CALL [27] { + CALL [20] { function: _+_ args: { - IDENT [28] { + IDENT [21] { name: @ac:0:0 } - LIST [29] { + LIST [22] { elements: { - IDENT [30] { - name: @index0 + COMPREHENSION [23] { + iter_var: @it:1:0 + iter_range: { + IDENT [24] { + name: @index2 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _?_:_ + args: { + CALL [28] { + function: _==_ + args: { + IDENT [29] { + name: @it:1:0 + } + IDENT [30] { + name: @it:0:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @ac:1:0 + } + LIST [33] { + elements: { + IDENT [34] { + name: @it:1:0 + } + } + } + } + } + IDENT [35] { + name: @ac:1:0 + } + } + } + } + result: { + IDENT [36] { + name: @ac:1:0 + } + } } } } @@ -2386,24 +2403,13 @@ CALL [1] { } } result: { - IDENT [31] { + IDENT [37] { name: @ac:0:0 } } } - LIST [32] { - elements: { - LIST [33] { - elements: { - CONSTANT [34] { value: 1 } - } - } - LIST [35] { - elements: { - CONSTANT [36] { value: 2 } - } - } - } + IDENT [38] { + name: @index0 } } } @@ -2557,122 +2563,139 @@ CALL [1] { args: { LIST [2] { elements: { - COMPREHENSION [3] { - iter_var: @it:1:0 - iter_range: { + LIST [3] { + elements: { LIST [4] { elements: { CONSTANT [5] { value: 1 } - CONSTANT [6] { value: 2 } - CONSTANT [7] { value: 3 } } } - } - accu_var: @ac:1:0 - accu_init: { - LIST [8] { + LIST [6] { elements: { + CONSTANT [7] { value: 2 } } } } - loop_condition: { - CONSTANT [9] { value: true } - } - loop_step: { - CALL [10] { - function: _?_:_ - args: { - CALL [11] { - function: _&&_ - args: { - CALL [12] { - function: _==_ - args: { - IDENT [13] { - name: @it:1:0 - } - IDENT [14] { - name: @it2:0:0 - } - } - } - CALL [15] { - function: _<_ - args: { - IDENT [16] { - name: @it:0:0 - } - IDENT [17] { - name: @it2:0:0 - } - } - } - } - } - CALL [18] { - function: _+_ - args: { - IDENT [19] { - name: @ac:1:0 - } - LIST [20] { - elements: { - IDENT [21] { - name: @it:1:0 - } - } - } - } - } - IDENT [22] { - name: @ac:1:0 - } - } - } + } + LIST [8] { + elements: { + CONSTANT [9] { value: 1 } + CONSTANT [10] { value: 2 } } - result: { - IDENT [23] { - name: @ac:1:0 - } + } + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } } } } } - CALL [24] { + CALL [15] { function: _==_ args: { - COMPREHENSION [25] { + COMPREHENSION [16] { iter_var: @it:0:0 iter_var2: @it2:0:0 iter_range: { - LIST [26] { - elements: { - CONSTANT [27] { value: 1 } - CONSTANT [28] { value: 2 } - } + IDENT [17] { + name: @index1 } } accu_var: @ac:0:0 accu_init: { - LIST [29] { + LIST [18] { elements: { } } } loop_condition: { - CONSTANT [30] { value: true } + CONSTANT [19] { value: true } } loop_step: { - CALL [31] { + CALL [20] { function: _+_ args: { - IDENT [32] { + IDENT [21] { name: @ac:0:0 } - LIST [33] { + LIST [22] { elements: { - IDENT [34] { - name: @index0 + COMPREHENSION [23] { + iter_var: @it:1:0 + iter_range: { + IDENT [24] { + name: @index2 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _?_:_ + args: { + CALL [28] { + function: _&&_ + args: { + CALL [29] { + function: _==_ + args: { + IDENT [30] { + name: @it:1:0 + } + IDENT [31] { + name: @it2:0:0 + } + } + } + CALL [32] { + function: _<_ + args: { + IDENT [33] { + name: @it:0:0 + } + IDENT [34] { + name: @it2:0:0 + } + } + } + } + } + CALL [35] { + function: _+_ + args: { + IDENT [36] { + name: @ac:1:0 + } + LIST [37] { + elements: { + IDENT [38] { + name: @it:1:0 + } + } + } + } + } + IDENT [39] { + name: @ac:1:0 + } + } + } + } + result: { + IDENT [40] { + name: @ac:1:0 + } + } } } } @@ -2680,24 +2703,13 @@ CALL [1] { } } result: { - IDENT [35] { + IDENT [41] { name: @ac:0:0 } } } - LIST [36] { - elements: { - LIST [37] { - elements: { - CONSTANT [38] { value: 1 } - } - } - LIST [39] { - elements: { - CONSTANT [40] { value: 2 } - } - } - } + IDENT [42] { + name: @index0 } } } diff --git a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_5.baseline b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_5.baseline index e9797057a..37a3d60a8 100644 --- a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_5.baseline +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_5.baseline @@ -2262,105 +2262,122 @@ CALL [1] { args: { LIST [2] { elements: { - COMPREHENSION [3] { - iter_var: @it:1:0 - iter_range: { + LIST [3] { + elements: { LIST [4] { elements: { CONSTANT [5] { value: 1 } - CONSTANT [6] { value: 2 } - CONSTANT [7] { value: 3 } } } - } - accu_var: @ac:1:0 - accu_init: { - LIST [8] { + LIST [6] { elements: { + CONSTANT [7] { value: 2 } } } } - loop_condition: { - CONSTANT [9] { value: true } - } - loop_step: { - CALL [10] { - function: _?_:_ - args: { - CALL [11] { - function: _==_ - args: { - IDENT [12] { - name: @it:1:0 - } - IDENT [13] { - name: @it:0:0 - } - } - } - CALL [14] { - function: _+_ - args: { - IDENT [15] { - name: @ac:1:0 - } - LIST [16] { - elements: { - IDENT [17] { - name: @it:1:0 - } - } - } - } - } - IDENT [18] { - name: @ac:1:0 - } - } - } + } + LIST [8] { + elements: { + CONSTANT [9] { value: 1 } + CONSTANT [10] { value: 2 } } - result: { - IDENT [19] { - name: @ac:1:0 - } + } + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } } } } } - CALL [20] { + CALL [15] { function: _==_ args: { - COMPREHENSION [21] { + COMPREHENSION [16] { iter_var: @it:0:0 iter_range: { - LIST [22] { - elements: { - CONSTANT [23] { value: 1 } - CONSTANT [24] { value: 2 } - } + IDENT [17] { + name: @index1 } } accu_var: @ac:0:0 accu_init: { - LIST [25] { + LIST [18] { elements: { } } } loop_condition: { - CONSTANT [26] { value: true } + CONSTANT [19] { value: true } } loop_step: { - CALL [27] { + CALL [20] { function: _+_ args: { - IDENT [28] { + IDENT [21] { name: @ac:0:0 } - LIST [29] { + LIST [22] { elements: { - IDENT [30] { - name: @index0 + COMPREHENSION [23] { + iter_var: @it:1:0 + iter_range: { + IDENT [24] { + name: @index2 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _?_:_ + args: { + CALL [28] { + function: _==_ + args: { + IDENT [29] { + name: @it:1:0 + } + IDENT [30] { + name: @it:0:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @ac:1:0 + } + LIST [33] { + elements: { + IDENT [34] { + name: @it:1:0 + } + } + } + } + } + IDENT [35] { + name: @ac:1:0 + } + } + } + } + result: { + IDENT [36] { + name: @ac:1:0 + } + } } } } @@ -2368,24 +2385,13 @@ CALL [1] { } } result: { - IDENT [31] { + IDENT [37] { name: @ac:0:0 } } } - LIST [32] { - elements: { - LIST [33] { - elements: { - CONSTANT [34] { value: 1 } - } - } - LIST [35] { - elements: { - CONSTANT [36] { value: 2 } - } - } - } + IDENT [38] { + name: @index0 } } } @@ -2539,122 +2545,139 @@ CALL [1] { args: { LIST [2] { elements: { - COMPREHENSION [3] { - iter_var: @it:1:0 - iter_range: { + LIST [3] { + elements: { LIST [4] { elements: { CONSTANT [5] { value: 1 } - CONSTANT [6] { value: 2 } - CONSTANT [7] { value: 3 } } } - } - accu_var: @ac:1:0 - accu_init: { - LIST [8] { + LIST [6] { elements: { + CONSTANT [7] { value: 2 } } } } - loop_condition: { - CONSTANT [9] { value: true } - } - loop_step: { - CALL [10] { - function: _?_:_ - args: { - CALL [11] { - function: _&&_ - args: { - CALL [12] { - function: _==_ - args: { - IDENT [13] { - name: @it:1:0 - } - IDENT [14] { - name: @it2:0:0 - } - } - } - CALL [15] { - function: _<_ - args: { - IDENT [16] { - name: @it:0:0 - } - IDENT [17] { - name: @it2:0:0 - } - } - } - } - } - CALL [18] { - function: _+_ - args: { - IDENT [19] { - name: @ac:1:0 - } - LIST [20] { - elements: { - IDENT [21] { - name: @it:1:0 - } - } - } - } - } - IDENT [22] { - name: @ac:1:0 - } - } - } + } + LIST [8] { + elements: { + CONSTANT [9] { value: 1 } + CONSTANT [10] { value: 2 } } - result: { - IDENT [23] { - name: @ac:1:0 - } + } + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } } } } } - CALL [24] { + CALL [15] { function: _==_ args: { - COMPREHENSION [25] { + COMPREHENSION [16] { iter_var: @it:0:0 iter_var2: @it2:0:0 iter_range: { - LIST [26] { - elements: { - CONSTANT [27] { value: 1 } - CONSTANT [28] { value: 2 } - } + IDENT [17] { + name: @index1 } } accu_var: @ac:0:0 accu_init: { - LIST [29] { + LIST [18] { elements: { } } } loop_condition: { - CONSTANT [30] { value: true } + CONSTANT [19] { value: true } } loop_step: { - CALL [31] { + CALL [20] { function: _+_ args: { - IDENT [32] { + IDENT [21] { name: @ac:0:0 } - LIST [33] { + LIST [22] { elements: { - IDENT [34] { - name: @index0 + COMPREHENSION [23] { + iter_var: @it:1:0 + iter_range: { + IDENT [24] { + name: @index2 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _?_:_ + args: { + CALL [28] { + function: _&&_ + args: { + CALL [29] { + function: _==_ + args: { + IDENT [30] { + name: @it:1:0 + } + IDENT [31] { + name: @it2:0:0 + } + } + } + CALL [32] { + function: _<_ + args: { + IDENT [33] { + name: @it:0:0 + } + IDENT [34] { + name: @it2:0:0 + } + } + } + } + } + CALL [35] { + function: _+_ + args: { + IDENT [36] { + name: @ac:1:0 + } + LIST [37] { + elements: { + IDENT [38] { + name: @it:1:0 + } + } + } + } + } + IDENT [39] { + name: @ac:1:0 + } + } + } + } + result: { + IDENT [40] { + name: @ac:1:0 + } + } } } } @@ -2662,24 +2685,13 @@ CALL [1] { } } result: { - IDENT [35] { + IDENT [41] { name: @ac:0:0 } } } - LIST [36] { - elements: { - LIST [37] { - elements: { - CONSTANT [38] { value: 1 } - } - } - LIST [39] { - elements: { - CONSTANT [40] { value: 2 } - } - } - } + IDENT [42] { + name: @index0 } } } diff --git a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_6.baseline b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_6.baseline index d220cfff4..420c6d01b 100644 --- a/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_6.baseline +++ b/optimizer/src/test/resources/subexpression_ast_block_recursion_depth_6.baseline @@ -2256,105 +2256,122 @@ CALL [1] { args: { LIST [2] { elements: { - COMPREHENSION [3] { - iter_var: @it:1:0 - iter_range: { + LIST [3] { + elements: { LIST [4] { elements: { CONSTANT [5] { value: 1 } - CONSTANT [6] { value: 2 } - CONSTANT [7] { value: 3 } } } - } - accu_var: @ac:1:0 - accu_init: { - LIST [8] { + LIST [6] { elements: { + CONSTANT [7] { value: 2 } } } } - loop_condition: { - CONSTANT [9] { value: true } - } - loop_step: { - CALL [10] { - function: _?_:_ - args: { - CALL [11] { - function: _==_ - args: { - IDENT [12] { - name: @it:1:0 - } - IDENT [13] { - name: @it:0:0 - } - } - } - CALL [14] { - function: _+_ - args: { - IDENT [15] { - name: @ac:1:0 - } - LIST [16] { - elements: { - IDENT [17] { - name: @it:1:0 - } - } - } - } - } - IDENT [18] { - name: @ac:1:0 - } - } - } + } + LIST [8] { + elements: { + CONSTANT [9] { value: 1 } + CONSTANT [10] { value: 2 } } - result: { - IDENT [19] { - name: @ac:1:0 - } + } + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } } } } } - CALL [20] { + CALL [15] { function: _==_ args: { - COMPREHENSION [21] { + COMPREHENSION [16] { iter_var: @it:0:0 iter_range: { - LIST [22] { - elements: { - CONSTANT [23] { value: 1 } - CONSTANT [24] { value: 2 } - } + IDENT [17] { + name: @index1 } } accu_var: @ac:0:0 accu_init: { - LIST [25] { + LIST [18] { elements: { } } } loop_condition: { - CONSTANT [26] { value: true } + CONSTANT [19] { value: true } } loop_step: { - CALL [27] { + CALL [20] { function: _+_ args: { - IDENT [28] { + IDENT [21] { name: @ac:0:0 } - LIST [29] { + LIST [22] { elements: { - IDENT [30] { - name: @index0 + COMPREHENSION [23] { + iter_var: @it:1:0 + iter_range: { + IDENT [24] { + name: @index2 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _?_:_ + args: { + CALL [28] { + function: _==_ + args: { + IDENT [29] { + name: @it:1:0 + } + IDENT [30] { + name: @it:0:0 + } + } + } + CALL [31] { + function: _+_ + args: { + IDENT [32] { + name: @ac:1:0 + } + LIST [33] { + elements: { + IDENT [34] { + name: @it:1:0 + } + } + } + } + } + IDENT [35] { + name: @ac:1:0 + } + } + } + } + result: { + IDENT [36] { + name: @ac:1:0 + } + } } } } @@ -2362,24 +2379,13 @@ CALL [1] { } } result: { - IDENT [31] { + IDENT [37] { name: @ac:0:0 } } } - LIST [32] { - elements: { - LIST [33] { - elements: { - CONSTANT [34] { value: 1 } - } - } - LIST [35] { - elements: { - CONSTANT [36] { value: 2 } - } - } - } + IDENT [38] { + name: @index0 } } } @@ -2533,122 +2539,139 @@ CALL [1] { args: { LIST [2] { elements: { - COMPREHENSION [3] { - iter_var: @it:1:0 - iter_range: { + LIST [3] { + elements: { LIST [4] { elements: { CONSTANT [5] { value: 1 } - CONSTANT [6] { value: 2 } - CONSTANT [7] { value: 3 } } } - } - accu_var: @ac:1:0 - accu_init: { - LIST [8] { + LIST [6] { elements: { + CONSTANT [7] { value: 2 } } } } - loop_condition: { - CONSTANT [9] { value: true } - } - loop_step: { - CALL [10] { - function: _?_:_ - args: { - CALL [11] { - function: _&&_ - args: { - CALL [12] { - function: _==_ - args: { - IDENT [13] { - name: @it:1:0 - } - IDENT [14] { - name: @it2:0:0 - } - } - } - CALL [15] { - function: _<_ - args: { - IDENT [16] { - name: @it:0:0 - } - IDENT [17] { - name: @it2:0:0 - } - } - } - } - } - CALL [18] { - function: _+_ - args: { - IDENT [19] { - name: @ac:1:0 - } - LIST [20] { - elements: { - IDENT [21] { - name: @it:1:0 - } - } - } - } - } - IDENT [22] { - name: @ac:1:0 - } - } - } + } + LIST [8] { + elements: { + CONSTANT [9] { value: 1 } + CONSTANT [10] { value: 2 } } - result: { - IDENT [23] { - name: @ac:1:0 - } + } + LIST [11] { + elements: { + CONSTANT [12] { value: 1 } + CONSTANT [13] { value: 2 } + CONSTANT [14] { value: 3 } } } } } - CALL [24] { + CALL [15] { function: _==_ args: { - COMPREHENSION [25] { + COMPREHENSION [16] { iter_var: @it:0:0 iter_var2: @it2:0:0 iter_range: { - LIST [26] { - elements: { - CONSTANT [27] { value: 1 } - CONSTANT [28] { value: 2 } - } + IDENT [17] { + name: @index1 } } accu_var: @ac:0:0 accu_init: { - LIST [29] { + LIST [18] { elements: { } } } loop_condition: { - CONSTANT [30] { value: true } + CONSTANT [19] { value: true } } loop_step: { - CALL [31] { + CALL [20] { function: _+_ args: { - IDENT [32] { + IDENT [21] { name: @ac:0:0 } - LIST [33] { + LIST [22] { elements: { - IDENT [34] { - name: @index0 + COMPREHENSION [23] { + iter_var: @it:1:0 + iter_range: { + IDENT [24] { + name: @index2 + } + } + accu_var: @ac:1:0 + accu_init: { + LIST [25] { + elements: { + } + } + } + loop_condition: { + CONSTANT [26] { value: true } + } + loop_step: { + CALL [27] { + function: _?_:_ + args: { + CALL [28] { + function: _&&_ + args: { + CALL [29] { + function: _==_ + args: { + IDENT [30] { + name: @it:1:0 + } + IDENT [31] { + name: @it2:0:0 + } + } + } + CALL [32] { + function: _<_ + args: { + IDENT [33] { + name: @it:0:0 + } + IDENT [34] { + name: @it2:0:0 + } + } + } + } + } + CALL [35] { + function: _+_ + args: { + IDENT [36] { + name: @ac:1:0 + } + LIST [37] { + elements: { + IDENT [38] { + name: @it:1:0 + } + } + } + } + } + IDENT [39] { + name: @ac:1:0 + } + } + } + } + result: { + IDENT [40] { + name: @ac:1:0 + } + } } } } @@ -2656,24 +2679,13 @@ CALL [1] { } } result: { - IDENT [35] { + IDENT [41] { name: @ac:0:0 } } } - LIST [36] { - elements: { - LIST [37] { - elements: { - CONSTANT [38] { value: 1 } - } - } - LIST [39] { - elements: { - CONSTANT [40] { value: 2 } - } - } - } + IDENT [42] { + name: @index0 } } } diff --git a/optimizer/src/test/resources/subexpression_unparsed.baseline b/optimizer/src/test/resources/subexpression_unparsed.baseline index 684c0ccb5..892a6fbe5 100644 --- a/optimizer/src/test/resources/subexpression_unparsed.baseline +++ b/optimizer/src/test/resources/subexpression_unparsed.baseline @@ -351,9 +351,9 @@ Result: true [BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2], [1, 2, 3], [1], [2], [@index2, @index3]], @index0.map(@it:0:0, @index1.filter(@it:1:0, @it:1:0 == @it:0:0)) == @index4) [BLOCK_RECURSION_DEPTH_2]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.map(@it:0:0, @index2.filter(@it:1:0, @it:1:0 == @it:0:0)) == @index0) [BLOCK_RECURSION_DEPTH_3]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.map(@it:0:0, @index2.filter(@it:1:0, @it:1:0 == @it:0:0)) == @index0) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1, 2, 3].filter(@it:1:0, @it:1:0 == @it:0:0)], [1, 2].map(@it:0:0, @index0) == [[1], [2]]) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1, 2, 3].filter(@it:1:0, @it:1:0 == @it:0:0)], [1, 2].map(@it:0:0, @index0) == [[1], [2]]) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1, 2, 3].filter(@it:1:0, @it:1:0 == @it:0:0)], [1, 2].map(@it:0:0, @index0) == [[1], [2]]) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.map(@it:0:0, @index2.filter(@it:1:0, @it:1:0 == @it:0:0)) == @index0) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.map(@it:0:0, @index2.filter(@it:1:0, @it:1:0 == @it:0:0)) == @index0) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.map(@it:0:0, @index2.filter(@it:1:0, @it:1:0 == @it:0:0)) == @index0) [BLOCK_RECURSION_DEPTH_7]: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] [BLOCK_RECURSION_DEPTH_8]: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] [BLOCK_RECURSION_DEPTH_9]: [1, 2].map(y, [1, 2, 3].filter(x, x == y)) == [[1], [2]] @@ -381,9 +381,9 @@ Result: true [BLOCK_RECURSION_DEPTH_1]: cel.@block([[1, 2], [1, 2, 3], [1], [2], [@index2, @index3]], @index0.transformList(@it:0:0, @it2:0:0, @index1.filter(@it:1:0, @it:1:0 == @it2:0:0 && @it:0:0 < @it2:0:0)) == @index4) [BLOCK_RECURSION_DEPTH_2]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.transformList(@it:0:0, @it2:0:0, @index2.filter(@it:1:0, @it:1:0 == @it2:0:0 && @it:0:0 < @it2:0:0)) == @index0) [BLOCK_RECURSION_DEPTH_3]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.transformList(@it:0:0, @it2:0:0, @index2.filter(@it:1:0, @it:1:0 == @it2:0:0 && @it:0:0 < @it2:0:0)) == @index0) -[BLOCK_RECURSION_DEPTH_4]: cel.@block([[1, 2, 3].filter(@it:1:0, @it:1:0 == @it2:0:0 && @it:0:0 < @it2:0:0)], [1, 2].transformList(@it:0:0, @it2:0:0, @index0) == [[1], [2]]) -[BLOCK_RECURSION_DEPTH_5]: cel.@block([[1, 2, 3].filter(@it:1:0, @it:1:0 == @it2:0:0 && @it:0:0 < @it2:0:0)], [1, 2].transformList(@it:0:0, @it2:0:0, @index0) == [[1], [2]]) -[BLOCK_RECURSION_DEPTH_6]: cel.@block([[1, 2, 3].filter(@it:1:0, @it:1:0 == @it2:0:0 && @it:0:0 < @it2:0:0)], [1, 2].transformList(@it:0:0, @it2:0:0, @index0) == [[1], [2]]) +[BLOCK_RECURSION_DEPTH_4]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.transformList(@it:0:0, @it2:0:0, @index2.filter(@it:1:0, @it:1:0 == @it2:0:0 && @it:0:0 < @it2:0:0)) == @index0) +[BLOCK_RECURSION_DEPTH_5]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.transformList(@it:0:0, @it2:0:0, @index2.filter(@it:1:0, @it:1:0 == @it2:0:0 && @it:0:0 < @it2:0:0)) == @index0) +[BLOCK_RECURSION_DEPTH_6]: cel.@block([[[1], [2]], [1, 2], [1, 2, 3]], @index1.transformList(@it:0:0, @it2:0:0, @index2.filter(@it:1:0, @it:1:0 == @it2:0:0 && @it:0:0 < @it2:0:0)) == @index0) [BLOCK_RECURSION_DEPTH_7]: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] [BLOCK_RECURSION_DEPTH_8]: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] [BLOCK_RECURSION_DEPTH_9]: [1, 2].transformList(i, y, [1, 2, 3].filter(x, x == y && i < y)) == [[1], [2]] diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java index 546290a4e..c0a21cc3f 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java @@ -982,7 +982,8 @@ private IntermediateResult evalComprehension( .build(); } IntermediateResult accuValue; - if (LazyExpression.isLazilyEvaluable(compre)) { + boolean isLazilyEvaluable = LazyExpression.isLazilyEvaluable(compre); + if (isLazilyEvaluable) { accuValue = IntermediateResult.create(new LazyExpression(compre.accuInit())); } else { accuValue = evalNonstrictly(frame, compre.accuInit()); @@ -1035,7 +1036,13 @@ private IntermediateResult evalComprehension( accuValue = maybeAdaptViewToList(accuValue); - frame.pushScope(Collections.singletonMap(accuVar, accuValue)); + Map scopedAttributes = + Collections.singletonMap(accuVar, accuValue); + if (isLazilyEvaluable) { + frame.pushLazyScope(scopedAttributes); + } else { + frame.pushScope(scopedAttributes); + } IntermediateResult result; try { result = evalInternal(frame, compre.result()); @@ -1051,11 +1058,12 @@ private IntermediateResult evalCelBlock( Map blockList = new HashMap<>(); for (int index = 0; index < exprList.elements().size(); index++) { // Register the block indices as lazily evaluated expressions stored as unique identifiers. + String indexKey = "@index" + index; blockList.put( - "@index" + index, + indexKey, IntermediateResult.create(new LazyExpression(exprList.elements().get(index)))); } - frame.pushScope(Collections.unmodifiableMap(blockList)); + frame.pushLazyScope(Collections.unmodifiableMap(blockList)); return evalInternal(frame, blockCall.args().get(1)); } @@ -1167,6 +1175,13 @@ private void cacheLazilyEvaluatedResult( currentResolver.cacheLazilyEvaluatedResult(name, result); } + private void pushLazyScope(Map scope) { + pushScope(scope); + for (String lazyAttribute : scope.keySet()) { + currentResolver.declareLazyAttribute(lazyAttribute); + } + } + /** Note: we utilize a HashMap instead of ImmutableMap to make lookups faster on string keys. */ private void pushScope(Map scope) { scopeLevel++; diff --git a/runtime/src/main/java/dev/cel/runtime/RuntimeUnknownResolver.java b/runtime/src/main/java/dev/cel/runtime/RuntimeUnknownResolver.java index e9fb9d052..b28729417 100644 --- a/runtime/src/main/java/dev/cel/runtime/RuntimeUnknownResolver.java +++ b/runtime/src/main/java/dev/cel/runtime/RuntimeUnknownResolver.java @@ -116,7 +116,13 @@ DefaultInterpreter.IntermediateResult resolveSimpleName(String name, Long exprId } void cacheLazilyEvaluatedResult(String name, DefaultInterpreter.IntermediateResult result) { - // no-op. Caching is handled in ScopedResolver. + throw new IllegalStateException( + "Internal error: Lazy attributes can only be cached in ScopedResolver."); + } + + void declareLazyAttribute(String attrName) { + throw new IllegalStateException( + "Internal error: Lazy attributes can only be declared in ScopedResolver."); } /** @@ -161,7 +167,26 @@ DefaultInterpreter.IntermediateResult resolveSimpleName(String name, Long exprId @Override void cacheLazilyEvaluatedResult(String name, DefaultInterpreter.IntermediateResult result) { - lazyEvalResultCache.put(name, copyIfMutable(result)); + // Ensure that lazily evaluated result is stored at the proper scope. + // A lazily attribute is first declared when a new cel.bind/cel.block expr is encountered. + // + // If this attribute isn't found in the current scope, we need to walk up the parent scopes + // until we find this declaration. + // + // For example: cel.bind(x, get_true(), ['foo','bar'].map(unused, x && x)) + // + // Here, `x` would be evaluated in map macro's scope, but the result should be stored in + // cel.bind's scope. + if (!lazyEvalResultCache.containsKey(name)) { + parent.cacheLazilyEvaluatedResult(name, result); + } else { + lazyEvalResultCache.put(name, copyIfMutable(result)); + } + } + + @Override + void declareLazyAttribute(String attrName) { + lazyEvalResultCache.put(attrName, null); } /**