Skip to content

Commit 48e5ab6

Browse files
committed
Add native-value planned evaluation
Introduce a conservative whole-expression evaluator for supported checked scalar expressions. Typed boolean, long, double, and string nodes keep successful intermediate values out of Val wrappers, while existing Err, UnknownT, null, activation, cost, and evaluation-order semantics remain intact. Unsupported syntax, options, decorators, adapters, field providers, or replaced overloads continue to use the standard interpreter. Extend the eligible domain to checked map scalar selectors, characterized protobuf scalar fields, Jackson 3 string fields, constant indexes into top-level numeric and string lists, same-type string membership against top-level string lists and checked non-optional string-list literals, and checked non-optional integer, double, string, and boolean list literals fused with an immediate constant index or exact standard global/receiver size consumer. Explicit adapter and field-provider capabilities prevent custom implementations from being treated as semantically compatible, typed generated-protobuf getters avoid primitive boxing, and null-backed strings preserve exact existing StringT(null) behavior through localized compatibility transport. The representation choice is measurement-driven. At depth 16, Object-returning nodes retained 384 B/op versus 408 B/op for the current mixed evaluator, while typed nodes were effectively allocation-free and reduced mixed long/double latency from 97.9/136.5 ns to 23.7/23.0 ns. The production Program boundary reduced depth-16 allocation from 464 B/op to 80 B/op (82.8 percent), with int latency improving from 118.9 to 71.3 ns and double from 102.4 to 46.2 ns. Retained selector and list slices also remove their raw-path allocation: map selection improved from 30.9 ns / 24 B to 18.3 ns / approximately 0 B, generated protobuf int selection from 48.4 ns / 72 B to 18.2 ns / approximately 0 B, Jackson 3 string selection from 30.4 ns / 32 B to 20.3 ns / approximately 0 B, long-array comparison from 60.6 ns / 80 B to 12.1 ns / approximately 0 B, and String-array comparison from 54.8 ns / 64 B to 13.7 ns / approximately 0 B. Raw String-array membership improved from 1114.8 ns / 48 B to 630.9 ns / approximately 0 B for an absent value at size 1024; the corresponding Program path improved from 1161.3 ns / 104 B to 759.7 ns / 40 B. Numeric membership was prototyped but rejected after absent size-1024 long-array latency regressed by 22 percent despite removing allocation. Checked string-list literal membership completes literal construction before matching, preserves early list error/unknown termination and left needle precedence, and allocates compatibility storage only for exceptional successful runtime values. For absent computed size 1024, raw evaluation improved from 28703 ns / 8313 B to 9200 ns / approximately 0 B and Program evaluation from 27904 ns / 8369 B to 9152 ns / 56 B. The constant Program path improved from 7157 ns / 8336 B to 1974 ns / 40 B. Checked scalar-list literal index fusion avoids materializing the aggregate while preserving complete construction order, delayed bounds errors, error/unknown precedence, selected and unselected runtime mismatches, null-backed strings, and double special values. For computed size 1024, integer raw evaluation improved from 20654.0 ns / 57473 B to 8271.2 ns / approximately 0 B, double from 16857.5 ns / 57449 B to 3184.5 ns / approximately 0 B, string from 32436.4 ns / 8297 B to 9090.5 ns / approximately 0 B, and boolean from 29059.3 ns / 8297 B to 7888.3 ns / approximately 0 B. The corresponding Program paths improved to 7382.8 ns / 56 B, 2605.6 ns / 64 B, 9307.1 ns / 72 B, and 8162.3 ns / 56 B respectively. The exact standard global and receiver size consumers reuse the literal plan without producing an aggregate. Constant elements return their known cardinality directly; computed elements still evaluate sequentially, preserve the first error or unknown, and discard successful runtime type mismatches exactly like current list construction. For computed size 1024, integer raw evaluation improved from 28804.8 ns / 57473 B to 8174.7 ns / approximately 0 B, double from 16710.3 ns / 57473 B to 2731.2 ns / approximately 0 B, string from 27968.1 ns / 8321 B to 6829.6 ns / approximately 0 B, and boolean from 28240.7 ns / 8321 B to 6884.3 ns / approximately 0 B. Their Program paths improved to 8325.7 ns / 80 B, 2208.7 ns / 64 B, 7060.5 ns / 80 B, and 7004.1 ns / 80 B respectively. Terminal scalar-list results remain on the interpreter because primitive arrays would change the observable ValListT/Object[] representation. Add OptDisableNativeEval as an explicit interpreter control, use it for differential and benchmark oracles, and execute conformance in native-enabled and native-disabled modes. The dual run completed 4,908 cases with 4,838 passing and 70 skipped. Validated with the core, Jackson 2, Jackson 3, and conformance test suites, Spotless, core Javadoc, and core/Jackson 3 JMH jar construction. Consolidate strict scalar dispatch behind package-private native-overload descriptors keyed by the checked overload ID and exact resolved implementation identity. Standard descriptors reuse the trusted standard-overload identity set, while custom registration is lazy and unregistered or incorrectly bound implementations fall back to the interpreter. Preserve the original switch-shaped arithmetic nodes after generic operation lambdas regressed deep latency; paired HEAD controls retain effectively zero primitive allocation, 24 B/op final adaptation, neutral steady-state latency, and identical supported-plan construction allocation. Keep this extension seam internal until a real external library consumer justifies a public API; macros remain AST expansion. Evaluate the exact canonical checked single-variable exists expansion over compatible top-level scalar lists with a typed evaluation-local loop binding. Match the checked comprehension shape independently of macro provenance, and preserve current fold scoping, short-circuit error and unknown suppression, cost, fallback, and concurrent plan reuse. Nested, two-variable, non-canonical, or unsupported predicates remain on the interpreter. Direct primitive and raw-string array traversal removes per-element Val and activation allocation. In three-fork JDK 21 size-1024 late-match Program.eval measurements, long[] improved from 76,932 ns / 49,027 B to 7,405 ns / 104 B, while String[] improved from 63,338 ns / 32,906 B to 8,450 ns / 104 B. Generalize canonical quantifier planning to the exact checked single-variable all expansion over the same top-level scalar-list domain. Share one typed traversal and 48-byte loop binding with exists while preserving empty-range truth, first-false short-circuiting, later-false suppression of earlier errors and unknowns, fold scope, cost, fallback, and concurrent reuse. Primitive boolean arrays retain the current adapter error rather than bypassing compatibility. In three-fork JDK 21 size-1024 late-failure Program.eval measurements, long[] improved from 67,814 ns / 49,026 B to 7,664 ns / 104 B, while String[] improved from 60,284 ns / 32,906 B to 9,233 ns / 104 B. A first/last/all-true matrix confirmed early termination and fixed allocation; the generalized exists control retained 48 B raw and 104 B public allocation and remained within the 10-percent latency gate. Evaluate the exact canonical checked single-variable exists_one expansion over compatible top-level scalar lists. Reuse one scalar traversal with a specialized counted loop binding, preserve complete traversal, nested conditional error wrapping, latest exceptional-predicate precedence, runtime-exception conversion, fold scope, cost, fallback, and concurrent plan reuse. Nested, two-variable, literal-range, non-canonical, and unsupported predicates remain on the interpreter. In three-fork JDK 21 size-1024 Program.eval measurements, long[] improved from 53,361 ns / 49,026 B to 7,442 ns / 112 B, while String[] improved from 60,791 ns / 32,906 B to 8,672 ns / 112 B. Size-16/1024 absent/two-match controls confirm fixed allocation. The refactored exists/all paths retain 48 B raw and 104 B public allocation, with all historical late-case latency controls inside the 10-percent gate. Fuse the exact canonical checked single-variable filter expansion when it is immediately consumed by the exact standard global or receiver list-size overload. Reuse the scalar traversal with a 56-byte counted binding, preserve optimized EvalListFold predicate termination, error and unknown handling, resolution order, scope, partial activation, cost, fallback, and concurrent reuse, and keep terminal filters and unsupported comprehension shapes on the interpreter. In three-fork JDK 21 size-1024 late-match Program.eval measurements, long[] improved from 31,879 ns / 53,193 B to 7,378 ns / 112 B, while String[] improved from 33,085 ns / 37,065 B to 8,735 ns / 112 B. Raw execution retains fixed 56 B allocation. Size and match-position controls confirm cardinality-independent allocation, while exists, all, and exists_one retain their accepted allocation and remain inside the 10-percent latency gate. Fuse the exact canonical checked unfiltered map expansion when it is immediately consumed by the exact standard global or receiver list-size overload. Evaluate every transform in source order without retaining successful values, preserve first error and unknown termination, successful runtime-mismatch and null-backed-string counting, resolution order, scope, partial activation, cost, fallback, and concurrent reuse, and keep terminal maps and unsupported comprehension shapes on the interpreter. In three-fork JDK 21 size-1024 computed-transform Program.eval measurements, long[] integer mapping improved from 28,734 ns / 81,897 B to 8,534 ns / 136 B, while String[] concatenation improved from 33,626 ns / 115,121 B to 17,253 ns / 33,097 B. Identity-transform controls retain fixed 56 B raw and 136 B public allocation independent of cardinality. String concatenation retains only its transform-intrinsic per-element allocation, and the existing filter-size control remains inside its accepted allocation and latency gate. Extend exact canonical map-size fusion to the standard filtered three-argument expansion. Plan the predicate and transform independently over one typed loop binding, evaluate the predicate first, suppress false-branch transforms, discard accepted successful values, and preserve conditional predicate signals plus transform error and unknown termination. Exact matching, scope, partial activation, resolution order, cost, fallback, and concurrent reuse remain aligned with the current evaluator. Because a filter expansion and filtered map with an identity transform are indistinguishable after macro expansion, evaluate each accepted filter element before counting. This fixes Err and Unknown propagation for independent true predicates without reintroducing list materialization; false predicates still suppress element evaluation. The size-1024 filter control remains fixed at 112 B/op and within roughly three percent of its prior latency. In three-fork JDK 21 size-1024 computed-transform Program.eval measurements, filtered long[] mapping improved from 55,319 ns / 106,425 B to 14,146 ns / 136 B, while filtered String[] concatenation improved from 62,170 ns / 131,409 B to 24,072 ns / 33,056 B. Cardinality controls confirm fixed allocation for integer transforms and string identity; string concatenation retains only accepted transform-intrinsic allocation. Fuse exact canonical unfiltered map(...)[constant] evaluation into the typed scalar loop. Traverse every transform without materializing the mapped list, retain only the selected scalar, and preserve later error/unknown precedence, delayed bounds failures, index-boundary error wrapping, runtime compatibility, cost, fallback, and concurrent reuse. At size 1024 on JDK 21, computed long[] Program.eval improves from 30,819 ns / 81,896 B to 8,584 ns / 144 B, and computed String[] improves from 36,446 ns / 115,113 B to 18,456 ns / 57,680 B. Identity transforms retain fixed 144 B public allocation across sizes and positions; the direct list-index control remains effectively allocation-free. Extend exact canonical map constant-index fusion to filtered three-argument map expansions and the AST-equivalent filter identity shape. Count accepted output positions, evaluate predicates before transforms, suppress false transforms, and preserve later failure precedence, delayed bounds, conditional/index error wrapping, runtime compatibility, cost, fallback, and concurrent reuse. At size 1024 on JDK 21, computed filtered long[] Program.eval improves from 58,065 ns / 106,425 B to 17,722 ns / 144 B, while computed filtered String[] improves from 58,397 ns / 131,401 B to 28,420 ns / 57,616 B. Identity transforms retain fixed 144 B public allocation; exact contemporaneous unfiltered controls retain allocation and stay inside the 10-percent latency gate. Fuse exact standard same-type string membership over canonical unfiltered and filtered string-map expansions and the AST-equivalent filter identity shape. Evaluate the needle first but complete required predicate and transform traversal before returning a match; preserve later mapped failures, left-first exceptional-needle precedence, false-transform suppression, conditional wrapping, runtime and null compatibility, cost, overload provenance, fallback, and concurrent reuse. Retain only match state on the ordinary path and allocate compatibility storage lazily. At size 1024 on JDK 21, absent-match Program.eval improves from 14,904 ns / 24,809 B to 2,906 ns / 144 B for unfiltered identity, from 42,255 ns / 115,113 B to 23,446 ns / 57,681 B for unfiltered computed strings, from 71,607 ns / 131,402 B to 34,587 ns / 57,617 B for filtered computed strings, and from 49,557 ns / 41,194 B to 14,312 ns / 144 B for filter identity. First-match allocation is identical, confirming complete construction semantics. Add a consumer-scoped two-pass integer aggregate for exact canonical unfiltered map expressions consumed by exists. Construct every mapped value before starting the predicate, retain primitive longs in an evaluation-local array, allocate compatibility storage only for exceptional successful values, and preserve transform termination, predicate suppression, fold scope, cost, fallback, and concurrent reuse. In three-fork JDK 21 size-1024 computed Program.eval measurements, first/last/absent cases improve from 33.9-110.3 us and 81.9-106.5 KB/op to 11.1-16.6 us and about 8.33 KB/op. The retained allocation is the required primitive aggregate rather than per-element wrappers; direct quantifier traversal remains at 104 B/op and inside its latency gate. Extend the consumer-scoped two-pass integer aggregate from canonical unfiltered map expressions consumed by exists to the exact standard all consumer. Share the primitive construction buffer and quantifier machinery, preserve complete construction before predicate evaluation, empty-range truth, first-false short circuiting, later-false suppression of earlier errors and unknowns, runtime compatibility, cost, fallback, and concurrent reuse. In three-fork JDK 21 size-1024 computed Program.eval measurements, first/last/absent-false all cases improve from 33.7-100.7 us and 81.9-106.5 KB/op to 10.8-16.7 us and about 8.33 KB/op. Cardinality controls retain the expected eight-byte-per-element primitive buffer, the isolated late mapped-exists control remains within four percent of its prior latency with identical allocation, and direct quantifiers retain fixed allocation. Extend the consumer-scoped two-pass integer aggregate to exact canonical unfiltered map expressions consumed by exists_one. Reuse the primitive construction pass and the established counted predicate activation so complete construction, full predicate traversal, zero/one/multiple-match results, latest exceptional precedence, exception conversion, fold scope, cost, fallback, and concurrent reuse remain aligned with the current evaluator. In three-fork JDK 21 size-1024 computed Program.eval measurements, first/last/absent/two-match cases improve from 108.1-120.7 us and 81.9-106.5 KB/op to 19.0-19.7 us and about 8.38 KB/op: 5.5-6.4 times faster with 89.8-92.1 percent less allocation. Cardinality scaling retains the expected eight-byte-per-element primitive buffer; mapped exists/all and direct exists_one controls retain their accepted boundaries. Complete the consumer-scoped integer aggregate family for exact canonical filtered map expressions and the AST-equivalent filter identity shape across exists, all, and exists_one. Evaluate predicates before transforms, wrap exceptional predicates through the canonical conditional, suppress false transforms, and defer the unchanged aggregate pass until construction succeeds. Grow filtered primitive storage only for accepted values while retaining exact pre-sizing for unfiltered maps; this consolidates the shared construction frontier without multiplying one implementation slice per quantifier. In three-fork JDK 21 size-1024 computed Program.eval measurements at 50 percent acceptance, the family improves from 40.6-89.0 us and 55.3-67.6 KB/op to 13.3-20.9 us and about 10.45 KB/op: 3.0-4.5 times faster with 81.1-84.6 percent less allocation. Zero, half, and almost-full acceptance retain 120 B, 10.45 KB, and 20.70 KB respectively; all unfiltered aggregate controls retain identical allocation within five percent of prior latency.
1 parent 51c0d96 commit 48e5ab6

23 files changed

Lines changed: 12014 additions & 32 deletions

File tree

conformance/src/test/java/org/projectnessie/cel/conformance/SimpleConformanceTest.java

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
import static org.projectnessie.cel.EnvOption.declarations;
3232
import static org.projectnessie.cel.EnvOption.macros;
3333
import static org.projectnessie.cel.EnvOption.types;
34+
import static org.projectnessie.cel.EvalOption.OptDisableNativeEval;
3435
import static org.projectnessie.cel.Library.StdLib;
36+
import static org.projectnessie.cel.ProgramOption.evalOptions;
3537
import static org.projectnessie.cel.common.types.BoolT.True;
3638
import static org.projectnessie.cel.common.types.BytesT.bytesOf;
3739
import static org.projectnessie.cel.common.types.DoubleT.doubleOf;
@@ -192,22 +194,39 @@ class SimpleConformanceTest {
192194
private static final AtomicInteger passed = new AtomicInteger();
193195
private static final AtomicInteger skipped = new AtomicInteger();
194196

197+
private enum EvaluationMode {
198+
NATIVE_ENABLED("native evaluation enabled"),
199+
NATIVE_DISABLED("native evaluation disabled");
200+
201+
private final String displayName;
202+
203+
EvaluationMode(String displayName) {
204+
this.displayName = displayName;
205+
}
206+
}
207+
195208
@TestFactory
196209
Stream<DynamicNode> simpleConformance() {
197210
List<DynamicNode> files = new ArrayList<>();
198-
TEST_FILES.forEach(fileName -> files.add(dynamicContainer(fileName, fileTests(fileName))));
211+
for (EvaluationMode mode : EvaluationMode.values()) {
212+
files.add(
213+
dynamicContainer(
214+
mode.displayName,
215+
TEST_FILES.stream()
216+
.map(fileName -> dynamicContainer(fileName, fileTests(fileName, mode)))));
217+
}
199218
files.add(dynamicTest("skip list matches testdata", this::assertAllSkipsMatched));
200219
return files.stream();
201220
}
202221

203222
@AfterAll
204223
static void printSummary() {
205224
System.out.printf(
206-
"Conformance tests: %d total, %d passed, %d skipped%n",
225+
"Conformance test executions: %d total, %d passed, %d skipped%n",
207226
total.get(), passed.get(), skipped.get());
208227
}
209228

210-
private Stream<DynamicNode> fileTests(String fileName) {
229+
private Stream<DynamicNode> fileTests(String fileName, EvaluationMode mode) {
211230
SimpleTestFile file;
212231
try {
213232
file = parseSimpleFile(TESTDATA_DIR.resolve(fileName));
@@ -216,15 +235,17 @@ private Stream<DynamicNode> fileTests(String fileName) {
216235
}
217236

218237
return file.getSectionList().stream()
219-
.map(
220-
section ->
221-
dynamicContainer(
222-
section.getName(),
223-
section.getTestList().stream()
224-
.map(test -> dynamicTest(test.getName(), () -> run(file, section, test)))));
238+
.map(section -> dynamicContainer(section.getName(), sectionTests(file, section, mode)));
239+
}
240+
241+
private Stream<DynamicNode> sectionTests(
242+
SimpleTestFile file, SimpleTestSection section, EvaluationMode mode) {
243+
return section.getTestList().stream()
244+
.map(test -> dynamicTest(test.getName(), () -> run(file, section, test, mode)));
225245
}
226246

227-
private void run(SimpleTestFile file, SimpleTestSection section, SimpleTest test)
247+
private void run(
248+
SimpleTestFile file, SimpleTestSection section, SimpleTest test, EvaluationMode mode)
228249
throws InvalidProtocolBufferException {
229250
total.incrementAndGet();
230251
String sectionPath = file.getName() + "/" + section.getName();
@@ -240,7 +261,7 @@ private void run(SimpleTestFile file, SimpleTestSection section, SimpleTest test
240261
abort("Skipped conformance test " + testPath);
241262
}
242263

243-
ConformanceCaseRunner.run(testPath, test);
264+
ConformanceCaseRunner.run(mode, testPath, test);
244265
passed.incrementAndGet();
245266
}
246267

@@ -290,7 +311,7 @@ private static Path testdataDir() {
290311
private static final class ConformanceCaseRunner {
291312
private ConformanceCaseRunner() {}
292313

293-
private static void run(String testPath, SimpleTest test)
314+
private static void run(EvaluationMode mode, String testPath, SimpleTest test)
294315
throws InvalidProtocolBufferException {
295316
if (test.getName().isEmpty()) {
296317
throw new IllegalArgumentException("simple test has no name");
@@ -323,9 +344,10 @@ private static void run(String testPath, SimpleTest test)
323344
return;
324345
}
325346

326-
match(testPath, test, ConformanceEvaluator.evalParsed(test, parsedExpr));
347+
String evaluationPath = mode.displayName + "/" + testPath;
348+
match(evaluationPath, test, ConformanceEvaluator.evalParsed(mode, test, parsedExpr));
327349
if (checkedExpr != null) {
328-
match(testPath, test, ConformanceEvaluator.evalChecked(test, checkedExpr));
350+
match(evaluationPath, test, ConformanceEvaluator.evalChecked(mode, test, checkedExpr));
329351
}
330352
}
331353
}
@@ -377,18 +399,23 @@ private static CheckedExpr check(SimpleTest test, ParsedExpr parsedExpr)
377399
return astToCheckedExpr(astIss.getAst());
378400
}
379401

380-
private static ExprValue evalParsed(SimpleTest test, ParsedExpr parsedExpr) {
381-
return eval(test, parsedExprToAst(parsedExpr));
402+
private static ExprValue evalParsed(
403+
EvaluationMode mode, SimpleTest test, ParsedExpr parsedExpr) {
404+
return eval(mode, test, parsedExprToAst(parsedExpr));
382405
}
383406

384-
private static ExprValue evalChecked(SimpleTest test, CheckedExpr checkedExpr) {
385-
return eval(test, checkedExprToAst(checkedExpr));
407+
private static ExprValue evalChecked(
408+
EvaluationMode mode, SimpleTest test, CheckedExpr checkedExpr) {
409+
return eval(mode, test, checkedExprToAst(checkedExpr));
386410
}
387411

388-
private static ExprValue eval(SimpleTest test, Ast ast) {
412+
private static ExprValue eval(EvaluationMode mode, SimpleTest test, Ast ast) {
389413
Env env = newEnv(conformanceEnvOptions(test).toArray(new EnvOption[0]));
390414

391-
Program program = env.program(ast);
415+
Program program =
416+
mode == EvaluationMode.NATIVE_ENABLED
417+
? env.program(ast)
418+
: env.program(ast, evalOptions(OptDisableNativeEval));
392419
Map<String, Object> args = new HashMap<>();
393420
test.getBindingsMap()
394421
.forEach(

0 commit comments

Comments
 (0)