Skip to content

Commit ef625f4

Browse files
l46kokcopybara-github
authored andcommitted
Share one adaptation across consecutive select chains on a message
PiperOrigin-RevId: 998208490
1 parent 4d49aaa commit ef625f4

8 files changed

Lines changed: 90 additions & 6 deletions

File tree

‎runtime/planner/BUILD.bazel‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ cel_android_library(
5656
exports = ["//runtime/src/main/java/dev/cel/runtime/planner:async_completion_coordinator_android"],
5757
)
5858

59+
java_library(
60+
name = "planned_interpretable",
61+
testonly = 1,
62+
visibility = ["//:internal"],
63+
exports = ["//runtime/src/main/java/dev/cel/runtime/planner:planned_interpretable"],
64+
)
65+
5966
java_library(
6067
name = "async_call_state_tracker",
6168
visibility = ["//:internal"],

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,12 +660,15 @@ java_library(
660660
"ExecutionFrame.java",
661661
"PlannedInterpretable.java",
662662
],
663+
tags = [
664+
],
663665
deps = [
664666
":async_call_state_tracker",
665667
":localized_evaluation_exception",
666668
"//common:options",
667669
"//common/ast",
668670
"//common/exceptions:iteration_budget_exceeded",
671+
"//common/values",
669672
"//runtime:evaluation_exception",
670673
"//runtime:evaluation_listener",
671674
"//runtime:function_resolver",
@@ -1334,6 +1337,7 @@ cel_android_library(
13341337
"//common:options",
13351338
"//common/ast:ast_android",
13361339
"//common/exceptions:iteration_budget_exceeded",
1340+
"//common/values:values_android",
13371341
"//runtime:evaluation_exception",
13381342
"//runtime:evaluation_listener_android",
13391343
"//runtime:interpretable_android",

‎runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) {
4848

4949
// The operand arrives already adapted, so re-materializing it would re-scan every entry of a
5050
// map for nothing. Traversing keeps that O(1); the selected value is materialized by the
51-
// attribute below.
52-
Object runtimeOperandValue = celValueConverter.toTraversalTarget(operandValue);
51+
// attribute below, which shares this adaptation through the frame if it has the same root.
52+
Object runtimeOperandValue = frame.toTraversalTarget(operandValue, celValueConverter);
5353
boolean hasField = false;
5454
if (runtimeOperandValue instanceof AccumulatedUnknowns) {
5555
Object hasFieldResult = EvalHelpers.evalStrictly(presenceAttribute, resolver, frame);

‎runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import dev.cel.common.CelOptions;
2121
import dev.cel.common.exceptions.CelIterationLimitExceededException;
22+
import dev.cel.common.values.CelValueConverter;
2223
import dev.cel.runtime.CelEvaluationException;
2324
import dev.cel.runtime.CelEvaluationListener;
2425
import dev.cel.runtime.CelFunctionResolver;
@@ -39,6 +40,9 @@ final class ExecutionFrame {
3940
private final @Nullable AsyncCallStateTracker asyncTracker;
4041
private int iterationCount;
4142
private @Nullable BlockMemoizer blockMemoizer;
43+
// The last select chain root that adapted to a new object in this evaluation, and that object.
44+
private @Nullable Object lastRoot;
45+
private @Nullable Object lastTraversalTarget;
4246

4347
static ExecutionFrame create(
4448
CelFunctionResolver functionResolver,
@@ -92,6 +96,22 @@ BlockMemoizer getBlockMemoizer() {
9296
return blockMemoizer;
9397
}
9498

99+
/** Adapts a select chain root, reusing the last adaptation if the root is the same object. */
100+
@SuppressWarnings("ReferenceEquality") // Identity is the point: value equality would be O(size).
101+
Object toTraversalTarget(Object root, CelValueConverter celValueConverter) {
102+
// The converter isn't part of the key: a program plans all its chains with one converter.
103+
if (root == lastRoot) {
104+
return lastTraversalTarget;
105+
}
106+
Object adapted = celValueConverter.toTraversalTarget(root);
107+
// Roots that adapt to themselves (maps, CelValues) mustn't evict a reusable wrapper.
108+
if (adapted != root) {
109+
lastRoot = root;
110+
lastTraversalTarget = adapted;
111+
}
112+
return adapted;
113+
}
114+
95115
boolean isAsync() {
96116
return asyncTracker != null;
97117
}

‎runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) {
9090
}
9191

9292
if (value != null) {
93-
return applyQualifiers(value, celValueConverter, qualifiers);
93+
return applyQualifiers(value, celValueConverter, qualifiers, frame);
9494
}
9595

9696
// Attempt to resolve the qualify type name if the name is not a variable identifier
@@ -183,15 +183,18 @@ public NamespacedAttribute addQualifier(Qualifier qualifier) {
183183
}
184184

185185
static Object applyQualifiers(
186-
Object value, CelValueConverter celValueConverter, ImmutableList<Qualifier> qualifiers) {
186+
Object value,
187+
CelValueConverter celValueConverter,
188+
ImmutableList<Qualifier> qualifiers,
189+
ExecutionFrame frame) {
187190
if (value instanceof AccumulatedUnknowns) {
188191
return value;
189192
}
190193
// Each Qualifier accepts and returns a traversal target, so only the root operand needs
191194
// adapting on the way in and only the terminal value is materialized on the way out.
192195
Object obj = value;
193196
if (!qualifiers.isEmpty()) {
194-
obj = celValueConverter.toTraversalTarget(obj);
197+
obj = frame.toTraversalTarget(obj, celValueConverter);
195198
// Avoid enhanced for loop to prevent UnmodifiableIterator from being allocated
196199
for (int i = 0; i < qualifiers.size(); i++) {
197200
obj = qualifiers.get(i).qualify(obj);

‎runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ final class RelativeAttribute implements Attribute {
3333
@Override
3434
public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) {
3535
Object obj = EvalHelpers.evalStrictly(operand, ctx, frame);
36-
return NamespacedAttribute.applyQualifiers(obj, celValueConverter, qualifiers);
36+
return NamespacedAttribute.applyQualifiers(obj, celValueConverter, qualifiers, frame);
3737
}
3838

3939
@Override

‎runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ java_library(
6565
"//runtime/planner:async_completion_coordinator",
6666
"//runtime/planner:async_gate",
6767
"//runtime/planner:optimized_select_planner",
68+
"//runtime/planner:planned_interpretable",
6869
"//runtime/planner:planned_program",
6970
"//runtime/planner:program_planner",
7071
"//runtime/standard:type",
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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 static com.google.common.truth.Truth.assertThat;
18+
19+
import com.google.common.collect.ImmutableMap;
20+
import dev.cel.common.CelOptions;
21+
import dev.cel.common.values.CelValueConverter;
22+
import dev.cel.runtime.CelFunctionResolver;
23+
import java.util.Optional;
24+
import org.junit.Test;
25+
import org.junit.runner.RunWith;
26+
import org.junit.runners.JUnit4;
27+
28+
@RunWith(JUnit4.class)
29+
public final class ExecutionFrameTest {
30+
31+
@Test
32+
public void toTraversalTarget_sameRootAfterSelfAdaptingRoot_reusesAdaptation() {
33+
ExecutionFrame frame =
34+
ExecutionFrame.create(
35+
CelFunctionResolver.EMPTY,
36+
CelOptions.DEFAULT,
37+
/* partialVars= */ null,
38+
/* listener= */ null);
39+
CelValueConverter converter = CelValueConverter.getDefaultInstance();
40+
// Adapts to a new OptionalValue each time, while a map adapts to itself.
41+
Optional<String> root = Optional.of("a");
42+
Object adapted = frame.toTraversalTarget(root, converter);
43+
frame.toTraversalTarget(ImmutableMap.of(), converter);
44+
45+
Object readapted = frame.toTraversalTarget(root, converter);
46+
47+
assertThat(readapted).isSameInstanceAs(adapted);
48+
}
49+
}

0 commit comments

Comments
 (0)