Skip to content

Commit b234aeb

Browse files
l46kokcopybara-github
authored andcommitted
Implement iterative eval for program planner
PiperOrigin-RevId: 964335064
1 parent a2353b3 commit b234aeb

29 files changed

Lines changed: 1905 additions & 180 deletions

bundle/src/test/java/dev/cel/bundle/CelImplTest.java

Lines changed: 244 additions & 77 deletions
Large diffs are not rendered by default.

extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage;
5151
import dev.cel.parser.CelMacro;
5252
import dev.cel.parser.CelStandardMacro;
53+
import dev.cel.runtime.CelAttribute.Qualifier;
5354
import dev.cel.runtime.CelAttributePattern;
5455
import dev.cel.runtime.CelEvaluationException;
5556
import dev.cel.runtime.CelFunctionBinding;
@@ -985,6 +986,132 @@ public void optionalIndex_onList_returnsOptionalValue() throws Exception {
985986
assertThat(result).isEqualTo(Optional.of("hello"));
986987
}
987988

989+
@Test
990+
public void optionalIndex_partialUnknownOnList_returnsUnknown() throws Exception {
991+
if (testMode.equals(TestMode.LEGACY_CHECKED)) {
992+
// Legacy runtime executes optional indexing through standard function bindings without
993+
// attribute
994+
// trail tracking, so it cannot intercept partial sub-attribute unknowns on known containers.
995+
return;
996+
}
997+
998+
Cel cel =
999+
newCelBuilder()
1000+
.addVar("l", ListType.create(SimpleType.STRING))
1001+
.setResultType(OptionalType.create(SimpleType.STRING))
1002+
.build();
1003+
CelAbstractSyntaxTree ast = compile(cel, "l[?1]");
1004+
PartialVars partialVars =
1005+
PartialVars.of(
1006+
ImmutableMap.of("l", ImmutableList.of("hello", "world")),
1007+
CelAttributePattern.fromQualifiedIdentifier("l").qualify(Qualifier.ofInt(1)));
1008+
1009+
Object result = cel.createProgram(ast).eval(partialVars);
1010+
1011+
assertThat(result).isInstanceOf(CelUnknownSet.class);
1012+
}
1013+
1014+
@Test
1015+
public void optionalIndex_partialUnknownOnList_unrelatedIndexEvaluatesNormally()
1016+
throws Exception {
1017+
if (testMode.equals(TestMode.LEGACY_CHECKED)) {
1018+
// Legacy runtime executes optional indexing through standard function bindings without
1019+
// attribute
1020+
// trail tracking, so it cannot intercept partial sub-attribute unknowns on known containers.
1021+
return;
1022+
}
1023+
1024+
Cel cel =
1025+
newCelBuilder()
1026+
.addVar("l", ListType.create(SimpleType.STRING))
1027+
.setResultType(OptionalType.create(SimpleType.STRING))
1028+
.build();
1029+
CelAbstractSyntaxTree ast = compile(cel, "l[?0]");
1030+
PartialVars partialVars =
1031+
PartialVars.of(
1032+
ImmutableMap.of("l", ImmutableList.of("hello", "world")),
1033+
CelAttributePattern.fromQualifiedIdentifier("l").qualify(Qualifier.ofInt(1)));
1034+
1035+
Object result = cel.createProgram(ast).eval(partialVars);
1036+
1037+
assertThat((Optional<?>) result).hasValue("hello");
1038+
}
1039+
1040+
@Test
1041+
public void optionalIndex_partialUnknownOnMap_returnsUnknown() throws Exception {
1042+
if (testMode.equals(TestMode.LEGACY_CHECKED)) {
1043+
// Legacy runtime executes optional indexing through standard function bindings without
1044+
// attribute
1045+
// trail tracking, so it cannot intercept partial sub-attribute unknowns on known containers.
1046+
return;
1047+
}
1048+
1049+
Cel cel =
1050+
newCelBuilder()
1051+
.addVar("m", MapType.create(SimpleType.STRING, SimpleType.INT))
1052+
.setResultType(OptionalType.create(SimpleType.INT))
1053+
.build();
1054+
CelAbstractSyntaxTree ast = compile(cel, "m[?'b']");
1055+
PartialVars partialVars =
1056+
PartialVars.of(
1057+
ImmutableMap.of("m", ImmutableMap.of("a", 1, "b", 2)),
1058+
CelAttributePattern.fromQualifiedIdentifier("m").qualify(Qualifier.ofString("b")));
1059+
1060+
Object result = cel.createProgram(ast).eval(partialVars);
1061+
1062+
assertThat(result).isInstanceOf(CelUnknownSet.class);
1063+
}
1064+
1065+
@Test
1066+
public void optionalIndex_partialUnknownOnMap_unrelatedKeyEvaluatesNormally() throws Exception {
1067+
if (testMode.equals(TestMode.LEGACY_CHECKED)) {
1068+
// Legacy runtime executes optional indexing through standard function bindings without
1069+
// attribute
1070+
// trail tracking, so it cannot intercept partial sub-attribute unknowns on known containers.
1071+
return;
1072+
}
1073+
1074+
Cel cel =
1075+
newCelBuilder()
1076+
.addVar("m", MapType.create(SimpleType.STRING, SimpleType.INT))
1077+
.setResultType(OptionalType.create(SimpleType.INT))
1078+
.build();
1079+
CelAbstractSyntaxTree ast = compile(cel, "m[?'a']");
1080+
PartialVars partialVars =
1081+
PartialVars.of(
1082+
ImmutableMap.of("m", ImmutableMap.of("a", 1, "b", 2)),
1083+
CelAttributePattern.fromQualifiedIdentifier("m").qualify(Qualifier.ofString("b")));
1084+
1085+
Object result = cel.createProgram(ast).eval(partialVars);
1086+
1087+
assertThat((Optional<?>) result).hasValue(1);
1088+
}
1089+
1090+
@Test
1091+
public void optionalIndex_partialUnknownOnMap_missingKeyEvaluatesToEmpty() throws Exception {
1092+
if (testMode.equals(TestMode.LEGACY_CHECKED)) {
1093+
// Legacy runtime executes optional indexing through standard function bindings without
1094+
// attribute
1095+
// trail tracking, so it cannot intercept partial sub-attribute unknowns on known containers.
1096+
return;
1097+
}
1098+
1099+
Cel cel =
1100+
newCelBuilder()
1101+
.addVar("m", MapType.create(SimpleType.STRING, SimpleType.INT))
1102+
.setResultType(OptionalType.create(SimpleType.INT))
1103+
.build();
1104+
CelAbstractSyntaxTree ast = compile(cel, "m[?'c']");
1105+
PartialVars partialVars =
1106+
PartialVars.of(
1107+
ImmutableMap.of("m", ImmutableMap.of("a", 1, "b", 2)),
1108+
CelAttributePattern.fromQualifiedIdentifier("m").qualify(Qualifier.ofString("b")));
1109+
1110+
Object result = cel.createProgram(ast).eval(partialVars);
1111+
1112+
assertThat((Optional<?>) result).isEmpty();
1113+
}
1114+
9881115
@Test
9891116
public void optionalIndex_onOptionalList_returnsOptionalEmpty() throws Exception {
9901117
Cel cel =
@@ -1049,7 +1176,8 @@ public void traditionalIndex_onOptionalList_returnsOptionalEmpty() throws Except
10491176
@Test
10501177
public void optionalFieldSelect_fieldMarkedUnknown_returnsUnknownSet() throws Exception {
10511178
if (testMode.equals(TestMode.LEGACY_CHECKED)) {
1052-
// This case is not possible to setup for legacy runtime
1179+
// Legacy runtime does not support attribute trail tracking for optional field selection
1180+
// (.?field).
10531181
return;
10541182
}
10551183

runtime/BUILD.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,3 +379,9 @@ cel_android_library(
379379
name = "partial_vars_android",
380380
exports = ["//runtime/src/main/java/dev/cel/runtime:partial_vars_android"],
381381
)
382+
383+
cel_android_library(
384+
name = "function_resolver_android",
385+
visibility = ["//:internal"],
386+
exports = ["//runtime/src/main/java/dev/cel/runtime:function_resolver_android"],
387+
)

runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.ArrayList;
2020
import java.util.Arrays;
2121
import java.util.Collection;
22+
import java.util.Collections;
2223
import java.util.HashSet;
2324
import java.util.Set;
2425
import org.jspecify.annotations.Nullable;
@@ -36,12 +37,12 @@ public final class AccumulatedUnknowns {
3637
private final Set<Long> exprIds;
3738
private final Set<CelAttribute> attributes;
3839

39-
Set<Long> exprIds() {
40-
return exprIds;
40+
public Set<Long> exprIds() {
41+
return Collections.unmodifiableSet(exprIds);
4142
}
4243

43-
Set<CelAttribute> attributes() {
44-
return attributes;
44+
public Set<CelAttribute> attributes() {
45+
return Collections.unmodifiableSet(attributes);
4546
}
4647

4748
/**

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,8 @@ java_library(
773773
cel_android_library(
774774
name = "function_resolver_android",
775775
srcs = ["CelFunctionResolver.java"],
776+
tags = [
777+
],
776778
deps = [
777779
":evaluation_exception",
778780
":resolved_overload_android",

runtime/src/main/java/dev/cel/runtime/CelAttribute.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.google.common.primitives.UnsignedLong;
2222
import com.google.errorprone.annotations.Immutable;
2323
import com.google.re2j.Pattern;
24+
import org.jspecify.annotations.Nullable;
2425

2526
/**
2627
* CelAttribute represents the select path from the root (.) to a single leaf value that may be
@@ -100,6 +101,19 @@ public static Qualifier ofWildCard() {
100101
* index.
101102
*/
102103
public static Qualifier fromGeneric(Object value) {
104+
Qualifier qualifier = fromGenericOrNull(value);
105+
if (qualifier != null) {
106+
return qualifier;
107+
}
108+
throw new IllegalArgumentException("Unsupported attribute qualifier kind");
109+
}
110+
111+
/**
112+
* Creates a Qualifier from a generic object, or null if the value cannot be interpreted as an
113+
* attribute qualifier.
114+
*/
115+
@SuppressWarnings("IfChainToSwitch")
116+
public static @Nullable Qualifier fromGenericOrNull(Object value) {
103117
if (value instanceof UnsignedLong) {
104118
return ofUint((UnsignedLong) value);
105119
} else if (value instanceof Long) {
@@ -111,7 +125,7 @@ public static Qualifier fromGeneric(Object value) {
111125
} else if (value instanceof String) {
112126
return ofString((String) value);
113127
}
114-
throw new IllegalArgumentException("Unsupported attribute qualifier kind");
128+
return null;
115129
}
116130

117131
public String toIndexFormat() {

runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,25 @@ public Object trace(PartialVars partialVars, CelEvaluationListener listener)
235235

236236
@Override
237237
public Object advanceEvaluation(UnknownContext context) throws CelEvaluationException {
238-
throw new UnsupportedOperationException("Unsupported operation.");
238+
PlannedProgram plannedProgram = (PlannedProgram) program;
239+
if (!plannedProgram.options().enableUnknownTracking()) {
240+
return plannedProgram.evalOrThrow(
241+
plannedProgram.interpretable(),
242+
context.variableResolver(),
243+
EMPTY_FUNCTION_RESOLVER,
244+
/* partialVars= */ null,
245+
/* attributeResolver= */ null,
246+
/* listener= */ null);
247+
}
248+
return plannedProgram.evalOrThrow(
249+
plannedProgram.interpretable(),
250+
context.variableResolver(),
251+
EMPTY_FUNCTION_RESOLVER,
252+
PartialVars.of(
253+
(name) -> Optional.ofNullable(context.variableResolver().resolve(name)),
254+
context.unresolvedAttributes()),
255+
context.createAttributeResolver(),
256+
/* listener= */ null);
239257
}
240258
};
241259
}

runtime/src/main/java/dev/cel/runtime/UnknownContext.java

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,16 @@ public GlobalResolver variableResolver() {
107107
return variableResolver;
108108
}
109109

110+
/** Accessor for unresolved attribute patterns. */
111+
ImmutableList<CelAttributePattern> unresolvedAttributes() {
112+
return unresolvedAttributes;
113+
}
114+
115+
/** Accessor for resolved attribute values. */
116+
ImmutableMap<CelAttribute, Object> resolvedAttributes() {
117+
return resolvedAttributes;
118+
}
119+
110120
/**
111121
* Creates a new unknown context that is a copy of the current context with the provided
112122
* additional attribute values.
@@ -123,12 +133,28 @@ public UnknownContext withResolvedAttributes(Map<CelAttribute, Object> resolvedA
123133
ImmutableMap.<CelAttribute, Object>builder()
124134
.putAll(this.resolvedAttributes)
125135
.putAll(resolvedAttributes)
126-
.buildOrThrow());
136+
.buildKeepingLast());
127137
}
128138

129-
private boolean patternMaskedByResolvedAttribute(
139+
private static boolean patternMaskedByResolvedAttribute(
130140
Map<CelAttribute, Object> resolved, CelAttributePattern pattern) {
131-
return resolved.keySet().stream().anyMatch(pattern::isPartialMatch);
141+
return resolved.keySet().stream().anyMatch(attr -> isPatternMaskedByAttribute(pattern, attr));
142+
}
143+
144+
private static boolean isPatternMaskedByAttribute(
145+
CelAttributePattern pattern, CelAttribute attribute) {
146+
if (attribute.qualifiers().size() > pattern.qualifiers().size()) {
147+
return false;
148+
}
149+
for (int i = 0; i < attribute.qualifiers().size(); i++) {
150+
CelAttribute.Qualifier patternQualifier = pattern.qualifiers().get(i);
151+
CelAttribute.Qualifier attrQualifier = attribute.qualifiers().get(i);
152+
if (patternQualifier.kind() == CelAttribute.Qualifier.Kind.WILD_CARD
153+
|| !patternQualifier.equals(attrQualifier)) {
154+
return false;
155+
}
156+
}
157+
return true;
132158
}
133159

134160
/**
@@ -168,10 +194,27 @@ public Optional<Object> resolve(CelAttribute attribute) {
168194

169195
@Override
170196
public Optional<CelUnknownSet> maybePartialUnknown(CelAttribute attribute) {
171-
return unresolvedAttributes.stream()
172-
.filter(p -> p.isPartialMatch(attribute))
173-
.findFirst()
174-
.map(p -> CelUnknownSet.create(p.simplify(attribute)));
197+
if (attribute.equals(CelAttribute.EMPTY)) {
198+
return Optional.empty();
199+
}
200+
Optional<CelUnknownSet> fromUnresolved =
201+
unresolvedAttributes.stream()
202+
.filter(p -> p.isPartialMatch(attribute))
203+
.findFirst()
204+
.map(p -> CelUnknownSet.create(p.simplify(attribute)));
205+
if (fromUnresolved.isPresent()) {
206+
return fromUnresolved;
207+
}
208+
for (CelAttribute resolved : resolvedAttributes.keySet()) {
209+
if (resolved.qualifiers().size() > attribute.qualifiers().size()
210+
&& resolved
211+
.qualifiers()
212+
.subList(0, attribute.qualifiers().size())
213+
.equals(attribute.qualifiers())) {
214+
return Optional.of(CelUnknownSet.create(attribute));
215+
}
216+
}
217+
return Optional.empty();
175218
}
176219
}
177220
}

runtime/src/main/java/dev/cel/runtime/planner/Attribute.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
/** Represents a resolvable symbol or path (such as a variable or a field selection). */
2121
@Immutable
2222
interface Attribute {
23-
Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame);
23+
AttributeResolution resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame);
2424

2525
Attribute addQualifier(Qualifier qualifier);
2626
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.runtime.planner;
16+
17+
import com.google.errorprone.annotations.Immutable;
18+
import dev.cel.runtime.CelAttribute;
19+
import org.jspecify.annotations.Nullable;
20+
21+
/** Bundles a resolved value and its corresponding {@link CelAttribute} trail. */
22+
@Immutable
23+
final class AttributeResolution {
24+
25+
@SuppressWarnings("Immutable")
26+
private final @Nullable Object value;
27+
28+
private final @Nullable CelAttribute attribute;
29+
30+
static AttributeResolution of(@Nullable Object value, @Nullable CelAttribute attribute) {
31+
return new AttributeResolution(value, attribute);
32+
}
33+
34+
static AttributeResolution ofValue(@Nullable Object value) {
35+
return new AttributeResolution(value, null);
36+
}
37+
38+
@Nullable Object value() {
39+
return value;
40+
}
41+
42+
@Nullable CelAttribute attribute() {
43+
return attribute;
44+
}
45+
46+
private AttributeResolution(@Nullable Object value, @Nullable CelAttribute attribute) {
47+
this.value = value;
48+
this.attribute = attribute;
49+
}
50+
}

0 commit comments

Comments
 (0)