Skip to content

Commit 76f5f5d

Browse files
l46kokcopybara-github
authored andcommitted
Add canonicalization for two-variable comprehensions
PiperOrigin-RevId: 961063857
1 parent d459cc9 commit 76f5f5d

15 files changed

Lines changed: 1145 additions & 491 deletions

common/src/main/java/dev/cel/common/navigation/CelNavigableExprUtil.java

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,58 @@
2727
@CheckReturnValue
2828
public final class CelNavigableExprUtil {
2929

30+
/**
31+
* Returns the nearest enclosing comprehension that declares {@code variableName} in scope for
32+
* {@code expr}, or {@code Optional.empty()} if none exists.
33+
*
34+
* <p>A comprehension declares {@code variableName} in scope for {@code expr} if {@code
35+
* variableName} matches {@code iterVar}, {@code iterVar2}, or {@code accuVar}, and {@code expr}
36+
* resides within the branch where that variable is active:
37+
*
38+
* <ul>
39+
* <li>In {@code loopCondition} and {@code loopStep}: {@code iterVar}, {@code iterVar2}, and
40+
* {@code accuVar} are in scope.
41+
* <li>In {@code result}: only {@code accuVar} is in scope.
42+
* <li>In {@code iterRange} and {@code accuInit}: none of the comprehension variables are in
43+
* scope.
44+
* </ul>
45+
*/
46+
@SuppressWarnings("ReferenceEquality") // Required to disambiguate child branches
47+
public static <E extends Expression, T extends BaseNavigableExpr<E>>
48+
Optional<T> findDeclaringComprehension(T expr, String variableName) {
49+
checkNotNull(expr);
50+
checkNotNull(variableName);
51+
if (variableName.isEmpty()) {
52+
return Optional.empty();
53+
}
54+
T curr = expr;
55+
Optional<T> maybeParent = curr.parent();
56+
while (maybeParent.isPresent()) {
57+
T parent = maybeParent.get();
58+
if (parent.getKind() == Kind.COMPREHENSION) {
59+
Expression.Comprehension<?> comp = parent.expr().comprehension();
60+
Expression currExpr = curr.expr();
61+
62+
if (currExpr != comp.iterRange() && currExpr != comp.accuInit()) {
63+
if (currExpr == comp.result()) {
64+
if (comp.accuVar().equals(variableName)) {
65+
return Optional.of(parent);
66+
}
67+
} else {
68+
if (comp.iterVar().equals(variableName)
69+
|| comp.iterVar2().equals(variableName)
70+
|| comp.accuVar().equals(variableName)) {
71+
return Optional.of(parent);
72+
}
73+
}
74+
}
75+
}
76+
curr = parent;
77+
maybeParent = parent.parent();
78+
}
79+
return Optional.empty();
80+
}
81+
3082
/**
3183
* Returns true if {@code variableName} is in scope and shadowed by an enclosing comprehension
3284
* above {@code expr}.
@@ -56,7 +108,7 @@ public final class CelNavigableExprUtil {
56108
* </ul>
57109
*/
58110
public static boolean isVariableShadowed(BaseNavigableExpr<?> expr, String variableName) {
59-
return areVariablesShadowed(expr, Collections.singleton(variableName));
111+
return findDeclaringComprehension(expr, variableName).isPresent();
60112
}
61113

62114
/**
@@ -72,38 +124,14 @@ public static boolean isVariableShadowed(BaseNavigableExpr<?> expr, String varia
72124
* At {@code y > 0}, {@code areVariablesShadowed(node, ImmutableSet.of("x", "z"))} is {@code true}
73125
* because {@code x} is in scope from the outer comprehension.
74126
*/
75-
@SuppressWarnings("ReferenceEquality") // Required to disambiguate child branches
76127
public static boolean areVariablesShadowed(
77128
BaseNavigableExpr<?> expr, Collection<String> variableNames) {
78129
checkNotNull(expr);
79130
checkNotNull(variableNames);
80-
if (variableNames.isEmpty()) {
81-
return false;
82-
}
83-
BaseNavigableExpr<?> curr = expr;
84-
Optional<? extends BaseNavigableExpr<?>> maybeParent = curr.parent();
85-
while (maybeParent.isPresent()) {
86-
BaseNavigableExpr<?> parent = maybeParent.get();
87-
if (parent.getKind() == Kind.COMPREHENSION) {
88-
Expression.Comprehension<?> comp = parent.expr().comprehension();
89-
Expression currExpr = curr.expr();
90-
91-
if (currExpr != comp.iterRange() && currExpr != comp.accuInit()) {
92-
if (currExpr == comp.result()) {
93-
if (variableNames.contains(comp.accuVar())) {
94-
return true;
95-
}
96-
} else {
97-
if (variableNames.contains(comp.iterVar())
98-
|| variableNames.contains(comp.iterVar2())
99-
|| variableNames.contains(comp.accuVar())) {
100-
return true;
101-
}
102-
}
103-
}
131+
for (String varName : variableNames) {
132+
if (findDeclaringComprehension(expr, varName).isPresent()) {
133+
return true;
104134
}
105-
curr = parent;
106-
maybeParent = parent.parent();
107135
}
108136
return false;
109137
}

common/src/test/java/dev/cel/common/navigation/CelNavigableExprUtilTest.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,4 +345,64 @@ public void isVariableShadowed_zeroedOutIds_scopedCorrectly() {
345345
assertThat(CelNavigableExprUtil.isVariableShadowed(navResult, "x")).isFalse();
346346
assertThat(CelNavigableExprUtil.isVariableShadowed(navResult, "accu")).isTrue();
347347
}
348+
349+
@Test
350+
public void
351+
findDeclaringComprehension_nestedComprehensions_resolvesToInnermostDeclaringComprehension()
352+
throws Exception {
353+
CelAbstractSyntaxTree ast =
354+
COMPILER
355+
.compile("[1, 2].all(x, {'k': 1}.exists(k, v, x > 0 && k != '' && v > 0))")
356+
.getAst();
357+
CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast);
358+
359+
CelNavigableExpr outerComp =
360+
navigableAst
361+
.getRoot()
362+
.allNodes()
363+
.filter(
364+
node ->
365+
node.getKind() == Kind.COMPREHENSION
366+
&& node.expr().comprehension().iterVar().equals("x"))
367+
.findFirst()
368+
.get();
369+
370+
CelNavigableExpr innerComp =
371+
navigableAst
372+
.getRoot()
373+
.allNodes()
374+
.filter(
375+
node ->
376+
node.getKind() == Kind.COMPREHENSION
377+
&& node.expr().comprehension().iterVar().equals("k"))
378+
.findFirst()
379+
.get();
380+
381+
CelNavigableExpr identX =
382+
navigableAst
383+
.getRoot()
384+
.allNodes()
385+
.filter(node -> node.expr().identOrDefault().name().equals("x"))
386+
.findFirst()
387+
.get();
388+
CelNavigableExpr identK =
389+
navigableAst
390+
.getRoot()
391+
.allNodes()
392+
.filter(node -> node.expr().identOrDefault().name().equals("k"))
393+
.findFirst()
394+
.get();
395+
CelNavigableExpr identV =
396+
navigableAst
397+
.getRoot()
398+
.allNodes()
399+
.filter(node -> node.expr().identOrDefault().name().equals("v"))
400+
.findFirst()
401+
.get();
402+
403+
assertThat(CelNavigableExprUtil.findDeclaringComprehension(identX, "x")).hasValue(outerComp);
404+
assertThat(CelNavigableExprUtil.findDeclaringComprehension(identK, "k")).hasValue(innerComp);
405+
assertThat(CelNavigableExprUtil.findDeclaringComprehension(identV, "v")).hasValue(innerComp);
406+
assertThat(CelNavigableExprUtil.findDeclaringComprehension(identX, "unknown")).isEmpty();
407+
}
348408
}

verifier/src/main/java/dev/cel/verifier/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,13 @@ java_library(
140140
"//common/ast",
141141
"//common/ast:mutable_expr",
142142
"//common/navigation:common",
143+
"//common/navigation:expr_util",
143144
"//common/navigation:mutable_navigation",
144145
"//common/values:cel_byte_string",
145146
"//optimizer:ast_optimizer",
146147
"//optimizer:mutable_ast",
147148
"@maven//:com_google_guava_guava",
149+
"@maven//:org_jspecify_jspecify",
148150
],
149151
)
150152

0 commit comments

Comments
 (0)