From 576cf7fa145c2a6d0730b026219d57de21c30598 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Mon, 5 Oct 2026 17:58:28 -0700 Subject: [PATCH] Deprecate legacy CEL runtimes and add planner migration guide PiperOrigin-RevId: 994020383 --- PLANNER_MIGRATION_GUIDE.md | 349 ++++++++++++++++++ README.md | 6 +- .../main/java/dev/cel/bundle/CelFactory.java | 14 +- .../main/java/dev/cel/extensions/README.md | 2 +- .../dev/cel/runtime/CelRuntimeFactory.java | 12 +- 5 files changed, 363 insertions(+), 20 deletions(-) create mode 100644 PLANNER_MIGRATION_GUIDE.md diff --git a/PLANNER_MIGRATION_GUIDE.md b/PLANNER_MIGRATION_GUIDE.md new file mode 100644 index 000000000..4f012171e --- /dev/null +++ b/PLANNER_MIGRATION_GUIDE.md @@ -0,0 +1,349 @@ +# CEL-Java Runtime Planner Migration Guide + +## Summary + +CEL-Java features an enhanced runtime (program planner) that is faster, more +ergonomic, and extensible. This document guides you through migrating your +codebase to leverage the planner. + +## Overview + +The new runtime offers the following advantages over the legacy implementation: + +- **Significantly improved performance:** Up to 90% faster evaluation latency + and 99% reduction in memory allocations when programs are cached. +- **Supports parsed-only evaluation mode:** Evaluate expressions directly + without requiring upfront type-checking. +- **Extensible CEL runtime value support:** Dynamically customize runtime + value representations via custom value providers. +- **Evaluation over POJOs:** Seamless integration with standard Java objects + using native type extensions. +- **Actionable error messages:** Clearer and more precise runtime error + descriptions. +- **Specification conformance:** Resolves subtle conformance and correctness + discrepancies against the CEL specification. + +## API Changes + +To opt in, swap your existing builder for `plannerRuntimeBuilder` or +`plannerCelBuilder`: + +```java +// Runtime only +CelRuntime runtime = CelRuntimeFactory.plannerRuntimeBuilder().build(); + +// Parser, type-checker, and runtime included +Cel cel = CelFactory.plannerCelBuilder().build(); +``` + +> **Note:** `plannerRuntimeBuilder` and `plannerCelBuilder` are the recommended +> builders for all new and existing workloads. The legacy builders +> (`standardCelBuilder`, `legacyCelBuilder`, `standardCelRuntimeBuilder`, +> `legacyCelRuntimeBuilder`) are deprecated. + +If you are overriding `CelOptions`, ensure +`enableHeterogeneousNumericComparisons` is enabled, and remove any deprecated +options: + +```java +CelOptions options = CelOptions.current() + // Ensure this is explicitly toggled on (enabled by default in planner + // builders) + .enableHeterogeneousNumericComparisons(true) + // Remove the following deprecated options if you have them set: + // .enableUnsignedLongs(...) + // .unwrapWellKnownTypesOnFunctionDispatch(...) + // .enableTimestampEpoch(...) + .build(); + +CelRuntime runtime = CelRuntimeFactory.plannerRuntimeBuilder() + .setOptions(options) + .build(); +``` + +For most setups, simply swapping the builder is all that is required. To get the +most out of the new runtime—and to troubleshoot any potential test +failures—please read through the **Enhancements** and **Behavioral Changes** +below. + +## Enhancements + +### Performance + +If `CelRuntime.Program` is cached, you will observe significant improvements in +both latency and memory usage. This setup provides up to a 90% speedup and 99% +memory reduction: + +```java +private static final CelCompiler CEL_COMPILER = + CelCompilerFactory.standardCelCompilerBuilder().build(); +private static final CelRuntime CEL_RUNTIME = + CelRuntimeFactory.plannerRuntimeBuilder().build(); + +// Create and store once -- evaluate multiple times against different inputs. +private CelRuntime.Program program; + +void compile(String expression) throws CelValidationException { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + this.program = CEL_RUNTIME.createProgram(ast); +} + +Object eval(Map variableMap) throws CelEvaluationException { + return program.eval(variableMap); +} +``` + +If `CelRuntime.Program` is created and evaluated on the same execution path, you +will still observe notable performance improvements for non-protobuf operations. +For expressions involving protobuf messages or field selections, performance +remains largely at parity. + +```java +private CelAbstractSyntaxTree ast; + +void compile(String expression) throws CelValidationException { + this.ast = CEL_COMPILER.compile(expression).getAst(); +} + +Object createProgramThenEval(Map variableMap) + throws CelEvaluationException { + CelRuntime.Program program = CEL_RUNTIME.createProgram(ast); + return program.eval(variableMap); +} +``` + +### Planning Step Validation + +In the legacy runtime, `createProgram` was essentially a no-op. The new planner +runtime uses this step to perform upfront validation, catching setup issues +early and reducing unexpected evaluation failures: + +```java +private static final CelRuntime CEL_RUNTIME = + CelRuntimeFactory.plannerRuntimeBuilder() + // Forgot to add function bindings here! + .build(); + +private CelRuntime.Program program; + +void compile(String expression) throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile("my_custom_func()").getAst(); + + // In the legacy runtime, an unbound function would only fail during eval(). + // The planner runtime will throw an exception right here during + // createProgram. + // Use it to catch and fix setup issues before caching the program. + this.program = CEL_RUNTIME.createProgram(ast); +} +``` + +### Parsed-Only Evaluation Mode + +You can now evaluate expressions without type-checking. This is useful if it is +infeasible to declare needed variables in advance, or if your expression relies +purely on dynamic types, and you would like to avoid type-checking overhead: + +```java +private static final CelParser CEL_PARSER = + CelParserFactory.standardCelParserBuilder().build(); +private static final CelRuntime CEL_RUNTIME = + CelRuntimeFactory.plannerRuntimeBuilder().build(); + +Object parsedOnlyEval() throws Exception { + CelAbstractSyntaxTree ast = CEL_PARSER.parse("a && b").getAst(); + CelRuntime.Program program = CEL_RUNTIME.createProgram(ast); + // This was previously impossible without declaring identifiers in the + // environment + return program.eval(ImmutableMap.of("a", true, "b", false)); +} +``` + +> **Note:** Evaluating parsed-only expressions is slower than evaluating +> type-checked counterparts. In most cases, we recommend type-checking +> expressions for both performance and correctness guarantees. + +### O(N) Time and Space Complexity + +With the planner, time and space complexity is guaranteed to be linear with +respect to input size for comprehensions involving both lists and maps. In the +legacy runtime, this was only guaranteed for lists and not for maps (such as in +two-variable comprehensions). + +### Custom Value Provider + +Previously, extending CEL's type system was strictly limited to compile time +(via a custom type provider). The new planner allows you to achieve similar +flexibility at runtime by designating a custom value provider to extend CEL's +value system dynamically: + +```java +// Example expression: +// AuditableRecord{ssn: "123-45-6789"}.ssn + +// Extend StructValue to intercept field access (e.g., for audit logging) +final class AuditableRecord extends StructValue> { + AuditableRecord(Map fields) { + super(fields); + } + + @Override + public Object select(String field) { + if ("ssn".equals(field)) { + AuditLogger.log("Sensitive field accessed: " + field); + } + return super.select(field); + } +} + +final class AuditableRecordProvider implements CelValueProvider { + @Override + public Optional newValue( + String structType, Map fields) { + if ("AuditableRecord".equals(structType)) { + return Optional.of(new AuditableRecord(fields)); + } + return Optional.empty(); + } +} + +CelRuntime runtime = CelRuntimeFactory.plannerRuntimeBuilder() + .setValueProvider(new AuditableRecordProvider()) + .build(); +``` + +### Seamless POJO Support (Native Type Extensions) + +With the legacy runtime, only protobuf messages were supported for evaluating +structs. The planner supports the native type extension library for registering +native Java types (POJOs) to be used directly in CEL expressions. Refer to +the +[Native Types Documentation](extensions/src/main/java/dev/cel/extensions/README.md#native-types) + for details. + +### Actionable Error Messages + +Runtime error messages are generally more accurate and actionable: + +``` +// Expression: +[1].flatten(-1) + +// Legacy Runtime Error: +Evaluation error: Function 'list_flatten_list_int' failed + +// Planner Runtime Error: +Evaluation error: Function 'flatten' failed +``` + +### Asynchronous Function Evaluation + +The planner runtime natively supports function-call-based asynchronous +evaluation via `CelFunctionBinding.fromAsync` and `Program.evalAsync`: + +```java +CelRuntime runtime = CelRuntimeFactory.plannerRuntimeBuilder() + .setAsyncExecutor(executorService) + .addFunctionBindings( + CelFunctionBinding.fromAsync( + "fetch_user_role_string", + String.class, + userId -> userClient.fetchRoleAsync(userId))) + .build(); + +CelRuntime.Program program = runtime.createProgram(ast); +ListenableFuture resultFuture = + program.evalAsync(ImmutableMap.of("user_id", "alice")); +``` + +Independent async calls across branches and comprehensions are dispatched +concurrently, identical calls are automatically memoized within an evaluation +session, and unneeded in-flight calls are cancelled when logical operators +short-circuit. Concurrency limits, iteration caps, completion batching, and +lifecycle hooks are configurable via `CelAsyncDrainStrategy`, +`CelAsyncObserver`, and `CelAsyncEvaluationOptions`. + +## Behavioral Changes + +### Map Creation with Decimal Keys + +Attempting to create a map with decimals as keys will now result in an error: + +``` +// Expression: +{1: "one", 1.0: "one point zero"}[1.0] + +// Legacy Runtime: +"one point zero" // Depended on java.util.Map implementation details. + // Note that in CEL, 1 == 1.0. + +// Planner Runtime: +Error +``` + +This corrects a bug in the legacy runtime that violated the CEL specification +regarding map key equivalence. + +### CEL Unknowns + +Historically, CEL-Java treated missing attributes as potentially unknown +variables, yielding a `CelUnknownSet`: + +```java +// Legacy behavior: +CelAbstractSyntaxTree ast = CEL_COMPILER.compile("a && b").getAst(); +CelRuntime.Program program = CEL_LEGACY_RUNTIME.createProgram(ast); + +// Note that "b" is not provided in the activation +Object result = program.eval(ImmutableMap.of("a", true)); +assertThat(result).isInstanceOf(CelUnknownSet.class); +``` + +This made it difficult to distinguish between an unintended misconfiguration and +an intentional unknown. In the planner runtime, this same expression will throw +an evaluation exception: **`No such attribute(s): 'b'`**. + +If your intent is to treat "b" as an unknown, you must explicitly declare it +using `CelAttributePattern` and pass it via `PartialVars`: + +```java +// Planner behavior: +CelAbstractSyntaxTree ast = CEL_COMPILER.compile("a && b").getAst(); +CelRuntime.Program program = CEL_PLANNER_RUNTIME.createProgram(ast); + +PartialVars input = PartialVars.of( + ImmutableMap.of("a", true), + CelAttributePattern.create("b") // Explicitly declare "b" as an unknown +); +Object result = program.eval(input); +assertThat(result).isInstanceOf(CelUnknownSet.class); +``` + +### Late Bound Functions + +Late bound functions must be registered in the runtime environment via the +`addLateBoundFunctions` builder method on `CelRuntimeBuilder`: + +```java +CelRuntime runtime = CelRuntimeFactory.plannerRuntimeBuilder() + .addLateBoundFunctions("record") + .build(); +``` + +### Lists and Maps as Evaluated Results + +While the legacy runtime evaluated lists and maps into mutable +`java.util.ArrayList` or `java.util.LinkedHashMap` objects, the planner strictly +returns immutable variants (such as Guava's `ImmutableList` or `ImmutableMap`). + +### Attribute-Level Async Resolution (CelAsyncRuntime) + +The legacy `CelAsyncRuntime` drove async evaluation by intercepting unknown +attribute patterns (e.g., `user.address` or `items[0]`) via `UnknownContext` and +re-evaluating. The planner runtime intentionally does not support the legacy +`CelAsyncRuntime` / `UnknownContext.withResolvedAttributes` workflow for +injecting resolved values at arbitrary sub-attribute or index paths. + +The recommended path forward is to use `PartialVars` to perform iterative +evaluation via unknowns out of band, or use asynchronous functions +(`CelFunctionBinding.fromAsync`), which is significantly more expressive, +performant, and ergonomic for I/O. diff --git a/README.md b/README.md index bf81ad6dc..e58154f59 100644 --- a/README.md +++ b/README.md @@ -109,13 +109,13 @@ against some input. Checking is optional, but strongly encouraged. ### Environment Setup Configuration for the entire CEL stack can be done all at once via the -`CelFactory.standardCelBuilder()`, or can be composed into compilation and +`CelFactory.plannerCelBuilder()`, or can be composed into compilation and evaluation via the `CelCompilerFactory` and `CelRuntimeFactory`. The simplest form of CEL usage is as follows: ```java -Cel cel = CelFactory.standardCelBuilder().build(); +Cel cel = CelFactory.plannerCelBuilder().build(); ``` More commonly, your application will want to configure type-checking separately @@ -221,7 +221,7 @@ leverage them, simply set the desired macros via `setStandardMacros` on the builder: ```java -CelCompiler.standardCelBuilder() +CelCompilerFactory.standardCelCompilerBuilder() .setStandardMacros(CelStandardMacro.STANDARD_MACROS) ``` diff --git a/bundle/src/main/java/dev/cel/bundle/CelFactory.java b/bundle/src/main/java/dev/cel/bundle/CelFactory.java index 79acccc93..653d7f8de 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelFactory.java +++ b/bundle/src/main/java/dev/cel/bundle/CelFactory.java @@ -14,7 +14,6 @@ package dev.cel.bundle; -import com.google.errorprone.annotations.InlineMe; import dev.cel.checker.CelCheckerLegacyImpl; import dev.cel.common.CelOptions; import dev.cel.compiler.CelCompiler; @@ -27,8 +26,6 @@ /** Helper class to configure the entire CEL stack in a common interface. */ public final class CelFactory { - private CelFactory() {} - /** * Creates a builder for configuring CEL using current parser for the parse, type-check, and eval * of expressions. @@ -36,10 +33,9 @@ private CelFactory() {} *

Note, the {@link CelOptions#current}, standard CEL function libraries, and linked message * evaluation are enabled by default. * - *

Note: This standard builder currently proxies the legacy builder, which will be deprecated. - * Callers are strongly encouraged to migrate to the planner ({@link #plannerCelBuilder()}). + * @deprecated Migrate to the planner ({@link #plannerCelBuilder()}). */ - @InlineMe(replacement = "CelFactory.legacyCelBuilder()", imports = "dev.cel.bundle.CelFactory") + @Deprecated public static CelBuilder standardCelBuilder() { return legacyCelBuilder(); } @@ -48,9 +44,9 @@ public static CelBuilder standardCelBuilder() { * Creates a builder for configuring a legacy CEL using current parser for the parse, type-check, * and eval of expressions. * - *

Note: This legacy builder will be deprecated. Callers are strongly encouraged to migrate to - * the planner ({@link #plannerCelBuilder()}). + * @deprecated Migrate to the planner ({@link #plannerCelBuilder()}). */ + @Deprecated public static CelBuilder legacyCelBuilder() { return CelImpl.newBuilder( CelCompilerImpl.newBuilder( @@ -89,4 +85,6 @@ public static CelBuilder plannerCelBuilder() { public static Cel combine(CelCompiler celCompiler, CelRuntime celRuntime) { return CelImpl.combine(celCompiler, celRuntime); } + + private CelFactory() {} } diff --git a/extensions/src/main/java/dev/cel/extensions/README.md b/extensions/src/main/java/dev/cel/extensions/README.md index 2ed1edf35..403566f56 100644 --- a/extensions/src/main/java/dev/cel/extensions/README.md +++ b/extensions/src/main/java/dev/cel/extensions/README.md @@ -7,7 +7,7 @@ To use, supply the desired extensions into `CelCompiler` and `CelRuntime` through `addLibrary` methods in their builder: CelCompilerFactory.standardCelCompilerBuilder().addLibraries(CelExtensions.strings()) - CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(CelExtensions.strings()) + CelRuntimeFactory.plannerRuntimeBuilder().addLibraries(CelExtensions.strings()) ## Cel diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeFactory.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeFactory.java index 8fa40d716..ad291b108 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeFactory.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeFactory.java @@ -14,7 +14,6 @@ package dev.cel.runtime; -import com.google.errorprone.annotations.InlineMe; import dev.cel.common.CelOptions; /** Helper class to construct new {@code CelRuntime} instances. */ @@ -26,12 +25,9 @@ public final class CelRuntimeFactory { *

Note, the {@link CelOptions#current}, standard CEL function libraries, and linked message * evaluation are enabled by default. * - *

Note: This standard runtime currently proxies the legacy runtime, which will be deprecated. - * Callers are strongly encouraged to migrate to the planner ({@link #plannerRuntimeBuilder()}). + * @deprecated Migrate to the planner ({@link #plannerRuntimeBuilder()}). */ - @InlineMe( - replacement = "CelRuntimeFactory.legacyCelRuntimeBuilder()", - imports = "dev.cel.runtime.CelRuntimeFactory") + @Deprecated public static CelRuntimeBuilder standardCelRuntimeBuilder() { return legacyCelRuntimeBuilder(); } @@ -39,9 +35,9 @@ public static CelRuntimeBuilder standardCelRuntimeBuilder() { /** * Create a new builder for constructing a legacy {@code CelRuntime} instance. * - *

Note: This legacy runtime will be deprecated. Callers are strongly encouraged to migrate to - * the planner ({@link #plannerRuntimeBuilder()}). + * @deprecated Migrate to the planner ({@link #plannerRuntimeBuilder()}). */ + @Deprecated public static CelRuntimeBuilder legacyCelRuntimeBuilder() { return CelRuntimeLegacyImpl.newBuilder() .setOptions(CelOptions.current().build())