Skip to content

Commit daef0cd

Browse files
l46kokcopybara-github
authored andcommitted
Add AsyncCallKey and AsyncCallRecord primitives for async evaluation.
PiperOrigin-RevId: 982639852
1 parent ef2f7aa commit daef0cd

7 files changed

Lines changed: 1250 additions & 4 deletions

File tree

runtime/planner/BUILD.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,9 @@ java_library(
3535
visibility = ["//:internal"],
3636
exports = ["//runtime/src/main/java/dev/cel/runtime/planner:async_completion_coordinator"],
3737
)
38+
39+
java_library(
40+
name = "async_call_state_tracker",
41+
visibility = ["//:internal"],
42+
exports = ["//runtime/src/main/java/dev/cel/runtime/planner:async_call_state_tracker"],
43+
)
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
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 java.util.Objects.requireNonNull;
18+
19+
import dev.cel.runtime.RuntimeEquality;
20+
import java.util.Arrays;
21+
import java.util.Collection;
22+
import java.util.Iterator;
23+
import java.util.Map;
24+
import java.util.Objects;
25+
import java.util.Set;
26+
import java.util.function.BiPredicate;
27+
import org.jspecify.annotations.Nullable;
28+
29+
/**
30+
* Unique cache key for an asynchronous function invocation at a given AST expression node.
31+
*
32+
* <p>Equality delegates to {@link RuntimeEquality}, except that {@link Double#NaN} and {@link
33+
* Float#NaN} argument values compare equal (and signed zero is normalized) across all arguments and
34+
* nested collections/maps so that re-evaluating a node with a NaN argument hits its existing call
35+
* record.
36+
*/
37+
final class AsyncCallKey {
38+
private final long exprId;
39+
private final String functionName;
40+
private final String overloadId;
41+
private final Object[] args;
42+
private final RuntimeEquality runtimeEquality;
43+
private final int hashCode;
44+
45+
static AsyncCallKey create(
46+
long exprId,
47+
String functionName,
48+
String overloadId,
49+
Object[] args,
50+
RuntimeEquality runtimeEquality) {
51+
return new AsyncCallKey(exprId, functionName, overloadId, args, runtimeEquality);
52+
}
53+
54+
@Override
55+
public boolean equals(Object o) {
56+
if (this == o) {
57+
return true;
58+
}
59+
if (!(o instanceof AsyncCallKey)) {
60+
return false;
61+
}
62+
AsyncCallKey other = (AsyncCallKey) o;
63+
if (exprId != other.exprId
64+
|| !functionName.equals(other.functionName)
65+
|| !overloadId.equals(other.overloadId)
66+
|| args.length != other.args.length) {
67+
return false;
68+
}
69+
for (int i = 0; i < args.length; i++) {
70+
if (!argEquals(args[i], other.args[i], runtimeEquality)) {
71+
return false;
72+
}
73+
if (!Objects.equals(runtimeEquality, other.runtimeEquality)
74+
&& !argEquals(other.args[i], args[i], other.runtimeEquality)) {
75+
return false;
76+
}
77+
}
78+
return true;
79+
}
80+
81+
@Override
82+
public int hashCode() {
83+
return hashCode;
84+
}
85+
86+
@SuppressWarnings("ReferenceEquality") // Fast path for identical object references.
87+
private static boolean argEquals(
88+
@Nullable Object a, @Nullable Object b, RuntimeEquality runtimeEquality) {
89+
if (a == b) {
90+
return true;
91+
}
92+
if (a == null || b == null) {
93+
return false;
94+
}
95+
if (a instanceof Number || b instanceof Number) {
96+
return a instanceof Number
97+
&& b instanceof Number
98+
&& numberEquals((Number) a, (Number) b, runtimeEquality);
99+
}
100+
if (a instanceof Set || b instanceof Set) {
101+
return a instanceof Set
102+
&& b instanceof Set
103+
&& setEquals((Set<?>) a, (Set<?>) b, runtimeEquality);
104+
}
105+
if (a instanceof Iterable || b instanceof Iterable) {
106+
return a instanceof Iterable
107+
&& b instanceof Iterable
108+
&& iterableEquals((Iterable<?>) a, (Iterable<?>) b, runtimeEquality);
109+
}
110+
if (a instanceof Map || b instanceof Map) {
111+
return a instanceof Map
112+
&& b instanceof Map
113+
&& mapEquals((Map<?, ?>) a, (Map<?, ?>) b, runtimeEquality);
114+
}
115+
if (a instanceof byte[] || b instanceof byte[]) {
116+
return a instanceof byte[] && b instanceof byte[] && Arrays.equals((byte[]) a, (byte[]) b);
117+
}
118+
if (a instanceof Object[] || b instanceof Object[]) {
119+
return a instanceof Object[]
120+
&& b instanceof Object[]
121+
&& arrayEquals((Object[]) a, (Object[]) b, runtimeEquality);
122+
}
123+
return celEquals(a, b, runtimeEquality);
124+
}
125+
126+
private static boolean numberEquals(Number a, Number b, RuntimeEquality runtimeEquality) {
127+
double da = a.doubleValue();
128+
double db = b.doubleValue();
129+
// CEL defines NaN != NaN; override so a node re-evaluated with NaN hits its existing record.
130+
if (Double.isNaN(da) && Double.isNaN(db)) {
131+
return true;
132+
}
133+
if (da == 0.0d && db == 0.0d) {
134+
return celEquals(normalizeSignedZero(a), normalizeSignedZero(b), runtimeEquality);
135+
}
136+
return celEquals(a, b, runtimeEquality);
137+
}
138+
139+
private static boolean setEquals(Set<?> a, Set<?> b, RuntimeEquality runtimeEquality) {
140+
return unorderedMatch(a, b, (x, y) -> argEquals(x, y, runtimeEquality));
141+
}
142+
143+
private static boolean iterableEquals(
144+
Iterable<?> a, Iterable<?> b, RuntimeEquality runtimeEquality) {
145+
if (a instanceof Collection && b instanceof Collection) {
146+
if (((Collection<?>) a).size() != ((Collection<?>) b).size()) {
147+
return false;
148+
}
149+
}
150+
Iterator<?> iterA = a.iterator();
151+
Iterator<?> iterB = b.iterator();
152+
while (iterA.hasNext() && iterB.hasNext()) {
153+
if (!argEquals(iterA.next(), iterB.next(), runtimeEquality)) {
154+
return false;
155+
}
156+
}
157+
return !iterA.hasNext() && !iterB.hasNext();
158+
}
159+
160+
private static boolean mapEquals(Map<?, ?> a, Map<?, ?> b, RuntimeEquality runtimeEquality) {
161+
return unorderedMatch(
162+
a.entrySet(),
163+
b.entrySet(),
164+
(entryA, entryB) ->
165+
argEquals(entryA.getKey(), entryB.getKey(), runtimeEquality)
166+
&& argEquals(entryA.getValue(), entryB.getValue(), runtimeEquality));
167+
}
168+
169+
private static boolean arrayEquals(Object[] a, Object[] b, RuntimeEquality runtimeEquality) {
170+
if (a.length != b.length) {
171+
return false;
172+
}
173+
for (int i = 0; i < a.length; i++) {
174+
if (!argEquals(a[i], b[i], runtimeEquality)) {
175+
return false;
176+
}
177+
}
178+
return true;
179+
}
180+
181+
private static <T> boolean unorderedMatch(
182+
Collection<? extends T> a,
183+
Collection<? extends T> b,
184+
BiPredicate<? super T, ? super T> matcher) {
185+
if (a.size() != b.size()) {
186+
return false;
187+
}
188+
Object[] targetArray = b.toArray();
189+
boolean[] matched = new boolean[targetArray.length];
190+
for (T elemA : a) {
191+
boolean found = false;
192+
for (int i = 0; i < targetArray.length; i++) {
193+
// Safe downcast because targetArray was created from Collection<? extends T> b.
194+
@SuppressWarnings("unchecked")
195+
T elemB = (T) targetArray[i];
196+
if (!matched[i] && matcher.test(elemA, elemB)) {
197+
matched[i] = true;
198+
found = true;
199+
break;
200+
}
201+
}
202+
if (!found) {
203+
return false;
204+
}
205+
}
206+
return true;
207+
}
208+
209+
/**
210+
* Applies CEL heterogeneous equality, treating incomparable argument pairs (which throw unchecked
211+
* exceptions from {@link RuntimeEquality#objectEquals}) as unequal.
212+
*/
213+
private static boolean celEquals(Object a, Object b, RuntimeEquality runtimeEquality) {
214+
try {
215+
return runtimeEquality.objectEquals(a, b);
216+
} catch (RuntimeException e) {
217+
return false;
218+
}
219+
}
220+
221+
/**
222+
* Normalizes {@code -0.0} to {@code 0.0} before comparison, because {@link
223+
* RuntimeEquality#objectEquals} returns {@code false} for cross-type pairs such as {@code (0L,
224+
* -0.0d)}.
225+
*/
226+
private static @Nullable Object normalizeSignedZero(@Nullable Object value) {
227+
if (value instanceof Double && (Double) value == 0.0d) {
228+
return 0.0d;
229+
}
230+
if (value instanceof Float && (Float) value == 0.0f) {
231+
return 0.0f;
232+
}
233+
return value;
234+
}
235+
236+
private static int computeHashCode(
237+
long exprId,
238+
String functionName,
239+
String overloadId,
240+
Object[] args,
241+
RuntimeEquality runtimeEquality) {
242+
int result = Objects.hash(exprId, functionName, overloadId);
243+
for (Object arg : args) {
244+
result = 31 * result + hashArg(arg, runtimeEquality);
245+
}
246+
return result;
247+
}
248+
249+
/**
250+
* Hashes a single argument consistently with {@link #argEquals}.
251+
*
252+
* <p>Does not delegate to {@link RuntimeEquality#hashCode} because that method hashes {@code
253+
* -0.0} and {@code 0.0} differently, which would break the {@link Object#hashCode} contract for
254+
* keys containing signed zero.
255+
*/
256+
private static int hashArg(@Nullable Object arg, RuntimeEquality runtimeEquality) {
257+
if (arg instanceof Number) {
258+
double d = ((Number) arg).doubleValue();
259+
// Normalize -0.0d to +0.0d so that values CEL considers equal hash identically.
260+
if (d == 0.0d) {
261+
return 0;
262+
}
263+
return Double.hashCode(d);
264+
}
265+
if (arg instanceof Set) {
266+
int h = 0;
267+
for (Object elem : (Set<?>) arg) {
268+
h += hashArg(elem, runtimeEquality);
269+
}
270+
return h;
271+
}
272+
if (arg instanceof Iterable) {
273+
return hashSequence((Iterable<?>) arg, runtimeEquality);
274+
}
275+
if (arg instanceof Map) {
276+
int h = 0;
277+
for (Map.Entry<?, ?> entry : ((Map<?, ?>) arg).entrySet()) {
278+
h += hashArg(entry.getKey(), runtimeEquality) ^ hashArg(entry.getValue(), runtimeEquality);
279+
}
280+
return h;
281+
}
282+
if (arg instanceof byte[]) {
283+
return Arrays.hashCode((byte[]) arg);
284+
}
285+
if (arg instanceof Object[]) {
286+
return hashArray((Object[]) arg, runtimeEquality);
287+
}
288+
return runtimeEquality.hashCode(arg);
289+
}
290+
291+
private static int hashSequence(Iterable<?> iterable, RuntimeEquality runtimeEquality) {
292+
int h = 1;
293+
for (Object elem : iterable) {
294+
h = h * 31 + hashArg(elem, runtimeEquality);
295+
}
296+
return h;
297+
}
298+
299+
private static int hashArray(Object[] array, RuntimeEquality runtimeEquality) {
300+
int h = 1;
301+
for (Object elem : array) {
302+
h = h * 31 + hashArg(elem, runtimeEquality);
303+
}
304+
return h;
305+
}
306+
307+
private AsyncCallKey(
308+
long exprId,
309+
String functionName,
310+
String overloadId,
311+
Object[] args,
312+
RuntimeEquality runtimeEquality) {
313+
this.exprId = exprId;
314+
this.functionName = requireNonNull(functionName);
315+
this.overloadId = requireNonNull(overloadId);
316+
this.args = requireNonNull(args).clone();
317+
this.runtimeEquality = requireNonNull(runtimeEquality);
318+
this.hashCode = computeHashCode(exprId, functionName, overloadId, this.args, runtimeEquality);
319+
}
320+
}

0 commit comments

Comments
 (0)