From 6afaf8b3bffd082a82efea458ea554730ab48c17 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 24 Feb 2026 11:27:34 -0800 Subject: [PATCH 001/204] Fix Github Action workflow Supercedes https://github.com/google/cel-java/pull/957 PiperOrigin-RevId: 874711556 --- .github/workflows/workflow.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 060b83bdd..4b206e3ec 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -21,7 +21,7 @@ jobs: steps: - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." - run: echo "🐧 Job is running on a ${{ runner.os }} server!" - - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." + - run: echo "🔎 The name of your branch is ${GITHUB_REF} and your repository is ${{ github.repository }}." - name: Check out repository code uses: actions/checkout@v6 - name: Setup Bazel @@ -121,4 +121,6 @@ jobs: - name: Run Conformance Maven Test on Version Change if: steps.changed_file.outputs.any_changed == 'true' run: bazelisk test //conformance/src/test/java/dev/cel/conformance:conformance_maven --test_output=errors - - run: echo "🍏 This job's status is ${{ job.status }}." + - run: echo "🍏 This job's status is ${JOB_STATUS}." + env: + JOB_STATUS: ${{ job.status }} From ffc708aec35132c0f41ea7a12b31c03546217b5e Mon Sep 17 00:00:00 2001 From: Jonathan Tatum Date: Tue, 24 Feb 2026 12:03:25 -0800 Subject: [PATCH 002/204] Add support for importing / exporting shared feature flags to the environment YAML format. PiperOrigin-RevId: 874728744 --- bundle/BUILD.bazel | 1 - .../src/main/java/dev/cel/bundle/BUILD.bazel | 2 + .../java/dev/cel/bundle/CelEnvironment.java | 57 +++++++++++++++++-- .../cel/bundle/CelEnvironmentExporter.java | 50 +++++++++++++++- .../cel/bundle/CelEnvironmentYamlParser.java | 48 ++++++++++++++++ .../bundle/CelEnvironmentYamlSerializer.java | 17 ++++++ .../dev/cel/bundle/CelEnvironmentTest.java | 43 ++++++++++++++ .../bundle/CelEnvironmentYamlParserTest.java | 40 ++++++++++++- .../CelEnvironmentYamlSerializerTest.java | 3 + .../dev/cel/checker/CelCheckerBuilder.java | 3 + .../dev/cel/checker/CelCheckerLegacyImpl.java | 10 ++-- .../test/resources/environment/dump_env.yaml | 5 ++ .../resources/environment/extended_env.yaml | 7 +-- 13 files changed, 267 insertions(+), 19 deletions(-) diff --git a/bundle/BUILD.bazel b/bundle/BUILD.bazel index 7f21cf219..70880e532 100644 --- a/bundle/BUILD.bazel +++ b/bundle/BUILD.bazel @@ -27,6 +27,5 @@ java_library( java_library( name = "environment_exporter", - visibility = ["//:internal"], exports = ["//bundle/src/main/java/dev/cel/bundle:environment_exporter"], ) diff --git a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel index 0201a5807..4dd81fd9e 100644 --- a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel +++ b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel @@ -126,6 +126,7 @@ java_library( ":environment", "//:auto_value", "//bundle:cel", + "//checker:checker_builder", "//checker:standard_decl", "//common:compiler_common", "//common:options", @@ -133,6 +134,7 @@ java_library( "//common/types:cel_proto_types", "//common/types:cel_types", "//common/types:type_providers", + "//compiler:compiler_builder", "//extensions", "//extensions:extension_library", "//parser:macro", diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java index b54e3ca51..7ec2149a7 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java @@ -48,6 +48,7 @@ import dev.cel.compiler.CelCompilerLibrary; import dev.cel.extensions.CelExtensions; import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.CelRuntime; import dev.cel.runtime.CelRuntimeBuilder; import dev.cel.runtime.CelRuntimeLibrary; import java.util.Arrays; @@ -108,6 +109,9 @@ public abstract class CelEnvironment { /** Standard library subset (which macros, functions to include/exclude) */ public abstract Optional standardLibrarySubset(); + /** Feature flags to enable in the environment. */ + public abstract ImmutableSet features(); + /** Builder for {@link CelEnvironment}. */ @AutoValue.Builder public abstract static class Builder { @@ -159,6 +163,13 @@ public Builder setFunctions(FunctionDecl... functions) { public abstract Builder setStandardLibrarySubset(LibrarySubset stdLibrarySubset); + @CanIgnoreReturnValue + public Builder setFeatures(FeatureFlag... featureFlags) { + return setFeatures(ImmutableSet.copyOf(featureFlags)); + } + + public abstract Builder setFeatures(ImmutableSet macros); + abstract CelEnvironment autoBuild(); @CheckReturnValue @@ -188,18 +199,21 @@ public static Builder newBuilder() { .setDescription("") .setContainer(CelContainer.ofName("")) .setVariables(ImmutableSet.of()) - .setFunctions(ImmutableSet.of()); + .setFunctions(ImmutableSet.of()) + .setFeatures(ImmutableSet.of()); } /** Extends the provided {@link CelCompiler} environment with this configuration. */ public CelCompiler extend(CelCompiler celCompiler, CelOptions celOptions) throws CelEnvironmentException { + celOptions = applyFeatureFlags(celOptions); try { CelTypeProvider celTypeProvider = celCompiler.getTypeProvider(); CelCompilerBuilder compilerBuilder = celCompiler .toCompilerBuilder() .setContainer(container()) + .setOptions(celOptions) .setTypeProvider(celTypeProvider) .addVarDeclarations( variables().stream() @@ -222,19 +236,35 @@ public CelCompiler extend(CelCompiler celCompiler, CelOptions celOptions) /** Extends the provided {@link Cel} environment with this configuration. */ public Cel extend(Cel cel, CelOptions celOptions) throws CelEnvironmentException { + celOptions = applyFeatureFlags(celOptions); try { // Casting is necessary to only extend the compiler here CelCompiler celCompiler = extend((CelCompiler) cel, celOptions); - CelRuntimeBuilder celRuntimeBuilder = cel.toRuntimeBuilder(); - addAllRuntimeExtensions(celRuntimeBuilder, celOptions); + CelRuntime celRuntime = extendRuntime(cel, celOptions); - return CelFactory.combine(celCompiler, celRuntimeBuilder.build()); + return CelFactory.combine(celCompiler, celRuntime); } catch (RuntimeException e) { throw new CelEnvironmentException(e.getMessage(), e); } } + private CelOptions applyFeatureFlags(CelOptions celOptions) { + CelOptions.Builder optionsBuilder = celOptions.toBuilder(); + for (FeatureFlag featureFlag : features()) { + if (featureFlag.name().equals("cel.feature.macro_call_tracking")) { + optionsBuilder.populateMacroCalls(featureFlag.enabled()); + } else if (featureFlag.name().equals("cel.feature.backtick_escape_syntax")) { + optionsBuilder.enableQuotedIdentifierSyntax(featureFlag.enabled()); + } else if (featureFlag.name().equals("cel.feature.cross_type_numeric_comparisons")) { + optionsBuilder.enableHeterogeneousNumericComparisons(featureFlag.enabled()); + } else { + throw new IllegalArgumentException("Unknown feature flag: " + featureFlag.name()); + } + } + return optionsBuilder.build(); + } + private void addAllCompilerExtensions( CelCompilerBuilder celCompilerBuilder, CelOptions celOptions) { // TODO: Add capability to accept user defined exceptions @@ -250,7 +280,9 @@ private void addAllCompilerExtensions( } } - private void addAllRuntimeExtensions(CelRuntimeBuilder celRuntimeBuilder, CelOptions celOptions) { + private CelRuntime extendRuntime(CelRuntime celRuntime, CelOptions celOptions) { + CelRuntimeBuilder celRuntimeBuilder = celRuntime.toRuntimeBuilder(); + celRuntimeBuilder.setOptions(celOptions); // TODO: Add capability to accept user defined exceptions for (ExtensionConfig extensionConfig : extensions()) { CanonicalCelExtension extension = getExtensionOrThrow(extensionConfig.name()); @@ -262,6 +294,7 @@ private void addAllRuntimeExtensions(CelRuntimeBuilder celRuntimeBuilder, CelOpt celRuntimeBuilder.addLibraries(celRuntimeLibrary); } } + return celRuntimeBuilder.build(); } private void applyStandardLibrarySubset(CelCompilerBuilder compilerBuilder) { @@ -625,6 +658,20 @@ public CelType toCelType(CelTypeProvider celTypeProvider) { } } + /** Represents a feature flag that can be enabled in the environment. */ + @AutoValue + public abstract static class FeatureFlag { + /** Normalized name of the feature flag. */ + public abstract String name(); + + /** Whether the feature is enabled or disabled. */ + public abstract boolean enabled(); + + public static FeatureFlag create(String name, boolean enabled) { + return new AutoValue_CelEnvironment_FeatureFlag(name, enabled); + } + } + /** * Represents a configuration for a canonical CEL extension that can be enabled in the * environment. diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java index 01410ad0d..1ed113db7 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java @@ -22,6 +22,7 @@ import dev.cel.expr.Decl.FunctionDecl; import dev.cel.expr.Decl.FunctionDecl.Overload; import com.google.auto.value.AutoValue; +import com.google.common.base.Preconditions; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ListMultimap; @@ -30,6 +31,7 @@ import dev.cel.bundle.CelEnvironment.LibrarySubset; import dev.cel.bundle.CelEnvironment.LibrarySubset.FunctionSelector; import dev.cel.bundle.CelEnvironment.OverloadDecl; +import dev.cel.checker.CelCheckerBuilder; import dev.cel.checker.CelStandardDeclarations.StandardFunction; import dev.cel.checker.CelStandardDeclarations.StandardIdentifier; import dev.cel.common.CelFunctionDecl; @@ -41,6 +43,7 @@ import dev.cel.common.types.CelProtoTypes; import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypes; +import dev.cel.compiler.CelCompiler; import dev.cel.extensions.CelExtensionLibrary; import dev.cel.extensions.CelExtensions; import dev.cel.parser.CelMacro; @@ -161,9 +164,24 @@ public static CelEnvironmentExporter.Builder newBuilder() { * */ public CelEnvironment export(Cel cel) { - CelEnvironment.Builder envBuilder = - CelEnvironment.newBuilder().setContainer(cel.toCheckerBuilder().container()); + return export((CelCompiler) cel); + } + /** + * Exports a {@link CelEnvironment} that describes the configuration of the given {@link + * CelCompiler} instance. + * + *

The exported environment includes: + * + *

    + *
  • Standard library subset: functions and their overloads that are either included or + * excluded from the standard library. + *
  • Extension libraries: names and versions of the extension libraries that are used. + *
  • Custom declarations: functions and variables that are not part of the standard library or + * any of the extension libraries. + *
+ */ + public CelEnvironment export(CelCompiler cel) { // Inventory is a full set of declarations and macros that are found in the configuration of // the supplied CEL instance. // @@ -172,6 +190,14 @@ public CelEnvironment export(Cel cel) { // // Whatever is left will be included in the Environment as custom declarations. + // Checker builder is used to access some parts of the config not exposed in the EnvVisitable + // interface. + CelCheckerBuilder checkerBuilder = cel.toCheckerBuilder(); + + CelEnvironment.Builder envBuilder = + CelEnvironment.newBuilder().setContainer(checkerBuilder.container()); + addOptions(envBuilder, checkerBuilder.options()); + Set inventory = new HashSet<>(); collectInventory(inventory, cel); addExtensionConfigsAndRemoveFromInventory(envBuilder, inventory); @@ -180,11 +206,29 @@ public CelEnvironment export(Cel cel) { return envBuilder.build(); } + private void addOptions(CelEnvironment.Builder envBuilder, CelOptions options) { + // The set of features supported in the exported environment in Go is pretty limited right now. + ImmutableSet.Builder featureFlags = ImmutableSet.builder(); + if (options.enableHeterogeneousNumericComparisons()) { + featureFlags.add( + CelEnvironment.FeatureFlag.create("cel.feature.cross_type_numeric_comparisons", true)); + } + if (options.enableQuotedIdentifierSyntax()) { + featureFlags.add( + CelEnvironment.FeatureFlag.create("cel.feature.backtick_escape_syntax", true)); + } + if (options.populateMacroCalls()) { + featureFlags.add(CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true)); + } + envBuilder.setFeatures(featureFlags.build()); + } + /** * Collects all function overloads, variable declarations and macros from the given {@link Cel} * instance and stores them in a map. */ - private void collectInventory(Set inventory, Cel cel) { + private void collectInventory(Set inventory, CelCompiler cel) { + Preconditions.checkArgument(cel instanceof EnvVisitable); ((EnvVisitable) cel) .accept( new EnvVisitor() { diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java index 8c19fcfa6..2fa8923f1 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java @@ -143,6 +143,51 @@ private CelContainer parseContainer(ParserContext ctx, Node node) { return builder.build(); } + private ImmutableSet parseFeatures( + ParserContext ctx, Node node) { + long valueId = ctx.collectMetadata(node); + if (!validateYamlType(node, YamlNodeType.LIST, YamlNodeType.TEXT)) { + ctx.reportError(valueId, "Unsupported features format"); + } + + ImmutableSet.Builder featureFlags = ImmutableSet.builder(); + + SequenceNode featureListNode = (SequenceNode) node; + for (Node featureMapNode : featureListNode.getValue()) { + long featureMapId = ctx.collectMetadata(featureMapNode); + if (!assertYamlType(ctx, featureMapId, featureMapNode, YamlNodeType.MAP)) { + continue; + } + + MappingNode featureMap = (MappingNode) featureMapNode; + String name = ""; + boolean enabled = true; + for (NodeTuple nodeTuple : featureMap.getValue()) { + Node keyNode = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(keyNode); + Node valueNode = nodeTuple.getValueNode(); + String keyName = ((ScalarNode) keyNode).getValue(); + switch (keyName) { + case "name": + name = newString(ctx, valueNode); + break; + case "enabled": + enabled = newBoolean(ctx, valueNode); + break; + default: + ctx.reportError(keyId, String.format("Unsupported feature tag: %s", keyName)); + break; + } + } + if (name.isEmpty()) { + ctx.reportError(featureMapId, "Missing required attribute(s): name"); + continue; + } + featureFlags.add(CelEnvironment.FeatureFlag.create(name, enabled)); + } + return featureFlags.build(); + } + private ImmutableSet parseAliases(ParserContext ctx, Node node) { ImmutableSet.Builder aliasSetBuilder = ImmutableSet.builder(); long valueId = ctx.collectMetadata(node); @@ -756,6 +801,9 @@ private CelEnvironment.Builder parseConfig(ParserContext ctx, Node node) { case "stdlib": builder.setStandardLibrarySubset(parseLibrarySubset(ctx, valueNode)); break; + case "features": + builder.setFeatures(parseFeatures(ctx, valueNode)); + break; default: ctx.reportError(id, "Unknown config tag: " + fieldName); // continue handling the rest of the nodes diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java index 81f206b94..2cc229dc9 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java @@ -60,6 +60,7 @@ private CelEnvironmentYamlSerializer() { CelEnvironment.LibrarySubset.OverloadSelector.class, new RepresentOverloadSelector()); this.multiRepresenters.put(CelEnvironment.Alias.class, new RepresentAlias()); this.multiRepresenters.put(CelContainer.class, new RepresentContainer()); + this.multiRepresenters.put(CelEnvironment.FeatureFlag.class, new RepresentFeatureFlag()); } public static String toYaml(CelEnvironment environment) { @@ -94,6 +95,9 @@ public Node representData(Object data) { if (environment.standardLibrarySubset().isPresent()) { configMap.put("stdlib", environment.standardLibrarySubset().get()); } + if (!environment.features().isEmpty()) { + configMap.put("features", environment.features().asList()); + } return represent(configMap.buildOrThrow()); } } @@ -258,4 +262,17 @@ public Node representData(Object data) { return represent(ImmutableMap.of("id", overloadSelector.id())); } } + + private final class RepresentFeatureFlag implements Represent { + + @Override + public Node representData(Object data) { + CelEnvironment.FeatureFlag featureFlag = (CelEnvironment.FeatureFlag) data; + return represent( + ImmutableMap.builder() + .put("name", featureFlag.name()) + .put("enabled", featureFlag.enabled()) + .buildOrThrow()); + } + } } diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java index 6bc84a48f..3386bdaae 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java @@ -100,6 +100,49 @@ public void extend_allExtensions() throws Exception { assertThat(result).isTrue(); } + @Test + public void extend_allFeatureFlags() throws Exception { + CelEnvironment environment = + CelEnvironment.newBuilder() + .setFeatures( + CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true), + CelEnvironment.FeatureFlag.create("cel.feature.backtick_escape_syntax", true), + CelEnvironment.FeatureFlag.create( + "cel.feature.cross_type_numeric_comparisons", true)) + .build(); + + Cel cel = + environment.extend( + CelFactory.standardCelBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .build(), + CelOptions.DEFAULT); + CelAbstractSyntaxTree ast = + cel.compile("[{'foo.bar': 1}, {'foo.bar': 2}].all(e, e.`foo.bar` < 2.5)").getAst(); + assertThat(ast.getSource().getMacroCalls()).hasSize(1); + boolean result = (boolean) cel.createProgram(ast).eval(); + assertThat(result).isTrue(); + } + + @Test + public void extend_unsupportedFeatureFlag_throws() throws Exception { + CelEnvironment environment = + CelEnvironment.newBuilder() + .setFeatures(CelEnvironment.FeatureFlag.create("unknown.feature", true)) + .build(); + + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> + environment.extend( + CelFactory.standardCelBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .build(), + CelOptions.DEFAULT)); + assertThat(e).hasMessageThat().contains("Unknown feature flag: unknown.feature"); + } + @Test public void extensionVersion_specific() throws Exception { CelEnvironment environment = diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java index d69d0517b..98ce55ecc 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java @@ -40,8 +40,8 @@ import dev.cel.common.types.SimpleType; import dev.cel.parser.CelUnparserFactory; import dev.cel.runtime.CelEvaluationListener; -import dev.cel.runtime.CelLateFunctionBindings; import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.CelLateFunctionBindings; import java.io.IOException; import java.net.URL; import java.util.Optional; @@ -81,6 +81,33 @@ public void environment_setBasicProperties() throws Exception { .build()); } + @Test + public void environment_setFeatures() throws Exception { + String yamlConfig = + "name: hello\n" + + "description: empty\n" + + "features:\n" + + " - name: 'cel.feature.macro_call_tracking'\n" + + " enabled: true\n" + + " - name: 'cel.feature.backtick_escape_syntax'\n" + + " enabled: false"; + + CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig); + + assertThat(environment) + .isEqualTo( + CelEnvironment.newBuilder() + .setSource(environment.source().get()) + .setName("hello") + .setDescription("empty") + .setFeatures( + ImmutableSet.of( + CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true), + CelEnvironment.FeatureFlag.create( + "cel.feature.backtick_escape_syntax", false))) + .build()); + } + @Test public void environment_setExtensions() throws Exception { String yamlConfig = @@ -672,6 +699,16 @@ private enum EnvironmentParseErrorTestcase { "ERROR: :6:7: Unsupported alias tag: unknown_tag\n" + " | unknown_tag: 'test_value'\n" + " | ......^"), + ILLEGAL_FEATURE_TAG( + "features:\n" + " - name: 'test_feature'\n" + " unknown_tag: 'test_value'\n", + "ERROR: :3:5: Unsupported feature tag: unknown_tag\n" + + " | unknown_tag: 'test_value'\n" + + " | ....^"), + MISSING_FEATURE_NAME( + "features:\n" + " - enabled: true\n", + "ERROR: :2:5: Missing required attribute(s): name\n" + + " | - enabled: true\n" + + " | ....^"), ; private final String yamlConfig; @@ -793,6 +830,7 @@ private enum EnvironmentYamlResourceTestCase { .build()) .setReturnType(TypeDecl.create("bool")) .build()))) + .setFeatures(CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true)) .build()), LIBRARY_SUBSET_ENV( diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java index 7e4be0912..1c56370b2 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java @@ -126,6 +126,9 @@ public void toYaml_success() throws Exception { FunctionSelector.create( "_+_", ImmutableSet.of("add_bytes", "add_list")))) .build()) + .setFeatures( + CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true), + CelEnvironment.FeatureFlag.create("cel.feature.backtick_escape_syntax", false)) .build(); String yamlOutput = CelEnvironmentYamlSerializer.toYaml(environment); diff --git a/checker/src/main/java/dev/cel/checker/CelCheckerBuilder.java b/checker/src/main/java/dev/cel/checker/CelCheckerBuilder.java index e19cf5b70..a7d531f88 100644 --- a/checker/src/main/java/dev/cel/checker/CelCheckerBuilder.java +++ b/checker/src/main/java/dev/cel/checker/CelCheckerBuilder.java @@ -35,6 +35,9 @@ public interface CelCheckerBuilder { @CanIgnoreReturnValue CelCheckerBuilder setOptions(CelOptions options); + /** Retrieves the currently configured {@link CelOptions} in the builder. */ + CelOptions options(); + /** * Set the {@link CelContainer} to use as the namespace for resolving CEL expression variables and * functions. diff --git a/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java b/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java index df8a82f43..ceab0fa93 100644 --- a/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java +++ b/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java @@ -202,6 +202,11 @@ public CelCheckerBuilder setOptions(CelOptions celOptions) { return this; } + @Override + public CelOptions options() { + return this.celOptions; + } + @Override public CelCheckerBuilder setContainer(CelContainer container) { checkNotNull(container); @@ -421,11 +426,6 @@ CelStandardDeclarations standardDeclarations() { return this.standardDeclarations; } - @VisibleForTesting - CelOptions options() { - return this.celOptions; - } - @VisibleForTesting CelTypeProvider celTypeProvider() { return this.celTypeProvider; diff --git a/testing/src/test/resources/environment/dump_env.yaml b/testing/src/test/resources/environment/dump_env.yaml index 6a885ea51..18f96fbcc 100644 --- a/testing/src/test/resources/environment/dump_env.yaml +++ b/testing/src/test/resources/environment/dump_env.yaml @@ -82,3 +82,8 @@ stdlib: overloads: - id: add_bytes - id: add_list +features: +- name: cel.feature.macro_call_tracking + enabled: true +- name: cel.feature.backtick_escape_syntax + enabled: false diff --git a/testing/src/test/resources/environment/extended_env.yaml b/testing/src/test/resources/environment/extended_env.yaml index fbed2b9d5..c420ad4db 100644 --- a/testing/src/test/resources/environment/extended_env.yaml +++ b/testing/src/test/resources/environment/extended_env.yaml @@ -38,6 +38,9 @@ functions: is_type_param: true return: type_name: "bool" +features: + - name: cel.feature.macro_call_tracking + enabled: true # TODO: Add support for below #validators: #- name: cel.validator.duration @@ -46,7 +49,3 @@ functions: #- name: cel.validator.nesting_comprehension_limit # config: # limit: 2 -# TODO: Add support for below -#features: -#- name: cel.feature.macro_call_tracking -# enabled: true \ No newline at end of file From e8079d0e4876c236b23de36516a8f2a6ccc2e2d2 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 24 Feb 2026 19:13:16 -0800 Subject: [PATCH 003/204] Implement optionals for planner PiperOrigin-RevId: 874895299 --- bundle/BUILD.bazel | 12 +- .../src/main/java/dev/cel/bundle/BUILD.bazel | 54 ++- .../src/main/java/dev/cel/bundle/CelImpl.java | 20 +- .../src/test/java/dev/cel/bundle/BUILD.bazel | 1 + .../cel/extensions/CelOptionalLibrary.java | 108 +++-- .../test/java/dev/cel/extensions/BUILD.bazel | 4 + .../extensions/CelOptionalLibraryTest.java | 389 +++++++++++------- runtime/BUILD.bazel | 1 - .../java/dev/cel/runtime/CelRuntimeImpl.java | 17 +- .../dev/cel/runtime/CelRuntimeLegacyImpl.java | 27 +- .../dev/cel/runtime/DefaultDispatcher.java | 93 ++++- .../dev/cel/runtime/DefaultInterpreter.java | 39 +- .../dev/cel/runtime/FunctionBindingImpl.java | 6 +- .../java/dev/cel/runtime/planner/BUILD.bazel | 46 +++ .../cel/runtime/planner/EvalCreateList.java | 32 +- .../cel/runtime/planner/EvalCreateMap.java | 42 +- .../cel/runtime/planner/EvalCreateStruct.java | 36 +- .../cel/runtime/planner/EvalOptionalOr.java | 53 +++ .../runtime/planner/EvalOptionalOrValue.java | 53 +++ .../planner/EvalOptionalSelectField.java | 89 ++++ .../cel/runtime/planner/ProgramPlanner.java | 81 +++- 21 files changed, 945 insertions(+), 258 deletions(-) create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java diff --git a/bundle/BUILD.bazel b/bundle/BUILD.bazel index 70880e532..1eaf0bec8 100644 --- a/bundle/BUILD.bazel +++ b/bundle/BUILD.bazel @@ -7,7 +7,10 @@ package( java_library( name = "cel", - exports = ["//bundle/src/main/java/dev/cel/bundle:cel"], + exports = [ + "//bundle/src/main/java/dev/cel/bundle:cel", + "//bundle/src/main/java/dev/cel/bundle:cel_factory", + ], ) java_library( @@ -29,3 +32,10 @@ java_library( name = "environment_exporter", exports = ["//bundle/src/main/java/dev/cel/bundle:environment_exporter"], ) + +java_library( + name = "cel_impl", + testonly = 1, + visibility = ["//:internal"], + exports = ["//bundle/src/main/java/dev/cel/bundle:cel_impl"], +) diff --git a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel index 4dd81fd9e..0a014ec73 100644 --- a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel +++ b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel @@ -11,8 +11,6 @@ package( CEL_SOURCES = [ "Cel.java", "CelBuilder.java", - "CelFactory.java", - "CelImpl.java", ] java_library( @@ -21,31 +19,74 @@ java_library( tags = [ ], deps = [ + "//checker:checker_legacy_environment", + "//checker:proto_type_mask", + "//checker:standard_decl", + "//common:compiler_common", + "//common:container", + "//common:options", + "//common/types:type_providers", + "//common/values:cel_value_provider", + "//compiler:compiler_builder", + "//parser:macro", + "//runtime", + "//runtime:function_binding", + "//runtime:standard_functions", + "@cel_spec//proto/cel/expr:checked_java_proto", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +java_library( + name = "cel_factory", + srcs = ["CelFactory.java"], + tags = [ + ], + deps = [ + ":cel", + ":cel_impl", "//checker", + "//common:options", + "//compiler", + "//compiler:compiler_builder", + "//parser", + "//runtime", + ], +) + +java_library( + name = "cel_impl", + srcs = ["CelImpl.java"], + tags = [ + ], + deps = [ + ":cel", "//checker:checker_builder", - "//checker:checker_legacy_environment", "//checker:proto_type_mask", "//checker:standard_decl", + "//checker:type_provider_legacy", "//common:cel_ast", "//common:cel_source", "//common:compiler_common", "//common:container", "//common:options", + "//common/annotations", "//common/internal:env_visitor", "//common/internal:file_descriptor_converter", "//common/types:cel_proto_types", "//common/types:type_providers", "//common/values:cel_value_provider", - "//compiler", "//compiler:compiler_builder", - "//parser", "//parser:macro", "//parser:parser_builder", "//runtime", "//runtime:function_binding", + "//runtime:runtime_planner_impl", "//runtime:standard_functions", "@cel_spec//proto/cel/expr:checked_java_proto", - "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", @@ -60,6 +101,7 @@ java_library( tags = [ ], deps = [ + ":cel_factory", ":environment_exception", ":required_fields_checker", "//:auto_value", diff --git a/bundle/src/main/java/dev/cel/bundle/CelImpl.java b/bundle/src/main/java/dev/cel/bundle/CelImpl.java index bc92cca7a..51fe2dc38 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelImpl.java +++ b/bundle/src/main/java/dev/cel/bundle/CelImpl.java @@ -38,6 +38,7 @@ import dev.cel.common.CelSource; import dev.cel.common.CelValidationResult; import dev.cel.common.CelVarDecl; +import dev.cel.common.annotations.Internal; import dev.cel.common.internal.EnvVisitable; import dev.cel.common.internal.EnvVisitor; import dev.cel.common.internal.FileDescriptorSetConverter; @@ -54,6 +55,7 @@ import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelRuntime; import dev.cel.runtime.CelRuntimeBuilder; +import dev.cel.runtime.CelRuntimeImpl; import dev.cel.runtime.CelRuntimeLibrary; import dev.cel.runtime.CelStandardFunctions; import java.util.Arrays; @@ -63,9 +65,14 @@ * Implementation of the synchronous CEL stack. * *

Note, the underlying {@link CelCompiler} and {@link CelRuntime} values are constructed lazily. + * + *

CEL Library Internals. Do Not Use. Consumers should use {@code CelFactory} instead. + * + *

TODO: Restrict visibility once factory is introduced */ @Immutable -final class CelImpl implements Cel, EnvVisitable { +@Internal +public final class CelImpl implements Cel, EnvVisitable { // The lazily constructed compiler and runtime values are memoized and guaranteed to be // constructed only once without side effects, thus making them effectively immutable. @@ -142,8 +149,13 @@ static CelImpl combine(CelCompiler compiler, CelRuntime runtime) { * Create a new builder for constructing a {@code CelImpl} instance. * *

By default, {@link CelOptions#DEFAULT} are enabled, as is the CEL standard environment. + * + *

CEL Library Internals. Do Not Use. Consumers should use {@code CelFactory} instead. + * + *

TODO: Restrict visibility once factory is introduced */ - static CelBuilder newBuilder( + @Internal + public static CelBuilder newBuilder( CelCompilerBuilder compilerBuilder, CelRuntimeBuilder celRuntimeBuilder) { return new CelImpl.Builder(compilerBuilder, celRuntimeBuilder); } @@ -199,6 +211,10 @@ public CelContainer container() { @Override public CelBuilder setContainer(CelContainer container) { compilerBuilder.setContainer(container); + if (runtimeBuilder instanceof CelRuntimeImpl.Builder) { + runtimeBuilder.setContainer(container); + } + return this; } diff --git a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel index cd33dd67d..ffa3322fe 100644 --- a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel +++ b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel @@ -17,6 +17,7 @@ java_library( deps = [ "//:java_truth", "//bundle:cel", + "//bundle:cel_impl", "//bundle:environment", "//bundle:environment_exception", "//bundle:environment_exporter", diff --git a/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java b/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java index baa8acb59..a3777c759 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java +++ b/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java @@ -17,6 +17,9 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.ImmutableList.toImmutableList; +import static dev.cel.common.Operator.INDEX; +import static dev.cel.common.Operator.OPTIONAL_INDEX; +import static dev.cel.common.Operator.OPTIONAL_SELECT; import static dev.cel.extensions.CelOptionalLibrary.Function.FIRST; import static dev.cel.extensions.CelOptionalLibrary.Function.HAS_VALUE; import static dev.cel.extensions.CelOptionalLibrary.Function.LAST; @@ -342,54 +345,69 @@ public void setRuntimeOptions( "optional_hasValue", Object.class, val -> ((Optional) val).isPresent()))); runtimeBuilder.addFunctionBindings( - CelFunctionBinding.from( - "select_optional_field", // This only handles map selection. Proto selection is - // special cased inside the interpreter. - Map.class, - String.class, - runtimeEquality::findInMap), - CelFunctionBinding.from( - "map_optindex_optional_value", Map.class, Object.class, runtimeEquality::findInMap), - CelFunctionBinding.from( - "optional_map_optindex_optional_value", - Optional.class, - Object.class, - (Optional optionalMap, Object key) -> - indexOptionalMap(optionalMap, key, runtimeEquality)), - CelFunctionBinding.from( - "optional_map_index_value", - Optional.class, - Object.class, - (Optional optionalMap, Object key) -> - indexOptionalMap(optionalMap, key, runtimeEquality)), - CelFunctionBinding.from( - "optional_list_index_int", - Optional.class, - Long.class, - CelOptionalLibrary::indexOptionalList), - CelFunctionBinding.from( - "list_optindex_optional_int", - List.class, - Long.class, - (List list, Long index) -> { - int castIndex = Ints.checkedCast(index); - if (castIndex < 0 || castIndex >= list.size()) { - return Optional.empty(); - } - return Optional.of(list.get(castIndex)); - }), - CelFunctionBinding.from( - "optional_list_optindex_optional_int", - Optional.class, - Long.class, - CelOptionalLibrary::indexOptionalList)); + fromOverloads( + OPTIONAL_SELECT.getFunction(), + CelFunctionBinding.from( + "select_optional_field", // This only handles map selection. Proto selection is + // special cased inside the interpreter. + Map.class, + String.class, + runtimeEquality::findInMap))); + + runtimeBuilder.addFunctionBindings( + fromOverloads( + OPTIONAL_INDEX.getFunction(), + CelFunctionBinding.from( + "list_optindex_optional_int", + List.class, + Long.class, + (List list, Long index) -> { + int castIndex = Ints.checkedCast(index); + if (castIndex < 0 || castIndex >= list.size()) { + return Optional.empty(); + } + return Optional.of(list.get(castIndex)); + }), + CelFunctionBinding.from( + "optional_list_optindex_optional_int", + Optional.class, + Long.class, + CelOptionalLibrary::indexOptionalList), + CelFunctionBinding.from( + "map_optindex_optional_value", Map.class, Object.class, runtimeEquality::findInMap), + CelFunctionBinding.from( + "optional_map_optindex_optional_value", + Optional.class, + Object.class, + (Optional optionalMap, Object key) -> + indexOptionalMap(optionalMap, key, runtimeEquality)))); + + runtimeBuilder.addFunctionBindings( + fromOverloads( + INDEX.getFunction(), + CelFunctionBinding.from( + "optional_list_index_int", + Optional.class, + Long.class, + CelOptionalLibrary::indexOptionalList), + CelFunctionBinding.from( + "optional_map_index_value", + Optional.class, + Object.class, + (Optional optionalMap, Object key) -> + indexOptionalMap(optionalMap, key, runtimeEquality)))); if (version >= 2) { runtimeBuilder.addFunctionBindings( - CelFunctionBinding.from( - "optional_list_first", Collection.class, CelOptionalLibrary::listOptionalFirst), - CelFunctionBinding.from( - "optional_list_last", Collection.class, CelOptionalLibrary::listOptionalLast)); + fromOverloads( + "first", + CelFunctionBinding.from( + "optional_list_first", Collection.class, CelOptionalLibrary::listOptionalFirst))); + runtimeBuilder.addFunctionBindings( + fromOverloads( + "last", + CelFunctionBinding.from( + "optional_list_last", Collection.class, CelOptionalLibrary::listOptionalLast))); } } diff --git a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel index d5155f662..b441c33cf 100644 --- a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel @@ -10,6 +10,8 @@ java_library( deps = [ "//:java_truth", "//bundle:cel", + "//bundle:cel_impl", + "//checker", "//common:cel_ast", "//common:compiler_common", "//common:container", @@ -30,6 +32,7 @@ java_library( "//extensions:sets", "//extensions:sets_function", "//extensions:strings", + "//parser", "//parser:macro", "//parser:unparser", "//runtime", @@ -37,6 +40,7 @@ java_library( "//runtime:interpreter_util", "//runtime:lite_runtime", "//runtime:lite_runtime_factory", + "//runtime:runtime_planner_impl", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/test:simple_java_proto", diff --git a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java index dd94333c3..0f0c649f8 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java @@ -27,6 +27,8 @@ import dev.cel.bundle.Cel; import dev.cel.bundle.CelBuilder; import dev.cel.bundle.CelFactory; +import dev.cel.bundle.CelImpl; +import dev.cel.checker.CelCheckerLegacyImpl; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; @@ -43,13 +45,17 @@ import dev.cel.common.types.TypeType; import dev.cel.common.values.CelByteString; import dev.cel.common.values.NullValue; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerImpl; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import dev.cel.parser.CelMacro; +import dev.cel.parser.CelParserImpl; import dev.cel.parser.CelStandardMacro; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeImpl; import dev.cel.runtime.InterpreterUtil; import java.time.Duration; import java.time.Instant; @@ -63,6 +69,14 @@ @SuppressWarnings({"unchecked", "SingleTestParameter"}) public class CelOptionalLibraryTest { + private enum TestMode { + PLANNER_PARSE_ONLY, + PLANNER_CHECKED, + LEGACY_CHECKED + } + + @TestParameter TestMode testMode; + @SuppressWarnings("ImmutableEnumChecker") // Test only private enum ConstantTestCases { INT("5", "0", SimpleType.INT, 5L), @@ -92,15 +106,40 @@ private enum ConstantTestCases { } } - private static CelBuilder newCelBuilder() { + private static CelBuilder plannerCelBuilder() { + // TODO: Replace with factory once available. + return CelImpl.newBuilder( + CelCompilerImpl.newBuilder( + CelParserImpl.newBuilder(), + CelCheckerLegacyImpl.newBuilder().setStandardEnvironmentEnabled(true)), + CelRuntimeImpl.newBuilder()) + // CEL-Internal-2 + .setOptions(CelOptions.current().build()); + } + + private CelBuilder newCelBuilder() { return newCelBuilder(Integer.MAX_VALUE); } - private static CelBuilder newCelBuilder(int version) { - return CelFactory.standardCelBuilder() + private CelBuilder newCelBuilder(int version) { + CelBuilder celBuilder; + switch (testMode) { + case PLANNER_PARSE_ONLY: + case PLANNER_CHECKED: + celBuilder = plannerCelBuilder(); + break; + case LEGACY_CHECKED: + celBuilder = CelFactory.standardCelBuilder(); + break; + default: + throw new IllegalArgumentException("Unknown test mode: " + testMode); + } + + return celBuilder .setOptions( CelOptions.current() .enableTimestampEpoch(true) + .enableHeterogeneousNumericComparisons(true) .build()) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) @@ -181,7 +220,7 @@ public void optionalOf_constant_success(@TestParameter ConstantTestCases testCas throws Exception { Cel cel = newCelBuilder().setResultType(OptionalType.create(testCase.type)).build(); String expression = String.format("optional.of(%s)", testCase.sourceWithNonZeroValue); - CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + CelAbstractSyntaxTree ast = compile(cel, expression); Object result = cel.createProgram(ast).eval(); @@ -198,7 +237,7 @@ public void optionalType_runtimeEquality(@TestParameter ConstantTestCases testCa .addVar("b", OptionalType.create(testCase.type)) .setResultType(SimpleType.BOOL) .build(); - CelAbstractSyntaxTree ast = cel.compile("a == b").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "a == b"); boolean result = (boolean) @@ -216,7 +255,7 @@ public void optionalType_runtimeEquality(@TestParameter ConstantTestCases testCa @Test public void optionalType_adaptsIntegerToLong_success() throws Exception { Cel cel = newCelBuilder().addVar("a", OptionalType.create(SimpleType.INT)).build(); - CelAbstractSyntaxTree ast = cel.compile("a").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "a"); Optional result = (Optional) cel.createProgram(ast).eval(ImmutableMap.of("a", Optional.of(5))); @@ -227,7 +266,7 @@ public void optionalType_adaptsIntegerToLong_success() throws Exception { @Test public void optionalType_adaptsFloatToLong_success() throws Exception { Cel cel = newCelBuilder().addVar("a", OptionalType.create(SimpleType.DOUBLE)).build(); - CelAbstractSyntaxTree ast = cel.compile("a").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "a"); Optional result = (Optional) cel.createProgram(ast).eval(ImmutableMap.of("a", Optional.of(5.5f))); @@ -239,7 +278,7 @@ public void optionalType_adaptsFloatToLong_success() throws Exception { public void optionalOf_nullValue_success() throws Exception { Cel cel = newCelBuilder().setResultType(SimpleType.DYN).build(); String expression = "optional.of(TestAllTypes{}.single_value)"; - CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + CelAbstractSyntaxTree ast = compile(cel, expression); Object result = cel.createProgram(ast).eval(); @@ -252,7 +291,7 @@ public void optionalOfNonZeroValue_withZeroValue_returnsEmptyOptionalValue( @TestParameter ConstantTestCases testCase) throws Exception { Cel cel = newCelBuilder().setResultType(OptionalType.create(testCase.type)).build(); String expression = String.format("optional.ofNonZeroValue(%s)", testCase.sourceWithZeroValue); - CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + CelAbstractSyntaxTree ast = compile(cel, expression); Object result = cel.createProgram(ast).eval(); @@ -266,7 +305,7 @@ public void optionalOfNonZeroValue_withNonZeroValue_returnsOptionalValue( Cel cel = newCelBuilder().setResultType(OptionalType.create(testCase.type)).build(); String expression = String.format("optional.ofNonZeroValue(%s)", testCase.sourceWithNonZeroValue); - CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + CelAbstractSyntaxTree ast = compile(cel, expression); Object result = cel.createProgram(ast).eval(); @@ -278,7 +317,7 @@ public void optionalOfNonZeroValue_withNonZeroValue_returnsOptionalValue( public void optionalOfNonZeroValue_withNullValue_returnsEmptyOptionalValue() throws Exception { Cel cel = newCelBuilder().setResultType(SimpleType.DYN).build(); CelAbstractSyntaxTree ast = - cel.compile("optional.ofNonZeroValue(TestAllTypes{}.single_value)").getAst(); + compile(cel, "optional.ofNonZeroValue(TestAllTypes{}.single_value)"); Object result = cel.createProgram(ast).eval(); @@ -289,7 +328,7 @@ public void optionalOfNonZeroValue_withNullValue_returnsEmptyOptionalValue() thr @Test public void optionalOfNonZeroValue_withEmptyMessage_returnsEmptyOptionalValue() throws Exception { Cel cel = newCelBuilder().setResultType(SimpleType.DYN).build(); - CelAbstractSyntaxTree ast = cel.compile("optional.ofNonZeroValue(TestAllTypes{})").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "optional.ofNonZeroValue(TestAllTypes{})"); Object result = cel.createProgram(ast).eval(); @@ -300,7 +339,7 @@ public void optionalOfNonZeroValue_withEmptyMessage_returnsEmptyOptionalValue() @Test public void optionalNone_success() throws Exception { Cel cel = newCelBuilder().setResultType(SimpleType.DYN).build(); - CelAbstractSyntaxTree ast = cel.compile("optional.none()").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "optional.none()"); Object result = cel.createProgram(ast).eval(); @@ -312,7 +351,7 @@ public void optionalNone_success() throws Exception { public void optionalValue_success(@TestParameter ConstantTestCases testCase) throws Exception { Cel cel = newCelBuilder().setResultType(testCase.type).build(); String expression = String.format("optional.of(%s).value()", testCase.sourceWithNonZeroValue); - CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + CelAbstractSyntaxTree ast = compile(cel, expression); Object result = cel.createProgram(ast).eval(); @@ -322,7 +361,7 @@ public void optionalValue_success(@TestParameter ConstantTestCases testCase) thr @Test public void optionalValue_whenOptionalValueEmpty_throws() throws Exception { Cel cel = newCelBuilder().build(); - CelAbstractSyntaxTree ast = cel.compile("optional.none().value()").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "optional.none().value()"); assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); } @@ -333,7 +372,7 @@ public void optionalHasValue_whenOptionalValuePresent_returnsTrue( Cel cel = newCelBuilder().setResultType(SimpleType.BOOL).build(); String expression = String.format("optional.of(%s).hasValue()", testCase.sourceWithNonZeroValue); - CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + CelAbstractSyntaxTree ast = compile(cel, expression); Object result = cel.createProgram(ast).eval(); @@ -346,7 +385,7 @@ public void optionalHasValue_whenOptionalValueEmpty_returnsFalse( Cel cel = newCelBuilder().setResultType(SimpleType.BOOL).build(); String expression = String.format("optional.ofNonZeroValue(%s).hasValue()", testCase.sourceWithZeroValue); - CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + CelAbstractSyntaxTree ast = compile(cel, expression); Object result = cel.createProgram(ast).eval(); @@ -361,7 +400,7 @@ public void optionalOr_success() throws Exception { .addVar("y", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) .build(); - CelRuntime.Program program = cel.createProgram(cel.compile("x.or(y)").getAst()); + CelRuntime.Program program = cel.createProgram(compile(cel, "x.or(y)")); Object resultLhs = program.eval(ImmutableMap.of("x", Optional.of(5), "y", Optional.empty())); Object resultRhs = program.eval(ImmutableMap.of("x", Optional.empty(), "y", Optional.of(10))); @@ -381,15 +420,18 @@ public void optionalOr_shortCircuits() throws Exception { CelOverloadDecl.newGlobalOverload( "error_overload", OptionalType.create(SimpleType.INT)))) .addFunctionBindings( - CelFunctionBinding.from( - "error_overload", - ImmutableList.of(), - val -> { - throw new IllegalStateException("This function should not have been called!"); - })) + CelFunctionBinding.fromOverloads( + "errorFunc", + CelFunctionBinding.from( + "error_overload", + ImmutableList.of(), + val -> { + throw new IllegalStateException( + "This function should not have been called!"); + }))) .setResultType(OptionalType.create(SimpleType.INT)) .build(); - CelRuntime.Program program = cel.createProgram(cel.compile("x.or(errorFunc())").getAst()); + CelRuntime.Program program = cel.createProgram(compile(cel, "x.or(errorFunc())")); Object resultLhs = program.eval(ImmutableMap.of("x", Optional.of(5))); @@ -398,22 +440,17 @@ public void optionalOr_shortCircuits() throws Exception { @Test public void optionalOr_producesNonOptionalValue_throws() throws Exception { - Cel cel = - CelFactory.standardCelBuilder() - .addFunctionDeclarations( - CelFunctionDecl.newFunctionDeclaration( - "or", - CelOverloadDecl.newMemberOverload( - "optional_or_optional", SimpleType.INT, SimpleType.INT, SimpleType.INT))) - .addFunctionBindings( - CelFunctionBinding.from("optional_or_optional", Long.class, Long.class, Long::sum)) - .build(); + Cel cel = newCelBuilder().addVar("x", OptionalType.create(SimpleType.INT)).build(); - CelAbstractSyntaxTree ast = cel.compile("5.or(10)").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "x.or(optional.of(10))"); CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); - assertThat(e).hasMessageThat().contains("expected optional value, found: 5"); + assertThrows( + CelEvaluationException.class, + () -> cel.createProgram(ast).eval(ImmutableMap.of("x", 5L))); + assertThat(e) + .hasMessageThat() + .contains("evaluation error at :4: No matching overload for function 'or'."); } @Test @@ -424,7 +461,7 @@ public void optionalOrValue_lhsHasValue_success(@TestParameter ConstantTestCases String.format( "optional.of(%s).orValue(%s)", testCase.sourceWithNonZeroValue, testCase.sourceWithZeroValue); - CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + CelAbstractSyntaxTree ast = compile(cel, expression); Object result = cel.createProgram(ast).eval(); @@ -441,15 +478,18 @@ public void optionalOrValue_shortCircuits() throws Exception { "errorFunc", CelOverloadDecl.newGlobalOverload("error_overload", SimpleType.INT))) .addFunctionBindings( - CelFunctionBinding.from( - "error_overload", - ImmutableList.of(), - val -> { - throw new IllegalStateException("This function should not have been called!"); - })) + CelFunctionBinding.fromOverloads( + "errorFunc", + CelFunctionBinding.from( + "error_overload", + ImmutableList.of(), + val -> { + throw new IllegalStateException( + "This function should not have been called!"); + }))) .setResultType(SimpleType.INT) .build(); - CelRuntime.Program program = cel.createProgram(cel.compile("x.orValue(errorFunc())").getAst()); + CelRuntime.Program program = cel.createProgram(compile(cel, "x.orValue(errorFunc())")); Object resultLhs = program.eval(ImmutableMap.of("x", Optional.of(5))); @@ -462,7 +502,7 @@ public void optionalOrValue_rhsHasValue_success(@TestParameter ConstantTestCases Cel cel = newCelBuilder().setResultType(testCase.type).build(); String expression = String.format("optional.none().orValue(%s)", testCase.sourceWithNonZeroValue); - CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + CelAbstractSyntaxTree ast = compile(cel, expression); Object result = cel.createProgram(ast).eval(); @@ -475,32 +515,29 @@ public void optionalOrValue_rhsHasValue_success(@TestParameter ConstantTestCases @TestParameters("{source: 5.orValue(optional.of(10))}") @TestParameters("{source: 5.orValue(optional.none())}") public void optionalOrValue_unmatchingTypes_throwsCompilationException(String source) { + if (testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + return; + } Cel cel = newCelBuilder().build(); CelValidationException e = - assertThrows(CelValidationException.class, () -> cel.compile(source).getAst()); + assertThrows(CelValidationException.class, () -> compile(cel, source)); assertThat(e).hasMessageThat().contains("found no matching overload for 'orValue'"); } @Test public void optionalOrValue_producesNonOptionalValue_throws() throws Exception { - Cel cel = - CelFactory.standardCelBuilder() - .addFunctionDeclarations( - CelFunctionDecl.newFunctionDeclaration( - "orValue", - CelOverloadDecl.newMemberOverload( - "optional_orValue_value", SimpleType.INT, SimpleType.INT, SimpleType.INT))) - .addFunctionBindings( - CelFunctionBinding.from( - "optional_orValue_value", Long.class, Long.class, Long::sum)) - .build(); + Cel cel = newCelBuilder().addVar("x", OptionalType.create(SimpleType.INT)).build(); - CelAbstractSyntaxTree ast = cel.compile("5.orValue(10)").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "x.orValue(10)"); CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); - assertThat(e).hasMessageThat().contains("expected optional value, found: 5"); + assertThrows( + CelEvaluationException.class, + () -> cel.createProgram(ast).eval(ImmutableMap.of("x", 5))); + assertThat(e) + .hasMessageThat() + .contains("evaluation error at :9: No matching overload for function 'orValue'."); } @Test @@ -508,7 +545,7 @@ public void optionalOrValue_producesNonOptionalValue_throws() throws Exception { @TestParameters("{source: optional.none().or(optional.none()).orValue(42) == 42}") public void optionalChainedFunctions_constants_success(String source) throws Exception { Cel cel = newCelBuilder().setResultType(SimpleType.BOOL).build(); - CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + CelAbstractSyntaxTree ast = compile(cel, source); boolean result = (boolean) cel.createProgram(ast).eval(); @@ -527,7 +564,7 @@ public void optionalChainedFunctions_nestedMaps_success() throws Exception { .build(); String expression = "optional.ofNonZeroValue('').or(optional.of(m.c['dashed-index'])).orValue('default value')"; - CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + CelAbstractSyntaxTree ast = compile(cel, expression); String result = (String) @@ -550,7 +587,7 @@ public void optionalChainedFunctions_nestedMapsInvalidAccess_throws() throws Exc .setResultType(SimpleType.STRING) .build(); String expression = "optional.ofNonZeroValue(m.a.z).orValue(m.c['dashed-index'])"; - CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + CelAbstractSyntaxTree ast = compile(cel, expression); CelEvaluationException e = assertThrows( @@ -567,7 +604,7 @@ public void optionalChainedFunctions_nestedMapsInvalidAccess_throws() throws Exc @Test public void optionalFieldSelection_onMap_returnsOptionalEmpty() throws Exception { Cel cel = newCelBuilder().setResultType(OptionalType.create(SimpleType.INT)).build(); - CelAbstractSyntaxTree ast = cel.compile("{'a': 2}.?x").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "{'a': 2}.?x"); Object result = cel.createProgram(ast).eval(); @@ -577,7 +614,7 @@ public void optionalFieldSelection_onMap_returnsOptionalEmpty() throws Exception @Test public void optionalFieldSelection_onMap_returnsOptionalValue() throws Exception { Cel cel = newCelBuilder().setResultType(OptionalType.create(SimpleType.INT)).build(); - CelAbstractSyntaxTree ast = cel.compile("{'a': 2}.?a").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "{'a': 2}.?a"); Optional result = (Optional) cel.createProgram(ast).eval(); @@ -591,7 +628,7 @@ public void optionalFieldSelection_onProtoMessage_returnsOptionalEmpty() throws .setResultType(OptionalType.create(SimpleType.INT)) .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) .build(); - CelAbstractSyntaxTree ast = cel.compile("msg.?single_int32").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "msg.?single_int32"); Optional result = (Optional) @@ -607,7 +644,7 @@ public void optionalFieldSelection_onProtoMessage_returnsOptionalValue() throws .setResultType(OptionalType.create(SimpleType.INT)) .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) .build(); - CelAbstractSyntaxTree ast = cel.compile("msg.?single_int32").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "msg.?single_int32"); Optional result = (Optional) @@ -621,8 +658,8 @@ public void optionalFieldSelection_onProtoMessage_returnsOptionalValue() throws public void optionalFieldSelection_onProtoMessage_listValue() throws Exception { Cel cel = newCelBuilder().build(); CelAbstractSyntaxTree ast = - cel.compile("optional.of(TestAllTypes{repeated_string: ['foo']}).?repeated_string.value()") - .getAst(); + compile( + cel, "optional.of(TestAllTypes{repeated_string: ['foo']}).?repeated_string.value()"); List result = (List) cel.createProgram(ast).eval(); @@ -633,9 +670,8 @@ public void optionalFieldSelection_onProtoMessage_listValue() throws Exception { public void optionalFieldSelection_onProtoMessage_indexValue() throws Exception { Cel cel = newCelBuilder().build(); CelAbstractSyntaxTree ast = - cel.compile( - "optional.of(TestAllTypes{repeated_string: ['foo']}).?repeated_string[0].value()") - .getAst(); + compile( + cel, "optional.of(TestAllTypes{repeated_string: ['foo']}).?repeated_string[0].value()"); String result = (String) cel.createProgram(ast).eval(); @@ -656,7 +692,7 @@ public void optionalFieldSelection_onProtoMessage_chainedSuccess() throws Except StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())))) .build(); CelAbstractSyntaxTree ast = - cel.compile("m.?c.missing.or(m.?c['dashed-index']).value().?single_int32").getAst(); + compile(cel, "m.?c.missing.or(m.?c['dashed-index']).value().?single_int32"); Optional result = (Optional) @@ -674,7 +710,10 @@ public void optionalFieldSelection_onProtoMessage_chainedSuccess() throws Except } @Test - public void optionalFieldSelection_indexerOnProtoMessage_throwsException() { + public void optionalFieldSelection_indexerOnProtoMessage_typeCheck_throwsException() { + if (testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + return; + } Cel cel = newCelBuilder() .setResultType(OptionalType.create(SimpleType.INT)) @@ -682,8 +721,7 @@ public void optionalFieldSelection_indexerOnProtoMessage_throwsException() { .build(); CelValidationException e = - assertThrows( - CelValidationException.class, () -> cel.compile("msg[?single_int32]").getAst()); + assertThrows(CelValidationException.class, () -> compile(cel, "msg[?single_int32]")); assertThat(e).hasMessageThat().contains("undeclared reference to 'single_int32'"); } @@ -696,8 +734,7 @@ public void optionalFieldSelection_onProtoMessage_presenceTest() throws Exceptio .setResultType(SimpleType.BOOL) .build(); CelAbstractSyntaxTree ast = - cel.compile("!has(msg.?single_nested_message.bb) && has(msg.?standalone_message.bb)") - .getAst(); + compile(cel, "!has(msg.?single_nested_message.bb) && has(msg.?standalone_message.bb)"); boolean result = (boolean) @@ -718,7 +755,7 @@ public void optionalFieldSelection_onProtoMessage_presenceTest() throws Exceptio public void optionalFieldSelection_onMap_hasValueReturnsBoolean( String source, boolean expectedResult) throws Exception { Cel cel = newCelBuilder().setResultType(SimpleType.BOOL).build(); - CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + CelAbstractSyntaxTree ast = compile(cel, source); boolean result = (boolean) cel.createProgram(ast).eval(); @@ -735,7 +772,7 @@ public void optionalFieldSelection_onMap_hasMacroReturnsTrue() throws Exception SimpleType.STRING, MapType.create(SimpleType.STRING, SimpleType.STRING))) .setResultType(SimpleType.BOOL) .build(); - CelAbstractSyntaxTree ast = cel.compile("has(m.?x.y)").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "has(m.?x.y)"); boolean result = (boolean) @@ -755,7 +792,7 @@ public void optionalFieldSelection_onMap_hasMacroReturnsFalse() throws Exception SimpleType.STRING, MapType.create(SimpleType.STRING, SimpleType.STRING))) .setResultType(SimpleType.BOOL) .build(); - CelAbstractSyntaxTree ast = cel.compile("has(m.?x.y)").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "has(m.?x.y)"); boolean result = (boolean) cel.createProgram(ast).eval(ImmutableMap.of("m", ImmutableMap.of())); @@ -773,7 +810,7 @@ public void optionalFieldSelection_onOptionalMap_presenceTest() throws Exception SimpleType.STRING, MapType.create(SimpleType.STRING, SimpleType.STRING)))) .setResultType(SimpleType.BOOL) .build(); - CelAbstractSyntaxTree ast = cel.compile("has(optm.c) && !has(optm.c.missing)").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "has(optm.c) && !has(optm.c.missing)"); boolean result = (boolean) @@ -798,7 +835,7 @@ public void optionalIndex_onOptionalMap_returnsOptionalValue() throws Exception SimpleType.STRING, MapType.create(SimpleType.STRING, SimpleType.STRING)))) .setResultType(OptionalType.create(SimpleType.STRING)) .build(); - CelAbstractSyntaxTree ast = cel.compile("optm.c[?'dashed-index']").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "optm.c[?'dashed-index']"); Object result = cel.createProgram(ast) @@ -821,7 +858,7 @@ public void optionalIndex_onOptionalMap_returnsOptionalEmpty() throws Exception SimpleType.STRING, MapType.create(SimpleType.STRING, SimpleType.STRING)))) .setResultType(OptionalType.create(SimpleType.STRING)) .build(); - CelAbstractSyntaxTree ast = cel.compile("optm.c[?'dashed-index']").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "optm.c[?'dashed-index']"); Object result = cel.createProgram(ast) @@ -840,7 +877,7 @@ public void optionalIndex_onMap_returnsOptionalEmpty() throws Exception { SimpleType.STRING, MapType.create(SimpleType.STRING, SimpleType.STRING))) .setResultType(OptionalType.create(SimpleType.STRING)) .build(); - CelAbstractSyntaxTree ast = cel.compile("m.c[?'dashed-index']").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "m.c[?'dashed-index']"); Object result = cel.createProgram(ast).eval(ImmutableMap.of("m", ImmutableMap.of("c", ImmutableMap.of()))); @@ -858,7 +895,7 @@ public void optionalIndex_onMap_returnsOptionalValue() throws Exception { SimpleType.STRING, MapType.create(SimpleType.STRING, SimpleType.STRING))) .setResultType(OptionalType.create(SimpleType.STRING)) .build(); - CelAbstractSyntaxTree ast = cel.compile("m.c[?'dashed-index']").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "m.c[?'dashed-index']"); Object result = cel.createProgram(ast) @@ -875,8 +912,12 @@ public void optionalIndex_onMap_returnsOptionalValue() throws Exception { @TestParameters("{source: '{?x: x}'}") public void optionalIndex_onMapWithUnknownInput_returnsUnknownResult(String source) throws Exception { + if (testMode.equals(TestMode.PLANNER_CHECKED) || testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + // TODO: Uncomment once unknowns is implemented + return; + } Cel cel = newCelBuilder().addVar("x", OptionalType.create(SimpleType.INT)).build(); - CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + CelAbstractSyntaxTree ast = compile(cel, source); Object result = cel.createProgram(ast).eval(); @@ -894,7 +935,7 @@ public void optionalIndex_onOptionalMapUsingFieldSelection_returnsOptionalValue( SimpleType.STRING, MapType.create(SimpleType.STRING, SimpleType.STRING))) .setResultType(OptionalType.create(SimpleType.STRING)) .build(); - CelAbstractSyntaxTree ast = cel.compile("{?'key': optional.of('test')}.?key").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "{?'key': optional.of('test')}.?key"); Object result = cel.createProgram(ast).eval(); @@ -908,7 +949,7 @@ public void optionalIndex_onList_returnsOptionalEmpty() throws Exception { .addVar("l", ListType.create(SimpleType.STRING)) .setResultType(OptionalType.create(SimpleType.STRING)) .build(); - CelAbstractSyntaxTree ast = cel.compile("l[?0]").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "l[?0]"); CelRuntime.Program program = cel.createProgram(ast); assertThat(program.eval(ImmutableMap.of("l", ImmutableList.of()))).isEqualTo(Optional.empty()); @@ -921,7 +962,7 @@ public void optionalIndex_onList_returnsOptionalValue() throws Exception { .addVar("l", ListType.create(SimpleType.STRING)) .setResultType(OptionalType.create(SimpleType.STRING)) .build(); - CelAbstractSyntaxTree ast = cel.compile("l[?0]").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "l[?0]"); Object result = cel.createProgram(ast).eval(ImmutableMap.of("l", ImmutableList.of("hello"))); @@ -935,7 +976,7 @@ public void optionalIndex_onOptionalList_returnsOptionalEmpty() throws Exception .addVar("optl", OptionalType.create(ListType.create(SimpleType.STRING))) .setResultType(OptionalType.create(SimpleType.STRING)) .build(); - CelAbstractSyntaxTree ast = cel.compile("optl[?0]").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "optl[?0]"); CelRuntime.Program program = cel.createProgram(ast); assertThat(program.eval(ImmutableMap.of("optl", Optional.empty()))).isEqualTo(Optional.empty()); @@ -950,7 +991,7 @@ public void optionalIndex_onOptionalList_returnsOptionalValue() throws Exception .addVar("optl", OptionalType.create(ListType.create(SimpleType.STRING))) .setResultType(OptionalType.create(SimpleType.STRING)) .build(); - CelAbstractSyntaxTree ast = cel.compile("optl[?0]").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "optl[?0]"); Object result = cel.createProgram(ast) @@ -961,12 +1002,16 @@ public void optionalIndex_onOptionalList_returnsOptionalValue() throws Exception @Test public void optionalIndex_onListWithUnknownInput_returnsUnknownResult() throws Exception { + if (testMode.equals(TestMode.PLANNER_CHECKED) || testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + // TODO: Uncomment once unknowns is implemented + return; + } Cel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(ListType.create(SimpleType.INT)) .build(); - CelAbstractSyntaxTree ast = cel.compile("[?x]").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "[?x]"); Object result = cel.createProgram(ast).eval(); @@ -980,7 +1025,7 @@ public void traditionalIndex_onOptionalList_returnsOptionalEmpty() throws Except .addVar("optl", OptionalType.create(ListType.create(SimpleType.STRING))) .setResultType(OptionalType.create(SimpleType.STRING)) .build(); - CelAbstractSyntaxTree ast = cel.compile("optl[0]").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "optl[0]"); Object result = cel.createProgram(ast).eval(ImmutableMap.of("optl", Optional.empty())); @@ -996,12 +1041,16 @@ public void traditionalIndex_onOptionalList_returnsOptionalEmpty() throws Except @TestParameters("{expression: 'optional.none().orValue(optx)'}") public void optionalChainedFunctions_lhsIsUnknown_returnsUnknown(String expression) throws Exception { + if (testMode.equals(TestMode.PLANNER_CHECKED) || testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + // TODO: Uncomment once unknowns is implemented + return; + } Cel cel = newCelBuilder() .addVar("optx", OptionalType.create(SimpleType.INT)) .addVar("x", SimpleType.INT) .build(); - CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + CelAbstractSyntaxTree ast = compile(cel, expression); Object result = cel.createProgram(ast).eval(); @@ -1021,7 +1070,7 @@ public void optionalChainedFunctions_lhsIsError_returnsError(String expression) .addVar("optx", OptionalType.create(SimpleType.INT)) .addVar("x", SimpleType.INT) .build(); - CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + CelAbstractSyntaxTree ast = compile(cel, expression); assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); } @@ -1033,7 +1082,7 @@ public void traditionalIndex_onOptionalList_returnsOptionalValue() throws Except .addVar("optl", OptionalType.create(ListType.create(SimpleType.STRING))) .setResultType(OptionalType.create(SimpleType.STRING)) .build(); - CelAbstractSyntaxTree ast = cel.compile("optl[0]").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "optl[0]"); Object result = cel.createProgram(ast) @@ -1056,7 +1105,7 @@ public void optionalFieldSelection_onMap_chainedWithSelectorAndIndexer(String so SimpleType.STRING, MapType.create(SimpleType.STRING, SimpleType.STRING))) .setResultType(SimpleType.BOOL) .build(); - CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + CelAbstractSyntaxTree ast = compile(cel, source); boolean result = (boolean) @@ -1084,7 +1133,7 @@ public void traditionalIndexSelection_onOptionalMap_chainedOperatorSuccess(Strin SimpleType.STRING, MapType.create(SimpleType.STRING, SimpleType.STRING)))) .setResultType(SimpleType.BOOL) .build(); - CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + CelAbstractSyntaxTree ast = compile(cel, source); boolean result = (boolean) @@ -1110,8 +1159,7 @@ public void traditionalIndexSelection_onOptionalMap_orChainedList() throws Excep .setResultType(SimpleType.STRING) .build(); - CelAbstractSyntaxTree ast = - cel.compile("optm.c.missing.or(optl[0]).orValue('default value')").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "optm.c.missing.or(optl[0]).orValue('default value')"); String result = (String) @@ -1129,7 +1177,7 @@ public void traditionalIndexSelection_onOptionalMap_orChainedList() throws Excep @Test public void optionalMapCreation_valueIsEmpty_returnsEmptyMap() throws Exception { Cel cel = newCelBuilder().build(); - CelAbstractSyntaxTree ast = cel.compile("{?'key': optional.none()}").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "{?'key': optional.none()}"); Map result = (Map) cel.createProgram(ast).eval(); @@ -1139,7 +1187,7 @@ public void optionalMapCreation_valueIsEmpty_returnsEmptyMap() throws Exception @Test public void optionalMapCreation_valueIsPresent_returnsMap() throws Exception { Cel cel = newCelBuilder().build(); - CelAbstractSyntaxTree ast = cel.compile("{?'key': optional.of(5)}").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "{?'key': optional.of(5)}"); Map result = (Map) cel.createProgram(ast).eval(); @@ -1161,7 +1209,7 @@ public void optionalMapCreation_withNestedMap_returnsNestedMap() throws Exceptio SimpleType.STRING, MapType.create(SimpleType.STRING, SimpleType.STRING))) .build(); CelAbstractSyntaxTree ast = - cel.compile("{?'nested_map': optional.ofNonZeroValue({?'map': m.?c})}").getAst(); + compile(cel, "{?'nested_map': optional.ofNonZeroValue({?'map': m.?c})}"); Map>> result = (Map>>) @@ -1190,8 +1238,7 @@ public void optionalMapCreation_withNestedMapContainingEmptyValue_emptyValueStri .build(); CelAbstractSyntaxTree ast = - cel.compile("{?'nested_map': optional.ofNonZeroValue({?'map': m.?c}), 'singleton': true}") - .getAst(); + compile(cel, "{?'nested_map': optional.ofNonZeroValue({?'map': m.?c}), 'singleton': true}"); Object result = cel.createProgram(ast).eval(ImmutableMap.of("m", ImmutableMap.of())); @@ -1199,11 +1246,14 @@ public void optionalMapCreation_withNestedMapContainingEmptyValue_emptyValueStri } @Test - public void optionalMapCreation_valueIsNonOptional_throws() { + public void optionalMapCreation_valueIsNonOptional_typeCheck_throws() { + if (testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + return; + } Cel cel = newCelBuilder().build(); CelValidationException e = - assertThrows(CelValidationException.class, () -> cel.compile("{?'hi': 'world'}").getAst()); + assertThrows(CelValidationException.class, () -> compile(cel, "{?'hi': 'world'}")); assertThat(e) .hasMessageThat() @@ -1217,7 +1267,7 @@ public void optionalMessageCreation_fieldValueIsEmpty_returnsEmptyMessage() thro .setResultType(StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) .build(); CelAbstractSyntaxTree ast = - cel.compile("TestAllTypes{?single_double_wrapper: optional.ofNonZeroValue(0.0)}").getAst(); + compile(cel, "TestAllTypes{?single_double_wrapper: optional.ofNonZeroValue(0.0)}"); TestAllTypes result = (TestAllTypes) cel.createProgram(ast).eval(); @@ -1228,7 +1278,7 @@ public void optionalMessageCreation_fieldValueIsEmpty_returnsEmptyMessage() thro public void optionalMessageCreation_fieldValueIsPresent_returnsMessage() throws Exception { Cel cel = newCelBuilder().build(); CelAbstractSyntaxTree ast = - cel.compile("TestAllTypes{?single_double_wrapper: optional.ofNonZeroValue(5.0)}").getAst(); + compile(cel, "TestAllTypes{?single_double_wrapper: optional.ofNonZeroValue(5.0)}"); TestAllTypes result = (TestAllTypes) cel.createProgram(ast).eval(); @@ -1253,7 +1303,7 @@ public void optionalMessageCreation_fieldValueContainsEmptyMap_returnsEmptyMessa MapType.create( SimpleType.STRING, MapType.create(SimpleType.STRING, SimpleType.STRING))) .build(); - CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + CelAbstractSyntaxTree ast = compile(cel, source); TestAllTypes result = (TestAllTypes) cel.createProgram(ast).eval(ImmutableMap.of("m", ImmutableMap.of())); @@ -1270,8 +1320,7 @@ public void optionalMessageCreation_fieldValueContainsMap_returnsEmptyMessage() MapType.create( SimpleType.STRING, MapType.create(SimpleType.STRING, SimpleType.STRING))) .build(); - CelAbstractSyntaxTree ast = - cel.compile("TestAllTypes{?map_string_string: m[?'nested']}").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "TestAllTypes{?map_string_string: m[?'nested']}"); TestAllTypes result = (TestAllTypes) @@ -1293,7 +1342,7 @@ public void optionalListCreation_allElementsAreEmpty_returnsEmptyList() throws E .addVar("x", OptionalType.create(SimpleType.INT)) .addVar("y", OptionalType.create(SimpleType.DYN)) .build(); - CelAbstractSyntaxTree ast = cel.compile("[?m.?c, ?x, ?y]").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "[?m.?c, ?x, ?y]"); List result = (List) @@ -1315,7 +1364,7 @@ public void optionalListCreation_containsEmptyElements_emptyElementsAreStripped( .addVar("x", OptionalType.create(SimpleType.INT)) .addVar("y", OptionalType.create(SimpleType.DYN)) .build(); - CelAbstractSyntaxTree ast = cel.compile("[?m.?c, ?x, ?y]").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "[?m.?c, ?x, ?y]"); List result = (List) @@ -1340,7 +1389,7 @@ public void optionalListCreation_containsMixedTypeElements_success() throws Exce .addVar("y", OptionalType.create(SimpleType.DYN)) .addVar("z", SimpleType.STRING) .build(); - CelAbstractSyntaxTree ast = cel.compile("[?m.?c, ?x, ?y, z]").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "[?m.?c, ?x, ?y, z]"); List result = (List) @@ -1362,7 +1411,10 @@ public void optionalListCreation_containsMixedTypeElements_success() throws Exce @Test public void - optionalListCreation_containsMixedTypeElements_throwsWhenHomogeneousLiteralsEnabled() { + optionalListCreation_containsMixedTypeElements_typeCheck_throwsWhenHomogeneousLiteralsEnabled() { + if (testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + return; + } Cel cel = newCelBuilder() .setOptions(CelOptions.current().enableHomogeneousLiterals(true).build()) @@ -1375,7 +1427,7 @@ public void optionalListCreation_containsMixedTypeElements_success() throws Exce .build(); CelValidationException e = - assertThrows(CelValidationException.class, () -> cel.compile("[?m.?c, ?x, ?y]").getAst()); + assertThrows(CelValidationException.class, () -> compile(cel, "[?m.?c, ?x, ?y]")); assertThat(e).hasMessageThat().contains("expected type 'map(string, string)' but found 'int'"); } @@ -1392,7 +1444,7 @@ public void optionalListCreation_withinProtoMessage_success() throws Exception { String expression = "TestAllTypes{repeated_string: ['greetings', ?m.nested.?hello], ?repeated_int32:" + " optional.ofNonZeroValue([?x, ?y])}"; - CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); + CelAbstractSyntaxTree ast = compile(cel, expression); TestAllTypes result = (TestAllTypes) @@ -1418,7 +1470,7 @@ public void optionalMapMacro_receiverIsEmpty_returnsOptionalEmpty() throws Excep .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) .build(); - CelAbstractSyntaxTree ast = cel.compile("x.optMap(y, y + 1)").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "x.optMap(y, y + 1)"); Optional result = (Optional) cel.createProgram(ast).eval(ImmutableMap.of("x", Optional.empty())); @@ -1433,7 +1485,7 @@ public void optionalMapMacro_receiverHasValue_returnsOptionalValue() throws Exce .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) .build(); - CelAbstractSyntaxTree ast = cel.compile("x.optMap(y, y + 1)").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "x.optMap(y, y + 1)"); Optional result = (Optional) cel.createProgram(ast).eval(ImmutableMap.of("x", Optional.of(42L))); @@ -1450,8 +1502,7 @@ public void optionalMapMacro_withNonIdent_throws() { .build(); CelValidationException e = - assertThrows( - CelValidationException.class, () -> cel.compile("x.optMap(y.z, y.z + 1)").getAst()); + assertThrows(CelValidationException.class, () -> compile(cel, "x.optMap(y.z, y.z + 1)")); assertThat(e).hasMessageThat().contains("optMap() variable name must be a simple identifier"); } @@ -1463,7 +1514,7 @@ public void optionalFlatMapMacro_receiverIsEmpty_returnsOptionalEmpty() throws E .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) .build(); - CelAbstractSyntaxTree ast = cel.compile("x.optFlatMap(y, optional.of(y + 1))").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "x.optFlatMap(y, optional.of(y + 1))"); Optional result = (Optional) cel.createProgram(ast).eval(ImmutableMap.of("x", Optional.empty())); @@ -1478,7 +1529,7 @@ public void optionalFlatMapMacro_receiverHasValue_returnsOptionalValue() throws .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) .build(); - CelAbstractSyntaxTree ast = cel.compile("x.optFlatMap(y, optional.of(y + 1))").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "x.optFlatMap(y, optional.of(y + 1))"); Optional result = (Optional) cel.createProgram(ast).eval(ImmutableMap.of("x", Optional.of(42L))); @@ -1494,8 +1545,7 @@ public void optionalFlatMapMacro_withOptionalOfNonZeroValue_optionalEmptyWhenVal .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) .build(); - CelAbstractSyntaxTree ast = - cel.compile("x.optFlatMap(y, optional.ofNonZeroValue(y - 1))").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "x.optFlatMap(y, optional.ofNonZeroValue(y - 1))"); Optional result = (Optional) cel.createProgram(ast).eval(ImmutableMap.of("x", Optional.of(1L))); @@ -1511,8 +1561,7 @@ public void optionalFlatMapMacro_withOptionalOfNonZeroValue_optionalValueWhenVal .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) .build(); - CelAbstractSyntaxTree ast = - cel.compile("x.optFlatMap(y, optional.ofNonZeroValue(y + 1))").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "x.optFlatMap(y, optional.ofNonZeroValue(y + 1))"); Optional result = (Optional) cel.createProgram(ast).eval(ImmutableMap.of("x", Optional.of(1L))); @@ -1521,15 +1570,18 @@ public void optionalFlatMapMacro_withOptionalOfNonZeroValue_optionalValueWhenVal } @Test - public void optionalFlatMapMacro_mappingExprIsNonOptional_throws() { + public void optionalFlatMapMacro_mappingExprIsNonOptional_typeCheck_throws() { + if (testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + return; + } + Cel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) .setResultType(OptionalType.create(SimpleType.INT)) .build(); CelValidationException e = - assertThrows( - CelValidationException.class, () -> cel.compile("x.optFlatMap(y, y + 1)").getAst()); + assertThrows(CelValidationException.class, () -> compile(cel, "x.optFlatMap(y, y + 1)")); assertThat(e).hasMessageThat().contains("found no matching overload for '_?_:_'"); } @@ -1544,7 +1596,7 @@ public void optionalFlatMapMacro_withNonIdent_throws() { CelValidationException e = assertThrows( - CelValidationException.class, () -> cel.compile("x.optFlatMap(y.z, y.z + 1)").getAst()); + CelValidationException.class, () -> compile(cel, "x.optFlatMap(y.z, y.z + 1)")); assertThat(e) .hasMessageThat() @@ -1554,7 +1606,7 @@ public void optionalFlatMapMacro_withNonIdent_throws() { @Test public void optionalType_typeResolution() throws Exception { Cel cel = newCelBuilder().build(); - CelAbstractSyntaxTree ast = cel.compile("optional_type").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "optional_type"); TypeType optionalRuntimeType = (TypeType) cel.createProgram(ast).eval(); @@ -1566,7 +1618,7 @@ public void optionalType_typeResolution() throws Exception { public void optionalType_typeComparison() throws Exception { Cel cel = newCelBuilder().build(); - CelAbstractSyntaxTree ast = cel.compile("type(optional.none()) == optional_type").getAst(); + CelAbstractSyntaxTree ast = compile(cel, "type(optional.none()) == optional_type"); assertThat(cel.createProgram(ast).eval()).isEqualTo(true); } @@ -1576,7 +1628,7 @@ public void optionalType_typeComparison() throws Exception { @TestParameters("{expression: '[\"a\",\"b\",\"c\"].first().value() == \"a\"'}") public void listFirst_success(String expression) throws Exception { Cel cel = newCelBuilder().build(); - boolean result = (boolean) cel.createProgram(cel.compile(expression).getAst()).eval(); + boolean result = (boolean) cel.createProgram(compile(cel, expression)).eval(); assertThat(result).isTrue(); } @@ -1585,15 +1637,18 @@ public void listFirst_success(String expression) throws Exception { @TestParameters("{expression: '[1, 2, 3].last().value() == 3'}") public void listLast_success(String expression) throws Exception { Cel cel = newCelBuilder().build(); - boolean result = (boolean) cel.createProgram(cel.compile(expression).getAst()).eval(); + boolean result = (boolean) cel.createProgram(compile(cel, expression)).eval(); assertThat(result).isTrue(); } @Test @TestParameters("{expression: '[1].first()', expectedError: 'undeclared reference to ''first'''}") @TestParameters("{expression: '[2].last()', expectedError: 'undeclared reference to ''last'''}") - public void listFirstAndLast_throws_earlyVersion(String expression, String expectedError) - throws Exception { + public void listFirstAndLast_typeCheck_throws_earlyVersion( + String expression, String expectedError) throws Exception { + if (testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + return; + } // Configure Cel with an earlier version of the 'optional' library, which did not support // 'first' and 'last' Cel cel = newCelBuilder(1).build(); @@ -1601,9 +1656,59 @@ public void listFirstAndLast_throws_earlyVersion(String expression, String expec assertThrows( CelValidationException.class, () -> { - cel.createProgram(cel.compile(expression).getAst()).eval(); + cel.createProgram(compile(cel, expression)).eval(); })) .hasMessageThat() .contains(expectedError); } + + @Test + public void optionalMapCreation_mapKeySetOnNonOptional_throws() { + String expression = "{?1: dyn(\"one\")}"; + Cel cel = newCelBuilder().build(); + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, () -> cel.createProgram(compile(cel, expression)).eval()); + assertThat(e) + .hasMessageThat() + .contains("Cannot initialize optional entry '1' from non-optional value one"); + } + + @Test + public void optionalListCreation_listKeySetOnNonOptional_throws() { + String expression = "[?dyn(1)]"; + Cel cel = newCelBuilder().build(); + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, () -> cel.createProgram(compile(cel, expression)).eval()); + assertThat(e) + .hasMessageThat() + .contains("Cannot initialize optional list element from non-optional value 1"); + } + + @Test + public void optionalMessageCreation_fieldKeySetOnNonOptional_throws() { + String expression = "TestAllTypes{?single_double_wrapper: dyn('foo')}"; + Cel cel = newCelBuilder().build(); + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, () -> cel.createProgram(compile(cel, expression)).eval()); + assertThat(e) + .hasMessageThat() + .contains( + "Cannot initialize optional entry 'single_double_wrapper' from non-optional value foo"); + } + + private CelAbstractSyntaxTree compile(CelCompiler compiler, String expression) + throws CelValidationException { + CelAbstractSyntaxTree ast = compiler.parse(expression).getAst(); + if (testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + return ast; + } + + return compiler.check(ast).getAst(); + } } diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index 074ef2059..72ec02d12 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -338,7 +338,6 @@ java_library( java_library( name = "runtime_planner_impl", - testonly = 1, # TODO: Move to factory when ready for exposure visibility = ["//:internal"], exports = [ "//runtime/src/main/java/dev/cel/runtime:runtime_planner_impl", diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index 44377db09..e910c77a9 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -54,10 +54,15 @@ import java.util.function.Function; import org.jspecify.annotations.Nullable; +/** + * {@link CelRuntime} implementation based on the {@link ProgramPlanner}. + * + *

CEL Library Internals. Do Not Use. + */ @AutoValue @Internal @Immutable -abstract class CelRuntimeImpl implements CelRuntime { +public abstract class CelRuntimeImpl implements CelRuntime { abstract ProgramPlanner planner(); @@ -180,7 +185,12 @@ public Object advanceEvaluation(UnknownContext context) throws CelEvaluationExce @Override public abstract Builder toRuntimeBuilder(); - static Builder newBuilder() { + /** + * CEL Library Internals. Do not use. Consumers should use {@code CelRuntimeFactory} instead. + * + *

TODO: Restrict visibility once factory is introduced + */ + public static Builder newBuilder() { return new AutoValue_CelRuntimeImpl.Builder() .setFunctionBindings(ImmutableMap.of()) .setStandardFunctions(CelStandardFunctions.newBuilder().build()) @@ -188,8 +198,9 @@ static Builder newBuilder() { .setExtensionRegistry(ExtensionRegistry.getEmptyRegistry()); } + /** Builder for {@link CelRuntimeImpl}. */ @AutoValue.Builder - abstract static class Builder implements CelRuntimeBuilder { + public abstract static class Builder implements CelRuntimeBuilder { public abstract Builder setPlanner(ProgramPlanner planner); diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java index ebd678f24..8ae4a9e3e 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java @@ -19,7 +19,6 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CanIgnoreReturnValue; import javax.annotation.concurrent.ThreadSafe; @@ -303,23 +302,23 @@ public CelRuntimeLegacyImpl build() { } } - ImmutableMap.Builder functionBindingsBuilder = - ImmutableMap.builder(); + DefaultDispatcher.Builder dispatcherBuilder = DefaultDispatcher.newBuilder(); for (CelFunctionBinding standardFunctionBinding : newStandardFunctionBindings(runtimeEquality)) { - functionBindingsBuilder.put( - standardFunctionBinding.getOverloadId(), standardFunctionBinding); + dispatcherBuilder.addOverload( + standardFunctionBinding.getOverloadId(), + standardFunctionBinding.getArgTypes(), + standardFunctionBinding.isStrict(), + standardFunctionBinding.getDefinition()); } - functionBindingsBuilder.putAll(customFunctionBindings); - - DefaultDispatcher.Builder dispatcherBuilder = DefaultDispatcher.newBuilder(); - functionBindingsBuilder - .buildOrThrow() - .forEach( - (String overloadId, CelFunctionBinding func) -> - dispatcherBuilder.addOverload( - overloadId, func.getArgTypes(), func.isStrict(), func.getDefinition())); + for (CelFunctionBinding customBinding : customFunctionBindings.values()) { + dispatcherBuilder.addOverload( + customBinding.getOverloadId(), + customBinding.getArgTypes(), + customBinding.isStrict(), + customBinding.getDefinition()); + } RuntimeTypeProvider runtimeTypeProvider; diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java index 0d13f13be..35e3b76a3 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java @@ -17,10 +17,11 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import com.google.auto.value.AutoBuilder; +import com.google.auto.value.AutoValue; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.Immutable; import dev.cel.common.CelErrorCode; @@ -29,6 +30,7 @@ import dev.cel.runtime.FunctionBindingImpl.DynamicDispatchOverload; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -124,16 +126,28 @@ Optional findSingleNonStrictOverload(List overloadI } public static Builder newBuilder() { - return new AutoBuilder_DefaultDispatcher_Builder(); + return new Builder(); } /** Builder for {@link DefaultDispatcher}. */ - @AutoBuilder(ofClass = DefaultDispatcher.class) - public abstract static class Builder { + public static class Builder { - abstract ImmutableMap overloads(); + @AutoValue + @Immutable + abstract static class OverloadEntry { + abstract ImmutableList> argTypes(); - abstract ImmutableMap.Builder overloadsBuilder(); + abstract boolean isStrict(); + + abstract CelFunctionOverload overload(); + + private static OverloadEntry of( + ImmutableList> argTypes, boolean isStrict, CelFunctionOverload overload) { + return new AutoValue_DefaultDispatcher_Builder_OverloadEntry(argTypes, isStrict, overload); + } + } + + private final Map overloads; @CanIgnoreReturnValue public Builder addOverload( @@ -146,18 +160,67 @@ public Builder addOverload( checkNotNull(argTypes); checkNotNull(overload); - overloadsBuilder() - .put( - overloadId, - CelResolvedOverload.of( - overloadId, - args -> guardedOp(overloadId, args, argTypes, isStrict, overload), - isStrict, - argTypes)); + OverloadEntry newEntry = OverloadEntry.of(argTypes, isStrict, overload); + + overloads.merge( + overloadId, + newEntry, + (existing, incoming) -> mergeDynamicDispatchesOrThrow(overloadId, existing, incoming)); + return this; } - public abstract DefaultDispatcher build(); + private OverloadEntry mergeDynamicDispatchesOrThrow( + String overloadId, OverloadEntry existing, OverloadEntry incoming) { + if (existing.overload() instanceof DynamicDispatchOverload + && incoming.overload() instanceof DynamicDispatchOverload) { + + DynamicDispatchOverload existingOverload = (DynamicDispatchOverload) existing.overload(); + DynamicDispatchOverload incomingOverload = (DynamicDispatchOverload) incoming.overload(); + + DynamicDispatchOverload mergedOverload = + new DynamicDispatchOverload( + overloadId, + ImmutableSet.builder() + .addAll(existingOverload.getOverloadBindings()) + .addAll(incomingOverload.getOverloadBindings()) + .build()); + + boolean isStrict = + mergedOverload.getOverloadBindings().stream().allMatch(CelFunctionBinding::isStrict); + + return OverloadEntry.of(incoming.argTypes(), isStrict, mergedOverload); + } + + throw new IllegalArgumentException("Duplicate overload ID binding: " + overloadId); + } + + public DefaultDispatcher build() { + ImmutableMap.Builder resolvedOverloads = ImmutableMap.builder(); + for (Map.Entry entry : overloads.entrySet()) { + String overloadId = entry.getKey(); + OverloadEntry overloadEntry = entry.getValue(); + resolvedOverloads.put( + overloadId, + CelResolvedOverload.of( + overloadId, + args -> + guardedOp( + overloadId, + args, + overloadEntry.argTypes(), + overloadEntry.isStrict(), + overloadEntry.overload()), + overloadEntry.isStrict(), + overloadEntry.argTypes())); + } + + return new DefaultDispatcher(resolvedOverloads.buildOrThrow()); + } + + private Builder() { + this.overloads = new HashMap<>(); + } } /** Creates an invocation guard around the overload definition. */ diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java index e49658190..9abc3716c 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java @@ -431,9 +431,9 @@ private IntermediateResult evalCall(ExecutionFrame frame, CelExpr expr, CelCall case "type": return evalType(frame, callExpr); case "optional_or_optional": - return evalOptionalOr(frame, callExpr); + return evalOptionalOr(frame, expr); case "optional_orValue_value": - return evalOptionalOrValue(frame, callExpr); + return evalOptionalOrValue(frame, expr); case "select_optional_field": Optional result = maybeEvalOptionalSelectField(frame, expr, callExpr); if (result.isPresent()) { @@ -721,19 +721,19 @@ private IntermediateResult evalType(ExecutionFrame frame, CelCall callExpr) typeResolver.resolveObjectType(argResult.value(), checkedTypeValue)); } - private IntermediateResult evalOptionalOr(ExecutionFrame frame, CelCall callExpr) + private IntermediateResult evalOptionalOr(ExecutionFrame frame, CelExpr expr) throws CelEvaluationException { - return evalOptionalOrInternal(frame, callExpr, /* unwrapOptional= */ false); + return evalOptionalOrInternal(frame, expr, /* unwrapOptional= */ false); } - private IntermediateResult evalOptionalOrValue(ExecutionFrame frame, CelCall callExpr) + private IntermediateResult evalOptionalOrValue(ExecutionFrame frame, CelExpr expr) throws CelEvaluationException { - return evalOptionalOrInternal(frame, callExpr, /* unwrapOptional= */ true); + return evalOptionalOrInternal(frame, expr, /* unwrapOptional= */ true); } private IntermediateResult evalOptionalOrInternal( - ExecutionFrame frame, CelCall callExpr, boolean unwrapOptional) - throws CelEvaluationException { + ExecutionFrame frame, CelExpr expr, boolean unwrapOptional) throws CelEvaluationException { + CelCall callExpr = expr.call(); CelExpr lhsExpr = callExpr .target() @@ -746,10 +746,11 @@ private IntermediateResult evalOptionalOrInternal( } if (!(lhsResult.value() instanceof Optional)) { + String functionName = unwrapOptional ? "orValue" : "or"; throw CelEvaluationExceptionBuilder.newBuilder( - "expected optional value, found: %s", lhsResult.value()) + "No matching overload for function '%s'.", functionName) .setErrorCode(CelErrorCode.INVALID_ARGUMENT) - .setMetadata(metadata, lhsExpr.id()) + .setMetadata(metadata, expr.id()) .build(); } @@ -832,6 +833,11 @@ private IntermediateResult evalList(ExecutionFrame frame, CelExpr unusedExpr, Ce // optionals. && optionalIndicesSet.contains(i) && !isUnknownValue(value)) { + if (!(value instanceof Optional)) { + throw new IllegalArgumentException( + String.format( + "Cannot initialize optional list element from non-optional value %s", value)); + } Optional optionalVal = (Optional) value; if (!optionalVal.isPresent()) { continue; @@ -870,6 +876,12 @@ private IntermediateResult evalMap(ExecutionFrame frame, CelMap mapExpr) Object value = valueResult.value(); if (entry.optionalEntry() && !isUnknownValue(value)) { + if (!(value instanceof Optional)) { + throw new IllegalArgumentException( + String.format( + "Cannot initialize optional entry '%s' from non-optional value %s", + keyResult.value(), value)); + } Optional optionalVal = (Optional) value; if (!optionalVal.isPresent()) { // This is a no-op currently but will be semantically correct when extended proto @@ -905,6 +917,13 @@ private IntermediateResult evalStruct(ExecutionFrame frame, CelExpr expr, CelStr Object value = fieldResult.value(); if (entry.optionalEntry()) { + if (!(value instanceof Optional)) { + throw new IllegalArgumentException( + String.format( + "Cannot initialize optional entry 'single_double_wrapper' from non-optional" + + " value %s", + value)); + } Optional optionalVal = (Optional) value; if (!optionalVal.isPresent()) { // This is a no-op currently but will be semantically correct when extended proto diff --git a/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java b/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java index 48c0eb47a..faea853f8 100644 --- a/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java @@ -145,7 +145,11 @@ public Object apply(Object[] args) throws CelEvaluationException { .collect(toImmutableList())); } - private DynamicDispatchOverload( + ImmutableSet getOverloadBindings() { + return overloadBindings; + } + + DynamicDispatchOverload( String functionName, ImmutableSet overloadBindings) { this.functionName = functionName; this.overloadBindings = overloadBindings; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index 0a7ebbfb3..b827afed5 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -24,6 +24,9 @@ java_library( ":eval_create_struct", ":eval_fold", ":eval_late_bound_call", + ":eval_optional_or", + ":eval_optional_or_value", + ":eval_optional_select_field", ":eval_or", ":eval_test_only", ":eval_unary", @@ -54,6 +57,7 @@ java_library( "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", ], ) @@ -412,3 +416,45 @@ java_library( "@maven//:com_google_errorprone_error_prone_annotations", ], ) + +java_library( + name = "eval_optional_or", + srcs = ["EvalOptionalOr.java"], + deps = [ + ":eval_helpers", + ":execution_frame", + ":planned_interpretable", + "//common/exceptions:overload_not_found", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_optional_or_value", + srcs = ["EvalOptionalOrValue.java"], + deps = [ + ":eval_helpers", + ":execution_frame", + ":planned_interpretable", + "//common/exceptions:overload_not_found", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_optional_select_field", + srcs = ["EvalOptionalSelectField.java"], + deps = [ + ":eval_helpers", + ":execution_frame", + ":planned_interpretable", + "//common/values", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java index 389a21a82..773272ea3 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java @@ -18,29 +18,49 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.GlobalResolver; +import java.util.Optional; @Immutable final class EvalCreateList extends PlannedInterpretable { - // Array contents are not mutated @SuppressWarnings("Immutable") private final PlannedInterpretable[] values; + @SuppressWarnings("Immutable") + private final boolean[] isOptional; + @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(values.length); - for (PlannedInterpretable value : values) { - builder.add(EvalHelpers.evalStrictly(value, resolver, frame)); + for (int i = 0; i < values.length; i++) { + Object element = EvalHelpers.evalStrictly(values[i], resolver, frame); + + if (isOptional[i]) { + if (!(element instanceof Optional)) { + throw new IllegalArgumentException( + String.format( + "Cannot initialize optional list element from non-optional value %s", element)); + } + + Optional opt = (Optional) element; + if (!opt.isPresent()) { + continue; + } + element = opt.get(); + } + + builder.add(element); } return builder.build(); } - static EvalCreateList create(long exprId, PlannedInterpretable[] values) { - return new EvalCreateList(exprId, values); + static EvalCreateList create(long exprId, PlannedInterpretable[] values, boolean[] isOptional) { + return new EvalCreateList(exprId, values, isOptional); } - private EvalCreateList(long exprId, PlannedInterpretable[] values) { + private EvalCreateList(long exprId, PlannedInterpretable[] values, boolean[] isOptional) { super(exprId); this.values = values; + this.isOptional = isOptional; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java index 4c5a1f0bf..1ab0f7e5b 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java @@ -22,6 +22,7 @@ import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.GlobalResolver; import java.util.HashSet; +import java.util.Optional; @Immutable final class EvalCreateMap extends PlannedInterpretable { @@ -34,6 +35,10 @@ final class EvalCreateMap extends PlannedInterpretable { @SuppressWarnings("Immutable") private final PlannedInterpretable[] values; + // Array contents are not mutated + @SuppressWarnings("Immutable") + private final boolean[] isOptional; + @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { ImmutableMap.Builder builder = @@ -42,26 +47,55 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval for (int i = 0; i < keys.length; i++) { Object key = keys[i].eval(resolver, frame); + Object val = values[i].eval(resolver, frame); if (!keysSeen.add(key)) { throw new LocalizedEvaluationException(CelDuplicateKeyException.of(key), keys[i].exprId()); } - builder.put(key, values[i].eval(resolver, frame)); + if (isOptional[i]) { + if (!(val instanceof Optional)) { + throw new IllegalArgumentException( + String.format( + "Cannot initialize optional entry '%s' from non-optional value %s", key, val)); + } + + Optional opt = (Optional) val; + if (!opt.isPresent()) { + // This is a no-op currently but will be semantically correct when extended proto + // support allows proto mutation. + keysSeen.remove(key); + continue; + } + val = opt.get(); + } else { + System.out.println(); + } + + builder.put(key, val); } return builder.buildOrThrow(); } static EvalCreateMap create( - long exprId, PlannedInterpretable[] keys, PlannedInterpretable[] values) { - return new EvalCreateMap(exprId, keys, values); + long exprId, + PlannedInterpretable[] keys, + PlannedInterpretable[] values, + boolean[] isOptional) { + return new EvalCreateMap(exprId, keys, values, isOptional); } - private EvalCreateMap(long exprId, PlannedInterpretable[] keys, PlannedInterpretable[] values) { + private EvalCreateMap( + long exprId, + PlannedInterpretable[] keys, + PlannedInterpretable[] values, + boolean[] isOptional) { super(exprId); Preconditions.checkArgument(keys.length == values.length); + Preconditions.checkArgument(keys.length == isOptional.length); this.keys = keys; this.values = values; + this.isOptional = isOptional; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java index 7d03854c2..4edc87b79 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Optional; @Immutable final class EvalCreateStruct extends PlannedInterpretable { @@ -38,11 +39,35 @@ final class EvalCreateStruct extends PlannedInterpretable { @SuppressWarnings("Immutable") private final PlannedInterpretable[] values; + // Array contents are not mutated + @SuppressWarnings("Immutable") + private final boolean[] isOptional; + @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { Map fieldValues = new HashMap<>(); for (int i = 0; i < keys.length; i++) { Object value = values[i].eval(resolver, frame); + + if (isOptional[i]) { + if (!(value instanceof Optional)) { + throw new IllegalArgumentException( + String.format( + "Cannot initialize optional entry 'single_double_wrapper' from non-optional value" + + " %s", + value)); + } + + Optional opt = (Optional) value; + if (!opt.isPresent()) { + // This is a no-op currently but will be semantically correct when extended proto + // support allows proto mutation. + fieldValues.remove(keys[i]); + continue; + } + value = opt.get(); + } + fieldValues.put(keys[i], value); } @@ -54,7 +79,7 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval () -> new IllegalArgumentException("Type name not found: " + structType.name())); if (value instanceof StructValue) { - return ((StructValue) value).value(); + return ((StructValue) value).value(); } return value; @@ -65,8 +90,9 @@ static EvalCreateStruct create( CelValueProvider valueProvider, CelType structType, String[] keys, - PlannedInterpretable[] values) { - return new EvalCreateStruct(exprId, valueProvider, structType, keys, values); + PlannedInterpretable[] values, + boolean[] isOptional) { + return new EvalCreateStruct(exprId, valueProvider, structType, keys, values, isOptional); } private EvalCreateStruct( @@ -74,11 +100,13 @@ private EvalCreateStruct( CelValueProvider valueProvider, CelType structType, String[] keys, - PlannedInterpretable[] values) { + PlannedInterpretable[] values, + boolean[] isOptional) { super(exprId); this.valueProvider = valueProvider; this.structType = structType; this.keys = keys; this.values = values; + this.isOptional = isOptional; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.java new file mode 100644 index 000000000..70009d567 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.java @@ -0,0 +1,53 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.exceptions.CelOverloadNotFoundException; +import dev.cel.runtime.GlobalResolver; +import java.util.Optional; + +@Immutable +final class EvalOptionalOr extends PlannedInterpretable { + private final PlannedInterpretable lhs; + private final PlannedInterpretable rhs; + + @Override + public Object eval(GlobalResolver resolver, ExecutionFrame frame) { + Object lhsValue = EvalHelpers.evalStrictly(lhs, resolver, frame); + + if (!(lhsValue instanceof Optional)) { + throw new CelOverloadNotFoundException("or"); + } + + Optional optionalLhs = (Optional) lhsValue; + if (optionalLhs.isPresent()) { + return optionalLhs; + } + + return EvalHelpers.evalStrictly(rhs, resolver, frame); + } + + static EvalOptionalOr create(long exprId, PlannedInterpretable lhs, PlannedInterpretable rhs) { + return new EvalOptionalOr(exprId, lhs, rhs); + } + + private EvalOptionalOr(long exprId, PlannedInterpretable lhs, PlannedInterpretable rhs) { + super(exprId); + this.lhs = Preconditions.checkNotNull(lhs); + this.rhs = Preconditions.checkNotNull(rhs); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.java new file mode 100644 index 000000000..7a4940c7c --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.java @@ -0,0 +1,53 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.exceptions.CelOverloadNotFoundException; +import dev.cel.runtime.GlobalResolver; +import java.util.Optional; + +@Immutable +final class EvalOptionalOrValue extends PlannedInterpretable { + private final PlannedInterpretable lhs; + private final PlannedInterpretable rhs; + + @Override + public Object eval(GlobalResolver resolver, ExecutionFrame frame) { + Object lhsValue = EvalHelpers.evalStrictly(lhs, resolver, frame); + if (!(lhsValue instanceof Optional)) { + throw new CelOverloadNotFoundException("orValue"); + } + + Optional optionalLhs = (Optional) lhsValue; + if (optionalLhs.isPresent()) { + return optionalLhs.get(); + } + + return EvalHelpers.evalStrictly(rhs, resolver, frame); + } + + static EvalOptionalOrValue create( + long exprId, PlannedInterpretable lhs, PlannedInterpretable rhs) { + return new EvalOptionalOrValue(exprId, lhs, rhs); + } + + private EvalOptionalOrValue(long exprId, PlannedInterpretable lhs, PlannedInterpretable rhs) { + super(exprId); + this.lhs = Preconditions.checkNotNull(lhs); + this.rhs = Preconditions.checkNotNull(rhs); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java new file mode 100644 index 000000000..bc14149f3 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.values.CelValueConverter; +import dev.cel.common.values.SelectableValue; +import dev.cel.runtime.GlobalResolver; +import java.util.Map; +import java.util.Optional; + +@Immutable +final class EvalOptionalSelectField extends PlannedInterpretable { + private final PlannedInterpretable operand; + private final PlannedInterpretable selectAttribute; + private final String field; + private final CelValueConverter celValueConverter; + + @Override + public Object eval(GlobalResolver resolver, ExecutionFrame frame) { + Object operandValue = EvalHelpers.evalStrictly(operand, resolver, frame); + + if (operandValue instanceof Optional) { + Optional opt = (Optional) operandValue; + if (!opt.isPresent()) { + return Optional.empty(); + } + operandValue = opt.get(); + } + + Object runtimeOperandValue = celValueConverter.toRuntimeValue(operandValue); + boolean hasField = false; + + if (runtimeOperandValue instanceof SelectableValue) { + // Guaranteed to be a string. Anything other than string is an error. + @SuppressWarnings("unchecked") + SelectableValue selectableValue = (SelectableValue) runtimeOperandValue; + hasField = selectableValue.find(field).isPresent(); + } else if (runtimeOperandValue instanceof Map) { + hasField = ((Map) runtimeOperandValue).containsKey(field); + } + if (!hasField) { + return Optional.empty(); + } + + Object resultValue = EvalHelpers.evalStrictly(selectAttribute, resolver, frame); + + if (resultValue instanceof Optional) { + return resultValue; + } + + return Optional.of(resultValue); + } + + static EvalOptionalSelectField create( + long exprId, + PlannedInterpretable operand, + String field, + PlannedInterpretable selectAttribute, + CelValueConverter celValueConverter) { + return new EvalOptionalSelectField(exprId, operand, field, selectAttribute, celValueConverter); + } + + private EvalOptionalSelectField( + long exprId, + PlannedInterpretable operand, + String field, + PlannedInterpretable selectAttribute, + CelValueConverter celValueConverter) { + super(exprId); + this.operand = Preconditions.checkNotNull(operand); + this.field = Preconditions.checkNotNull(field); + this.selectAttribute = Preconditions.checkNotNull(selectAttribute); + this.celValueConverter = Preconditions.checkNotNull(celValueConverter); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index fc22d4f10..7935e4838 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -52,6 +52,7 @@ import java.util.HashMap; import java.util.NoSuchElementException; import java.util.Optional; +import org.jspecify.annotations.Nullable; /** * {@code ProgramPlanner} resolves functions, types, and identifiers at plan time given a @@ -244,6 +245,13 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { resolvedOverload = dispatcher.findOverload(functionName).orElse(null); } + PlannedInterpretable optionalCall = + maybeInterceptOptionalCalls(resolvedOverload, functionName, evaluatedArgs, expr) + .orElse(null); + if (optionalCall != null) { + return optionalCall; + } + if (resolvedOverload == null) { if (!lateBoundFunctionNames.contains(functionName)) { CelReference reference = ctx.referenceMap().get(expr.id()); @@ -274,6 +282,62 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { } } + /** + * Intercepts a potential optional function call. + * + *

This is analogous to cel-go's decorator. This could be moved to {@code CelOptionalLibrary} + * once we add the support for it. + */ + private Optional maybeInterceptOptionalCalls( + @Nullable CelResolvedOverload resolvedOverload, + String functionName, + PlannedInterpretable[] evaluatedArgs, + CelExpr expr) { + if (evaluatedArgs.length != 2) { + return Optional.empty(); + } + + String overloadId = resolvedOverload == null ? "" : resolvedOverload.getOverloadId(); + + switch (functionName) { + case "or": + if (overloadId.isEmpty() || overloadId.equals("optional_or_optional")) { + return Optional.of(EvalOptionalOr.create(expr.id(), evaluatedArgs[0], evaluatedArgs[1])); + } + + return Optional.empty(); + case "orValue": + if (overloadId.isEmpty() || overloadId.equals("optional_orValue_value")) { + return Optional.of( + EvalOptionalOrValue.create(expr.id(), evaluatedArgs[0], evaluatedArgs[1])); + } + + return Optional.empty(); + default: + break; + } + + if (Operator.OPTIONAL_SELECT.getFunction().equals(functionName)) { + String field = expr.call().args().get(1).constant().stringValue(); + InterpretableAttribute attribute; + if (evaluatedArgs[0] instanceof EvalAttribute) { + attribute = (EvalAttribute) evaluatedArgs[0]; + } else { + attribute = + EvalAttribute.create( + expr.id(), attributeFactory.newRelativeAttribute(evaluatedArgs[0])); + } + Qualifier qualifier = StringQualifier.create(field); + PlannedInterpretable selectAttribute = attribute.addQualifier(expr.id(), qualifier); + + return Optional.of( + EvalOptionalSelectField.create( + expr.id(), evaluatedArgs[0], field, selectAttribute, celValueConverter)); + } + + return Optional.empty(); + } + private PlannedInterpretable planCreateStruct(CelExpr celExpr, PlannerContext ctx) { CelStruct struct = celExpr.struct(); CelType structType = resolveStructType(struct); @@ -281,19 +345,21 @@ private PlannedInterpretable planCreateStruct(CelExpr celExpr, PlannerContext ct ImmutableList entries = struct.entries(); String[] keys = new String[entries.size()]; PlannedInterpretable[] values = new PlannedInterpretable[entries.size()]; + boolean[] isOptional = new boolean[entries.size()]; for (int i = 0; i < entries.size(); i++) { Entry entry = entries.get(i); keys[i] = entry.fieldKey(); values[i] = plan(entry.value(), ctx); + isOptional[i] = entry.optionalEntry(); } - return EvalCreateStruct.create(celExpr.id(), valueProvider, structType, keys, values); + return EvalCreateStruct.create( + celExpr.id(), valueProvider, structType, keys, values, isOptional); } private PlannedInterpretable planCreateList(CelExpr celExpr, PlannerContext ctx) { CelList list = celExpr.list(); - ImmutableList elements = list.elements(); PlannedInterpretable[] values = new PlannedInterpretable[elements.size()]; @@ -301,7 +367,12 @@ private PlannedInterpretable planCreateList(CelExpr celExpr, PlannerContext ctx) values[i] = plan(elements.get(i), ctx); } - return EvalCreateList.create(celExpr.id(), values); + boolean[] isOptional = new boolean[elements.size()]; + for (int optionalIndex : list.optionalIndices()) { + isOptional[optionalIndex] = true; + } + + return EvalCreateList.create(celExpr.id(), values, isOptional); } private PlannedInterpretable planCreateMap(CelExpr celExpr, PlannerContext ctx) { @@ -310,14 +381,16 @@ private PlannedInterpretable planCreateMap(CelExpr celExpr, PlannerContext ctx) ImmutableList entries = map.entries(); PlannedInterpretable[] keys = new PlannedInterpretable[entries.size()]; PlannedInterpretable[] values = new PlannedInterpretable[entries.size()]; + boolean[] isOptional = new boolean[entries.size()]; for (int i = 0; i < entries.size(); i++) { CelMap.Entry entry = entries.get(i); keys[i] = plan(entry.key(), ctx); values[i] = plan(entry.value(), ctx); + isOptional[i] = entry.optionalEntry(); } - return EvalCreateMap.create(celExpr.id(), keys, values); + return EvalCreateMap.create(celExpr.id(), keys, values, isOptional); } private PlannedInterpretable planComprehension(CelExpr expr, PlannerContext ctx) { From 50063902f8114b1c331ef81a3b9e3007082d8d9a Mon Sep 17 00:00:00 2001 From: Jonathan Tatum Date: Wed, 25 Feb 2026 14:41:09 -0800 Subject: [PATCH 004/204] Add support for importing/exporting common limits to YAML environment configs. PiperOrigin-RevId: 875343469 --- .../java/dev/cel/bundle/CelEnvironment.java | 82 ++++++++++++++++--- .../cel/bundle/CelEnvironmentExporter.java | 17 ++++ .../cel/bundle/CelEnvironmentYamlParser.java | 68 +++++++++++++++ .../bundle/CelEnvironmentYamlSerializer.java | 17 ++++ .../bundle/CelEnvironmentExporterTest.java | 29 +++++++ .../dev/cel/bundle/CelEnvironmentTest.java | 50 +++++++++++ .../bundle/CelEnvironmentYamlParserTest.java | 57 +++++++++++++ .../CelEnvironmentYamlSerializerTest.java | 4 + .../test/resources/environment/dump_env.yaml | 7 ++ .../resources/environment/extended_env.yaml | 4 + 10 files changed, 323 insertions(+), 12 deletions(-) diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java index 7ec2149a7..c8e424217 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java @@ -53,6 +53,7 @@ import dev.cel.runtime.CelRuntimeLibrary; import java.util.Arrays; import java.util.Optional; +import java.util.function.ObjIntConsumer; /** * CelEnvironment is a native representation of a CEL environment for compiler and runtime. This @@ -74,6 +75,24 @@ public abstract class CelEnvironment { "strings", CanonicalCelExtension.STRINGS, "comprehensions", CanonicalCelExtension.COMPREHENSIONS); + private static final ImmutableMap> LIMIT_HANDLERS = + ImmutableMap.of( + "cel.limit.expression_code_points", + (options, value) -> options.maxExpressionCodePointSize(value), + "cel.limit.parse_error_recovery", + (options, value) -> options.maxParseErrorRecoveryLimit(value), + "cel.limit.parse_recursion_depth", + (options, value) -> options.maxParseRecursionDepth(value)); + + private static final ImmutableMap FEATURE_HANDLERS = + ImmutableMap.of( + "cel.feature.macro_call_tracking", + (options, enabled) -> options.populateMacroCalls(enabled), + "cel.feature.backtick_escape_syntax", + (options, enabled) -> options.enableQuotedIdentifierSyntax(enabled), + "cel.feature.cross_type_numeric_comparisons", + (options, enabled) -> options.enableHeterogeneousNumericComparisons(enabled)); + /** Environment source in textual format (ex: textproto, YAML). */ public abstract Optional source(); @@ -112,6 +131,9 @@ public abstract class CelEnvironment { /** Feature flags to enable in the environment. */ public abstract ImmutableSet features(); + /** Limits to set in the environment. */ + public abstract ImmutableSet limits(); + /** Builder for {@link CelEnvironment}. */ @AutoValue.Builder public abstract static class Builder { @@ -168,7 +190,14 @@ public Builder setFeatures(FeatureFlag... featureFlags) { return setFeatures(ImmutableSet.copyOf(featureFlags)); } - public abstract Builder setFeatures(ImmutableSet macros); + public abstract Builder setFeatures(ImmutableSet featureFlags); + + @CanIgnoreReturnValue + public Builder setLimits(Limit... limits) { + return setLimits(ImmutableSet.copyOf(limits)); + } + + public abstract Builder setLimits(ImmutableSet limits); abstract CelEnvironment autoBuild(); @@ -200,13 +229,14 @@ public static Builder newBuilder() { .setContainer(CelContainer.ofName("")) .setVariables(ImmutableSet.of()) .setFunctions(ImmutableSet.of()) - .setFeatures(ImmutableSet.of()); + .setFeatures(ImmutableSet.of()) + .setLimits(ImmutableSet.of()); } /** Extends the provided {@link CelCompiler} environment with this configuration. */ public CelCompiler extend(CelCompiler celCompiler, CelOptions celOptions) throws CelEnvironmentException { - celOptions = applyFeatureFlags(celOptions); + celOptions = applyEnvironmentOptions(celOptions); try { CelTypeProvider celTypeProvider = celCompiler.getTypeProvider(); CelCompilerBuilder compilerBuilder = @@ -236,7 +266,7 @@ public CelCompiler extend(CelCompiler celCompiler, CelOptions celOptions) /** Extends the provided {@link Cel} environment with this configuration. */ public Cel extend(Cel cel, CelOptions celOptions) throws CelEnvironmentException { - celOptions = applyFeatureFlags(celOptions); + celOptions = applyEnvironmentOptions(celOptions); try { // Casting is necessary to only extend the compiler here CelCompiler celCompiler = extend((CelCompiler) cel, celOptions); @@ -249,18 +279,22 @@ public Cel extend(Cel cel, CelOptions celOptions) throws CelEnvironmentException } } - private CelOptions applyFeatureFlags(CelOptions celOptions) { + private CelOptions applyEnvironmentOptions(CelOptions celOptions) { CelOptions.Builder optionsBuilder = celOptions.toBuilder(); for (FeatureFlag featureFlag : features()) { - if (featureFlag.name().equals("cel.feature.macro_call_tracking")) { - optionsBuilder.populateMacroCalls(featureFlag.enabled()); - } else if (featureFlag.name().equals("cel.feature.backtick_escape_syntax")) { - optionsBuilder.enableQuotedIdentifierSyntax(featureFlag.enabled()); - } else if (featureFlag.name().equals("cel.feature.cross_type_numeric_comparisons")) { - optionsBuilder.enableHeterogeneousNumericComparisons(featureFlag.enabled()); - } else { + BooleanOptionConsumer consumer = FEATURE_HANDLERS.get(featureFlag.name()); + if (consumer == null) { throw new IllegalArgumentException("Unknown feature flag: " + featureFlag.name()); } + consumer.accept(optionsBuilder, featureFlag.enabled()); + } + for (Limit limit : limits()) { + int value = limit.value() < 0 ? -1 : limit.value(); + ObjIntConsumer consumer = LIMIT_HANDLERS.get(limit.name()); + if (consumer == null) { + throw new IllegalArgumentException("Unknown limit: " + limit.name()); + } + consumer.accept(optionsBuilder, value); } return optionsBuilder.build(); } @@ -672,6 +706,25 @@ public static FeatureFlag create(String name, boolean enabled) { } } + /** + * Represents a configurable limit in the environment. + * + *

A negative value indicates no limit. If not specified, the limit should be set to the + * library default. + */ + @AutoValue + public abstract static class Limit { + /** Normalized name of the limit (e.g. cel.limit.expression_code_points */ + public abstract String name(); + + /** The value of the limit, -1 means no limit. */ + public abstract int value(); + + public static Limit create(String name, int value) { + return new AutoValue_CelEnvironment_Limit(name, value); + } + } + /** * Represents a configuration for a canonical CEL extension that can be enabled in the * environment. @@ -995,4 +1048,9 @@ public static OverloadSelector.Builder newBuilder() { } } } + + @FunctionalInterface + private static interface BooleanOptionConsumer { + void accept(CelOptions.Builder options, boolean value); + } } diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java index 1ed113db7..f86787090 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java @@ -221,6 +221,23 @@ private void addOptions(CelEnvironment.Builder envBuilder, CelOptions options) { featureFlags.add(CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true)); } envBuilder.setFeatures(featureFlags.build()); + ImmutableSet.Builder limits = ImmutableSet.builder(); + if (options.maxExpressionCodePointSize() != CelOptions.DEFAULT.maxExpressionCodePointSize()) { + limits.add( + CelEnvironment.Limit.create( + "cel.limit.expression_code_points", options.maxExpressionCodePointSize())); + } + if (options.maxParseErrorRecoveryLimit() != CelOptions.DEFAULT.maxParseErrorRecoveryLimit()) { + limits.add( + CelEnvironment.Limit.create( + "cel.limit.parse_error_recovery", options.maxParseErrorRecoveryLimit())); + } + if (options.maxParseRecursionDepth() != CelOptions.DEFAULT.maxParseRecursionDepth()) { + limits.add( + CelEnvironment.Limit.create( + "cel.limit.parse_recursion_depth", options.maxParseRecursionDepth())); + } + envBuilder.setLimits(limits.build()); } /** diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java index 2fa8923f1..ce8857654 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java @@ -43,6 +43,7 @@ import dev.cel.common.formats.YamlHelper.YamlNodeType; import dev.cel.common.formats.YamlParserContextImpl; import dev.cel.common.internal.CelCodePointArray; +import java.util.Optional; import org.jspecify.annotations.Nullable; import org.yaml.snakeyaml.DumperOptions.FlowStyle; import org.yaml.snakeyaml.nodes.MappingNode; @@ -188,6 +189,70 @@ private ImmutableSet parseFeatures( return featureFlags.build(); } + private ImmutableSet parseLimits(ParserContext ctx, Node node) { + long valueId = ctx.collectMetadata(node); + if (!validateYamlType(node, YamlNodeType.LIST, YamlNodeType.TEXT)) { + ctx.reportError(valueId, "Unsupported limits format"); + } + + ImmutableSet.Builder limits = ImmutableSet.builder(); + + SequenceNode featureListNode = (SequenceNode) node; + for (Node featureMapNode : featureListNode.getValue()) { + long featureMapId = ctx.collectMetadata(featureMapNode); + if (!assertYamlType(ctx, featureMapId, featureMapNode, YamlNodeType.MAP)) { + continue; + } + + MappingNode featureMap = (MappingNode) featureMapNode; + String name = ""; + Optional value = Optional.empty(); + // Shorthand syntax for limit: "cel.limit.foo: 1" + if (featureMap.getValue().size() == 1) { + NodeTuple nodeTuple = featureMap.getValue().get(0); + Node keyNode = nodeTuple.getKeyNode(); + Node valueNode = nodeTuple.getValueNode(); + String keyName = ((ScalarNode) keyNode).getValue(); + if (!keyName.equals("name") && !keyName.equals("value")) { + limits.add(CelEnvironment.Limit.create(keyName, newInteger(ctx, valueNode))); + continue; + } + // Fall through to check against the long syntax. + } + // Long syntax for limit: + // limits: + // - name: cel.limit.foo + // value: 1 + for (NodeTuple nodeTuple : featureMap.getValue()) { + Node keyNode = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(keyNode); + Node valueNode = nodeTuple.getValueNode(); + String keyName = ((ScalarNode) keyNode).getValue(); + switch (keyName) { + case "name": + name = newString(ctx, valueNode); + break; + case "value": + value = Optional.of(newInteger(ctx, valueNode)); + break; + default: + ctx.reportError(keyId, String.format("Unsupported limits tag: %s", keyName)); + break; + } + } + if (name.isEmpty()) { + ctx.reportError(featureMapId, "Missing required attribute(s): name"); + continue; + } + if (!value.isPresent()) { + ctx.reportError(featureMapId, "Missing required attribute(s): value"); + continue; + } + limits.add(CelEnvironment.Limit.create(name, value.get())); + } + return limits.build(); + } + private ImmutableSet parseAliases(ParserContext ctx, Node node) { ImmutableSet.Builder aliasSetBuilder = ImmutableSet.builder(); long valueId = ctx.collectMetadata(node); @@ -804,6 +869,9 @@ private CelEnvironment.Builder parseConfig(ParserContext ctx, Node node) { case "features": builder.setFeatures(parseFeatures(ctx, valueNode)); break; + case "limits": + builder.setLimits(parseLimits(ctx, valueNode)); + break; default: ctx.reportError(id, "Unknown config tag: " + fieldName); // continue handling the rest of the nodes diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java index 2cc229dc9..179faf2ac 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java @@ -61,6 +61,7 @@ private CelEnvironmentYamlSerializer() { this.multiRepresenters.put(CelEnvironment.Alias.class, new RepresentAlias()); this.multiRepresenters.put(CelContainer.class, new RepresentContainer()); this.multiRepresenters.put(CelEnvironment.FeatureFlag.class, new RepresentFeatureFlag()); + this.multiRepresenters.put(CelEnvironment.Limit.class, new RepresentLimit()); } public static String toYaml(CelEnvironment environment) { @@ -98,6 +99,9 @@ public Node representData(Object data) { if (!environment.features().isEmpty()) { configMap.put("features", environment.features().asList()); } + if (!environment.limits().isEmpty()) { + configMap.put("limits", environment.limits().asList()); + } return represent(configMap.buildOrThrow()); } } @@ -275,4 +279,17 @@ public Node representData(Object data) { .buildOrThrow()); } } + + private final class RepresentLimit implements Represent { + + @Override + public Node representData(Object data) { + CelEnvironment.Limit limit = (CelEnvironment.Limit) data; + return represent( + ImmutableMap.builder() + .put("name", limit.name()) + .put("value", limit.value() < 0 ? -1 : limit.value()) + .buildOrThrow()); + } + } } diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java index d6608a9d4..f70f1d466 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java @@ -260,5 +260,34 @@ public void container() { assertThat(container.abbreviations()).containsExactly("foo.Bar", "baz.Qux").inOrder(); assertThat(container.aliases()).containsAtLeast("nm", "user.name", "id", "user.id").inOrder(); } + + @Test + public void options() { + Cel cel = + CelFactory.standardCelBuilder() + .setOptions( + CelOptions.current() + .maxExpressionCodePointSize(100) + .maxParseErrorRecoveryLimit(10) + .maxParseRecursionDepth(10) + .enableQuotedIdentifierSyntax(true) + .enableHeterogeneousNumericComparisons(true) + .populateMacroCalls(true) + .build()) + .build(); + + CelEnvironmentExporter exporter = CelEnvironmentExporter.newBuilder().build(); + CelEnvironment celEnvironment = exporter.export(cel); + assertThat(celEnvironment.features()) + .containsExactly( + CelEnvironment.FeatureFlag.create("cel.feature.backtick_escape_syntax", true), + CelEnvironment.FeatureFlag.create("cel.feature.cross_type_numeric_comparisons", true), + CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true)); + assertThat(celEnvironment.limits()) + .containsExactly( + CelEnvironment.Limit.create("cel.limit.expression_code_points", 100), + CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10), + CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10)); + } } diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java index 3386bdaae..f7eb254d7 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java @@ -124,6 +124,37 @@ public void extend_allFeatureFlags() throws Exception { assertThat(result).isTrue(); } + @Test + public void extend_allLimits() throws Exception { + CelEnvironment environment = + CelEnvironment.newBuilder() + .setLimits( + CelEnvironment.Limit.create("cel.limit.expression_code_points", 20), + CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10), + CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10)) + .build(); + + Cel cel = + environment.extend( + CelFactory.standardCelBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .build(), + CelOptions.DEFAULT); + CelOptions checkerOptions = cel.toCheckerBuilder().options(); + assertThat(checkerOptions.maxExpressionCodePointSize()).isEqualTo(20); + assertThat(checkerOptions.maxParseErrorRecoveryLimit()).isEqualTo(10); + assertThat(checkerOptions.maxParseRecursionDepth()).isEqualTo(10); + + CelAbstractSyntaxTree ast = cel.compile("1 + 2 + 3 + 4 + 5").getAst(); + Long result = (Long) cel.createProgram(ast).eval(); + assertThat(result).isEqualTo(15L); + + CelValidationResult validationResult = cel.compile("1 + 2 + 3 + 4 + 5 + 6"); + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()) + .contains("expression code point size exceeds limit: size: 21, limit 20"); + } + @Test public void extend_unsupportedFeatureFlag_throws() throws Exception { CelEnvironment environment = @@ -143,6 +174,25 @@ public void extend_unsupportedFeatureFlag_throws() throws Exception { assertThat(e).hasMessageThat().contains("Unknown feature flag: unknown.feature"); } + @Test + public void extend_unsupportedLimit_throws() throws Exception { + CelEnvironment environment = + CelEnvironment.newBuilder() + .setLimits(CelEnvironment.Limit.create("unknown.limit", 5)) + .build(); + + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> + environment.extend( + CelFactory.standardCelBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .build(), + CelOptions.DEFAULT)); + assertThat(e).hasMessageThat().contains("Unknown limit: unknown.limit"); + } + @Test public void extensionVersion_specific() throws Exception { CelEnvironment environment = diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java index 98ce55ecc..e98f6110e 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java @@ -108,6 +108,35 @@ public void environment_setFeatures() throws Exception { .build()); } + @Test + public void environment_setLimits() throws Exception { + String yamlConfig = + "name: hello\n" + + "description: empty\n" + + "limits:\n" + + " - name: 'cel.limit.expression_code_points'\n" + + " value: 1000\n" + + " - name: 'cel.limit.parse_error_recovery'\n" + + " value: 10\n" + + " - name: 'cel.limit.parse_recursion_depth'\n" + + " value: 7"; + + CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig); + + assertThat(environment) + .isEqualTo( + CelEnvironment.newBuilder() + .setSource(environment.source().get()) + .setName("hello") + .setDescription("empty") + .setLimits( + ImmutableSet.of( + CelEnvironment.Limit.create("cel.limit.expression_code_points", 1000), + CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10), + CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 7))) + .build()); + } + @Test public void environment_setExtensions() throws Exception { String yamlConfig = @@ -699,6 +728,30 @@ private enum EnvironmentParseErrorTestcase { "ERROR: :6:7: Unsupported alias tag: unknown_tag\n" + " | unknown_tag: 'test_value'\n" + " | ......^"), + UNSUPPORTED_LIMIT_TAG( + "limits:\n" + + " - name: 'test_limit'\n" + + " unknown_tag: 'test_value'\n" + + " value: 100\n", + "ERROR: :3:5: Unsupported limits tag: unknown_tag\n" + + " | unknown_tag: 'test_value'\n" + + " | ....^"), + MISSING_LIMIT_NAME( + "limits:\n" + " - value: 100\n", + "ERROR: :2:5: Missing required attribute(s): name\n" + + " | - value: 100\n" + + " | ....^"), + MISSING_LIMIT_VALUE( + "limits:\n" + " - name: 'test_limit'\n", + "ERROR: :2:5: Missing required attribute(s): value\n" + + " | - name: 'test_limit'\n" + + " | ....^"), + ILLEGAL_LIMIT_VALUE( + "limits:\n" + " - cel.limit.foo: 'not_a_number'\n", + "ERROR: :2:21: Got yaml node type tag:yaml.org,2002:str, wanted type(s)" + + " [tag:yaml.org,2002:int]\n" + + " | - cel.limit.foo: 'not_a_number'\n" + + " | ....................^"), ILLEGAL_FEATURE_TAG( "features:\n" + " - name: 'test_feature'\n" + " unknown_tag: 'test_value'\n", "ERROR: :3:5: Unsupported feature tag: unknown_tag\n" @@ -831,6 +884,10 @@ private enum EnvironmentYamlResourceTestCase { .setReturnType(TypeDecl.create("bool")) .build()))) .setFeatures(CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true)) + .setLimits( + ImmutableSet.of( + CelEnvironment.Limit.create("cel.limit.expression_code_points", 1000), + CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 7))) .build()), LIBRARY_SUBSET_ENV( diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java index 1c56370b2..0235cb2f4 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java @@ -129,6 +129,10 @@ public void toYaml_success() throws Exception { .setFeatures( CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true), CelEnvironment.FeatureFlag.create("cel.feature.backtick_escape_syntax", false)) + .setLimits( + CelEnvironment.Limit.create("cel.limit.expression_code_points", 1000), + CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10), + CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 7)) .build(); String yamlOutput = CelEnvironmentYamlSerializer.toYaml(environment); diff --git a/testing/src/test/resources/environment/dump_env.yaml b/testing/src/test/resources/environment/dump_env.yaml index 18f96fbcc..1e7e7880b 100644 --- a/testing/src/test/resources/environment/dump_env.yaml +++ b/testing/src/test/resources/environment/dump_env.yaml @@ -87,3 +87,10 @@ features: enabled: true - name: cel.feature.backtick_escape_syntax enabled: false +limits: +- name: cel.limit.expression_code_points + value: 1000 +- name: cel.limit.parse_error_recovery + value: 10 +- name: cel.limit.parse_recursion_depth + value: 7 diff --git a/testing/src/test/resources/environment/extended_env.yaml b/testing/src/test/resources/environment/extended_env.yaml index c420ad4db..4763c868f 100644 --- a/testing/src/test/resources/environment/extended_env.yaml +++ b/testing/src/test/resources/environment/extended_env.yaml @@ -41,6 +41,10 @@ functions: features: - name: cel.feature.macro_call_tracking enabled: true +limits: +- name: cel.limit.expression_code_points + value: 1000 +- cel.limit.parse_recursion_depth: 7 # TODO: Add support for below #validators: #- name: cel.validator.duration From 07376918815d16893ec63c24a366540d2e3d5bb7 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 25 Feb 2026 17:25:55 -0800 Subject: [PATCH 005/204] Add conformance tests for planner PiperOrigin-RevId: 875410045 --- MODULE.bazel | 15 ++-- .../java/dev/cel/bundle/CelEnvironment.java | 4 + .../java/dev/cel/common/types/SimpleType.java | 1 - .../values/BaseProtoCelValueConverter.java | 16 ++++ .../cel/common/values/CelValueConverter.java | 4 + .../test/java/dev/cel/conformance/BUILD.bazel | 80 ++++++++++++++++--- .../dev/cel/conformance/ConformanceTest.java | 46 ++++++++--- .../conformance/ConformanceTestRunner.java | 7 +- .../dev/cel/conformance/conformance_test.bzl | 19 ++--- .../dev/cel/extensions/CelMathExtensions.java | 16 +++- .../cel/extensions/CelRegexExtensions.java | 3 +- .../cel/extensions/CelStringExtensions.java | 3 +- .../src/main/java/dev/cel/runtime/BUILD.bazel | 2 +- .../java/dev/cel/runtime/CelRuntimeImpl.java | 4 +- .../java/dev/cel/runtime/planner/EvalAnd.java | 7 +- .../java/dev/cel/runtime/planner/EvalOr.java | 7 +- .../runtime/planner/NamespacedAttribute.java | 6 +- .../runtime/planner/ProgramPlannerTest.java | 1 - 18 files changed, 183 insertions(+), 58 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 007adcb3c..fd35e41a2 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -16,18 +16,23 @@ module( name = "cel_java", ) -bazel_dep(name = "bazel_skylib", version = "1.8.2") -bazel_dep(name = "rules_jvm_external", version = "6.9") +bazel_dep(name = "bazel_skylib", version = "1.9.0") +bazel_dep(name = "rules_jvm_external", version = "6.10") bazel_dep(name = "protobuf", version = "33.4", repo_name = "com_google_protobuf") # see https://github.com/bazelbuild/rules_android/issues/373 -bazel_dep(name = "googleapis", version = "0.0.0-20241220-5e258e33.bcr.1", repo_name = "com_google_googleapis") -bazel_dep(name = "rules_pkg", version = "1.0.1") +bazel_dep(name = "googleapis", version = "0.0.0-20260223-edfe7983", repo_name = "com_google_googleapis") +bazel_dep(name = "rules_pkg", version = "1.2.0") bazel_dep(name = "rules_license", version = "1.0.0") bazel_dep(name = "rules_proto", version = "7.1.0") bazel_dep(name = "rules_java", version = "9.3.0") bazel_dep(name = "rules_android", version = "0.7.1") bazel_dep(name = "rules_shell", version = "0.6.1") bazel_dep(name = "googleapis-java", version = "1.0.0") -bazel_dep(name = "cel-spec", version = "0.24.0", repo_name = "cel_spec") +bazel_dep(name = "cel-spec", version = "0.25.1", repo_name = "cel_spec") +bazel_dep(name = "rules_go", version = "0.50.1") + +# Required by cel-spec to satisfy gazelle transitive dependency +go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk") +go_sdk.download(version = "1.23.0") switched_rules = use_extension("@com_google_googleapis//:extensions.bzl", "switched_rules") switched_rules.use_languages(java = True) diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java index c8e424217..8614b87b5 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java @@ -672,6 +672,10 @@ public CelType toCelType(CelTypeProvider celTypeProvider) { return TypeParamType.create(name()); } + if (name().equals("dyn")) { + return SimpleType.DYN; + } + CelType simpleType = SimpleType.findByName(name()).orElse(null); if (simpleType != null) { return simpleType; diff --git a/common/src/main/java/dev/cel/common/types/SimpleType.java b/common/src/main/java/dev/cel/common/types/SimpleType.java index 6c43ab53f..93bd5326d 100644 --- a/common/src/main/java/dev/cel/common/types/SimpleType.java +++ b/common/src/main/java/dev/cel/common/types/SimpleType.java @@ -46,7 +46,6 @@ public abstract class SimpleType extends CelType { public static final ImmutableMap TYPE_MAP = ImmutableMap.of( - DYN.name(), DYN, BOOL.name(), BOOL, BYTES.name(), BYTES, DOUBLE.name(), DOUBLE, diff --git a/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java b/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java index b05a21e24..6851deed5 100644 --- a/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java @@ -17,6 +17,8 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; +import com.google.common.base.CaseFormat; +import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -27,6 +29,7 @@ import com.google.protobuf.BytesValue; import com.google.protobuf.DoubleValue; import com.google.protobuf.Duration; +import com.google.protobuf.FieldMask; import com.google.protobuf.FloatValue; import com.google.protobuf.Int32Value; import com.google.protobuf.Int64Value; @@ -41,6 +44,8 @@ import dev.cel.common.annotations.Internal; import dev.cel.common.internal.ProtoTimeUtils; import dev.cel.common.internal.WellKnownProto; +import java.util.ArrayList; +import java.util.List; /** * {@code BaseProtoCelValueConverter} contains the common logic for converting between native Java @@ -98,6 +103,17 @@ protected Object fromWellKnownProto(MessageLiteOrBuilder message, WellKnownProto return UnsignedLong.valueOf(((UInt32Value) message).getValue()); case UINT64_VALUE: return UnsignedLong.fromLongBits(((UInt64Value) message).getValue()); + case FIELD_MASK: + FieldMask fieldMask = (FieldMask) message; + List paths = new ArrayList<>(fieldMask.getPathsCount()); + for (String path : fieldMask.getPathsList()) { + if (!path.isEmpty()) { + paths.add(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, path)); + } + } + return normalizePrimitive(Joiner.on(",").join(paths)); + case EMPTY: + return ImmutableMap.of(); default: throw new UnsupportedOperationException( "Unsupported well known proto conversion - " + wellKnownProto); diff --git a/common/src/main/java/dev/cel/common/values/CelValueConverter.java b/common/src/main/java/dev/cel/common/values/CelValueConverter.java index ae0b40ef7..fda014f31 100644 --- a/common/src/main/java/dev/cel/common/values/CelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/CelValueConverter.java @@ -54,6 +54,10 @@ public Object unwrap(CelValue celValue) { return Optional.of(optionalValue.value()); } + if (celValue instanceof ErrorValue) { + return celValue; + } + return celValue.value(); } diff --git a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel index ab7468e54..3ac7ffcba 100644 --- a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -29,6 +29,7 @@ java_library( "//parser:parser_builder", "//parser:parser_factory", "//runtime", + "//runtime:runtime_planner_impl", "//testing:expr_value_utils", "@cel_spec//proto/cel/expr:expr_java_proto", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", @@ -57,6 +58,8 @@ java_library( deps = MAVEN_JAR_DEPS + [ "//:java_truth", "//compiler:compiler_builder", + "//parser:parser_factory", + "//runtime:runtime_planner_impl", "//testing:expr_value_utils", "@cel_spec//proto/cel/expr:expr_java_proto", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", @@ -100,14 +103,8 @@ _ALL_TESTS = [ "@cel_spec//tests/simple:testdata/wrappers.textproto", ] -_TESTS_TO_SKIP = [ - # Tests which require spec changes. - # TODO: Deprecate Duration.get_milliseconds - "timestamps/duration_converters/get_milliseconds", - +_TESTS_TO_SKIP_LEGACY = [ # Broken test cases which should be supported. - # TODO: Invalid bytes to string conversion should error. - "conversions/string/bytes_invalid", # TODO: Support setting / getting enum values out of the defined enum value range. "enums/legacy_proto2/select_big,select_neg", "enums/legacy_proto2/assign_standalone_int_big,assign_standalone_int_neg", @@ -116,7 +113,6 @@ _TESTS_TO_SKIP = [ # TODO: Ensure overflow occurs on conversions of double values which might not work properly on all platforms. "conversions/int/double_int_min_range", # TODO: Duration and timestamp operations should error on overflow. - "timestamps/duration_range/from_string_under,from_string_over", "timestamps/timestamp_range/sub_time_duration_over,sub_time_duration_under", # TODO: Ensure adding negative duration values is appropriately supported. "timestamps/timestamp_arithmetic/add_time_to_duration_nanos_negative", @@ -159,17 +155,74 @@ _TESTS_TO_SKIP = [ "type_deductions/legacy_nullable_types/null_assignable_to_timestamp_parameter_candidate", ] +_TESTS_TO_SKIP_PLANNER = [ + # TODO: Add strings.format and strings.quote. + "string_ext/quote", + "string_ext/format", + "string_ext/format_errors", + + # TODO: Check behavior for go/cpp + "basic/functions/unbound", + "basic/functions/unbound_is_runtime_error", + + # TODO: Ensure overflow occurs on conversions of double values which might not work properly on all platforms. + "conversions/int/double_int_min_range", + "enums/legacy_proto3/assign_standalone_int_too_big", + "enums/legacy_proto3/assign_standalone_int_too_neg", + + # TODO: Duration and timestamp operations should error on overflow. + "timestamps/timestamp_range/sub_time_duration_over", + "timestamps/timestamp_range/sub_time_duration_under", + + # Skip until fixed. + "fields/qualified_identifier_resolution/map_key_float", + "fields/qualified_identifier_resolution/map_key_null", + "fields/qualified_identifier_resolution/map_value_repeat_key_heterogeneous", + "optionals/optionals/map_null_entry_no_such_key", + "optionals/optionals/map_present_key_invalid_field", + "parse/receiver_function_names", + "proto2/extensions_get/package_scoped_test_all_types_ext", + "proto2/extensions_get/package_scoped_repeated_test_all_types", + "proto2/extensions_get/message_scoped_nested_ext", + "proto2/extensions_get/message_scoped_repeated_test_all_types", + "proto2_ext/get_ext/package_scoped_repeated_test_all_types", + "proto2_ext/get_ext/message_scoped_repeated_test_all_types", + + # TODO: Fix null assignment to a field + "proto2/set_null/single_message", + "proto2/set_null/single_duration", + "proto2/set_null/single_timestamp", + "proto3/set_null/single_message", + "proto3/set_null/single_duration", + "proto3/set_null/single_timestamp", + + # Type inference edgecases around null(able) assignability. + # These type check, but resolve to a different type. + # list(int), want list(wrapper(int)) + "type_deductions/wrappers/wrapper_promotion", + # list(null), want list(Message) + "type_deductions/legacy_nullable_types/null_assignable_to_message_parameter_candidate", + "type_deductions/legacy_nullable_types/null_assignable_to_abstract_parameter_candidate", + "type_deductions/legacy_nullable_types/null_assignable_to_duration_parameter_candidate", + "type_deductions/legacy_nullable_types/null_assignable_to_timestamp_parameter_candidate", + + # Future features for CEL 1.0 + # TODO: Strong typing support for enums, specified but not implemented. + "enums/strong_proto2", + "enums/strong_proto3", +] + conformance_test( name = "conformance", data = _ALL_TESTS, - skip_tests = _TESTS_TO_SKIP, + skip_tests = _TESTS_TO_SKIP_LEGACY, ) conformance_test( name = "conformance_maven", data = _ALL_TESTS, mode = MODE.MAVEN_TEST, - skip_tests = _TESTS_TO_SKIP, + skip_tests = _TESTS_TO_SKIP_LEGACY, ) conformance_test( @@ -177,3 +230,10 @@ conformance_test( data = _ALL_TESTS, mode = MODE.DASHBOARD, ) + +conformance_test( + name = "conformance_planner", + data = _ALL_TESTS, + skip_tests = _TESTS_TO_SKIP_PLANNER, + use_planner = True, +) diff --git a/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java index dcd226fa5..437e50fea 100644 --- a/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java +++ b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java @@ -45,7 +45,9 @@ import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelRuntime; import dev.cel.runtime.CelRuntime.Program; +import dev.cel.runtime.CelRuntimeBuilder; import dev.cel.runtime.CelRuntimeFactory; +import dev.cel.runtime.CelRuntimeImpl; import dev.cel.runtime.CelRuntimeLibrary; import java.util.Map; import org.junit.runners.model.Statement; @@ -118,15 +120,25 @@ private static CelChecker getChecker(SimpleTest test) throws Exception { .build(); } - private static final CelRuntime RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder() - .setOptions(OPTIONS) - .addLibraries(CANONICAL_RUNTIME_EXTENSIONS) - .setExtensionRegistry(DEFAULT_EXTENSION_REGISTRY) - .addMessageTypes(dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor()) - .addMessageTypes(dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor()) - .addFileTypes(dev.cel.expr.conformance.proto2.TestAllTypesExtensions.getDescriptor()) - .build(); + private static CelRuntime getRuntime(SimpleTest test, boolean usePlanner) { + CelRuntimeBuilder builder = + usePlanner ? CelRuntimeImpl.newBuilder() : CelRuntimeFactory.standardCelRuntimeBuilder(); + + builder + // CEL-Internal-2 + .setOptions(OPTIONS) + .addLibraries(CANONICAL_RUNTIME_EXTENSIONS) + .setExtensionRegistry(DEFAULT_EXTENSION_REGISTRY) + .addMessageTypes(dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor()) + .addMessageTypes(dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor()) + .addFileTypes(dev.cel.expr.conformance.proto2.TestAllTypesExtensions.getDescriptor()); + + if (usePlanner) { + builder.setContainer(CelContainer.ofName(test.getContainer())); + } + + return builder.build(); + } private static ImmutableMap getBindings(SimpleTest test) throws Exception { ImmutableMap.Builder bindings = @@ -157,13 +169,15 @@ private static SimpleTest defaultTestMatcherToTrueIfUnset(SimpleTest test) { private final String name; private final SimpleTest test; private final boolean skip; + private final boolean usePlanner; - public ConformanceTest(String name, SimpleTest test, boolean skip) { + public ConformanceTest(String name, SimpleTest test, boolean skip, boolean usePlanner) { this.name = Preconditions.checkNotNull(name); this.test = Preconditions.checkNotNull( defaultTestMatcherToTrueIfUnset(Preconditions.checkNotNull(test))); this.skip = skip; + this.usePlanner = usePlanner; } public String getName() { @@ -178,7 +192,9 @@ public boolean shouldSkip() { public void evaluate() throws Throwable { CelValidationResult response = getParser(test).parse(test.getExpr(), test.getName()); assertThat(response.hasError()).isFalse(); - response = getChecker(test).check(response.getAst()); + if (!test.getDisableCheck()) { + response = getChecker(test).check(response.getAst()); + } assertThat(response.hasError()).isFalse(); Type resultType = CelProtoTypes.celTypeToType(response.getAst().getResultType()); @@ -188,7 +204,13 @@ public void evaluate() throws Throwable { return; } - Program program = RUNTIME.createProgram(response.getAst()); + if (!usePlanner && test.getDisableCheck()) { + // Only planner supports parsed-only evaluation + return; + } + + CelRuntime runtime = getRuntime(test, usePlanner); + Program program = runtime.createProgram(response.getAst()); ExprValue result = null; CelEvaluationException error = null; try { diff --git a/conformance/src/test/java/dev/cel/conformance/ConformanceTestRunner.java b/conformance/src/test/java/dev/cel/conformance/ConformanceTestRunner.java index 89598ed3e..dc3d5021e 100644 --- a/conformance/src/test/java/dev/cel/conformance/ConformanceTestRunner.java +++ b/conformance/src/test/java/dev/cel/conformance/ConformanceTestRunner.java @@ -43,6 +43,7 @@ public final class ConformanceTestRunner extends ParentRunner { private final ImmutableSortedMap testFiles; private final ImmutableList testsToSkip; + private final boolean usePlanner; private static ImmutableSortedMap loadTestFiles() { List testPaths = @@ -75,6 +76,9 @@ public ConformanceTestRunner(Class clazz) throws InitializationError { ImmutableList.copyOf( SPLITTER.splitToList( System.getProperty("dev.cel.conformance.ConformanceTests.skip_tests"))); + usePlanner = + Boolean.parseBoolean( + System.getProperty("dev.cel.conformance.ConformanceTests.use_planner", "false")); } private boolean shouldSkipTest(String name) { @@ -97,8 +101,7 @@ protected List getChildren() { for (SimpleTest test : testSection.getTestList()) { String name = String.format("%s/%s/%s", testFile.getName(), testSection.getName(), test.getName()); - tests.add( - new ConformanceTest(name, test, test.getDisableCheck() || shouldSkipTest(name))); + tests.add(new ConformanceTest(name, test, shouldSkipTest(name), usePlanner)); } } } diff --git a/conformance/src/test/java/dev/cel/conformance/conformance_test.bzl b/conformance/src/test/java/dev/cel/conformance/conformance_test.bzl index f884a2a84..b91ce4a8b 100644 --- a/conformance/src/test/java/dev/cel/conformance/conformance_test.bzl +++ b/conformance/src/test/java/dev/cel/conformance/conformance_test.bzl @@ -38,11 +38,12 @@ def _expand_tests_to_skip(tests_to_skip): result.append(test_to_skip[0:slash] + part) return result -def _conformance_test_args(data, skip_tests): - args = [] - args.append("-Ddev.cel.conformance.ConformanceTests.skip_tests={}".format(",".join(_expand_tests_to_skip(skip_tests)))) - args.append("-Ddev.cel.conformance.ConformanceTests.tests={}".format(",".join(["$(location " + test + ")" for test in data]))) - return args +def _conformance_test_args(data, skip_tests, use_planner): + return [ + "-Ddev.cel.conformance.ConformanceTests.skip_tests={}".format(",".join(_expand_tests_to_skip(skip_tests))), + "-Ddev.cel.conformance.ConformanceTests.tests={}".format(",".join(["$(location {})".format(t) for t in data])), + "-Ddev.cel.conformance.ConformanceTests.use_planner={}".format("true" if use_planner else "false"), + ] MODE = struct( # Standard test execution against HEAD @@ -53,7 +54,7 @@ MODE = struct( DASHBOARD = "dashboard", ) -def conformance_test(name, data, mode = MODE.TEST, skip_tests = []): +def conformance_test(name, data, mode = MODE.TEST, skip_tests = [], use_planner = False): """Executes conformance tests Args: @@ -69,7 +70,7 @@ def conformance_test(name, data, mode = MODE.TEST, skip_tests = []): if mode == MODE.DASHBOARD: java_test( name = "_" + name, - jvm_flags = _conformance_test_args(data, skip_tests), + jvm_flags = _conformance_test_args(data, skip_tests, use_planner), data = data, size = "small", test_class = "dev.cel.conformance.ConformanceTests", @@ -95,7 +96,7 @@ def conformance_test(name, data, mode = MODE.TEST, skip_tests = []): elif mode == MODE.TEST: java_test( name = name, - jvm_flags = _conformance_test_args(data, skip_tests), + jvm_flags = _conformance_test_args(data, skip_tests, use_planner), data = data, size = "small", test_class = "dev.cel.conformance.ConformanceTests", @@ -104,7 +105,7 @@ def conformance_test(name, data, mode = MODE.TEST, skip_tests = []): elif mode == MODE.MAVEN_TEST: java_test( name = name, - jvm_flags = _conformance_test_args(data, skip_tests), + jvm_flags = _conformance_test_args(data, skip_tests, use_planner), data = data, size = "small", test_class = "dev.cel.conformance.ConformanceTests", diff --git a/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java index 57c8c1378..22336eb22 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java @@ -663,9 +663,19 @@ String getFunction() { ImmutableSet functionBindingsULongSigned, ImmutableSet functionBindingsULongUnsigned) { this.functionDecl = functionDecl; - this.functionBindings = functionBindings; - this.functionBindingsULongSigned = functionBindingsULongSigned; - this.functionBindingsULongUnsigned = functionBindingsULongUnsigned; + this.functionBindings = + functionBindings.isEmpty() + ? ImmutableSet.of() + : CelFunctionBinding.fromOverloads(functionDecl.name(), functionBindings); + this.functionBindingsULongSigned = + functionBindingsULongSigned.isEmpty() + ? ImmutableSet.of() + : CelFunctionBinding.fromOverloads(functionDecl.name(), functionBindingsULongSigned); + this.functionBindingsULongUnsigned = + functionBindingsULongUnsigned.isEmpty() + ? ImmutableSet.of() + : CelFunctionBinding.fromOverloads( + functionDecl.name(), functionBindingsULongUnsigned); } } diff --git a/extensions/src/main/java/dev/cel/extensions/CelRegexExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelRegexExtensions.java index f1ed3b478..564422cd4 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelRegexExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelRegexExtensions.java @@ -123,7 +123,8 @@ String getFunction() { Function(CelFunctionDecl functionDecl, ImmutableSet functionBindings) { this.functionDecl = functionDecl; - this.functionBindings = functionBindings; + this.functionBindings = + CelFunctionBinding.fromOverloads(functionDecl.name(), functionBindings); } } diff --git a/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java index 37b2a368b..10caa7db8 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java @@ -238,7 +238,8 @@ String getFunction() { Function(CelFunctionDecl functionDecl, CelFunctionBinding... functionBindings) { this.functionDecl = functionDecl; - this.functionBindings = ImmutableSet.copyOf(functionBindings); + this.functionBindings = + CelFunctionBinding.fromOverloads(functionDecl.name(), functionBindings); } } diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 0746a5b83..70568cd90 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -827,7 +827,7 @@ java_library( ":function_binding", ":function_resolver", ":program", - ":proto_message_runtime_helpers", + ":proto_message_runtime_equality", ":runtime", ":runtime_equality", ":standard_functions", diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index e910c77a9..346b25ae9 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -424,9 +424,7 @@ public CelRuntime build() { CombinedCelValueProvider.combine(protoMessageValueProvider, valueProvider()); } - RuntimeEquality runtimeEquality = - RuntimeEquality.create( - ProtoMessageRuntimeHelpers.create(dynamicProto, options()), options()); + RuntimeEquality runtimeEquality = ProtoMessageRuntimeEquality.create(dynamicProto, options()); ImmutableSet runtimeLibraries = runtimeLibrariesBuilder().build(); // Add libraries, such as extensions for (CelRuntimeLibrary celLibrary : runtimeLibraries) { diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java index b09191e9f..763f8faba 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java @@ -39,8 +39,11 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { errorValue = (ErrorValue) argVal; } else { // TODO: Handle unknowns - throw new IllegalArgumentException( - String.format("Expected boolean value, found: %s", argVal)); + errorValue = + ErrorValue.create( + arg.exprId(), + new IllegalArgumentException( + String.format("Expected boolean value, found: %s", argVal))); } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java index 8c8f5954d..22fc56a7f 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java @@ -39,8 +39,11 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { errorValue = (ErrorValue) argVal; } else { // TODO: Handle unknowns - throw new IllegalArgumentException( - String.format("Expected boolean value, found: %s", argVal)); + errorValue = + ErrorValue.create( + arg.exprId(), + new IllegalArgumentException( + String.format("Expected boolean value, found: %s", argVal))); } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java index 6bdf0c072..de1a90291 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java @@ -62,11 +62,7 @@ public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { Object value = resolver.resolve(name); if (value != null) { - if (!qualifiers.isEmpty()) { - return applyQualifiers(value, celValueConverter, qualifiers); - } else { - return value; - } + return applyQualifiers(value, celValueConverter, qualifiers); } // Attempt to resolve the qualify type name if the name is not a variable identifier diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index 33500f217..7bdcaaac1 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -1055,7 +1055,6 @@ private enum TypeLiteralTestCase { INT("int", SimpleType.INT), UINT("uint", SimpleType.UINT), STRING("string", SimpleType.STRING), - DYN("dyn", SimpleType.DYN), LIST("list", ListType.create(SimpleType.DYN)), MAP("map", MapType.create(SimpleType.DYN, SimpleType.DYN)), NULL("null_type", SimpleType.NULL_TYPE), From 941932c0f10ee29711cd156c962a8e601501e445 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 26 Feb 2026 09:09:59 -0800 Subject: [PATCH 006/204] Prevent planning createMap for unsupported keys PiperOrigin-RevId: 875751638 --- .../exceptions/CelInvalidArgumentException.java | 4 ++++ .../test/java/dev/cel/conformance/BUILD.bazel | 2 -- .../java/dev/cel/runtime/planner/BUILD.bazel | 1 + .../dev/cel/runtime/planner/EvalCreateMap.java | 17 +++++++++++++++-- .../cel/runtime/planner/ProgramPlannerTest.java | 11 +++++++++++ 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/common/src/main/java/dev/cel/common/exceptions/CelInvalidArgumentException.java b/common/src/main/java/dev/cel/common/exceptions/CelInvalidArgumentException.java index 41358bb79..6a2b4ab72 100644 --- a/common/src/main/java/dev/cel/common/exceptions/CelInvalidArgumentException.java +++ b/common/src/main/java/dev/cel/common/exceptions/CelInvalidArgumentException.java @@ -24,4 +24,8 @@ public final class CelInvalidArgumentException extends CelRuntimeException { public CelInvalidArgumentException(Throwable cause) { super(cause, CelErrorCode.INVALID_ARGUMENT); } + + public CelInvalidArgumentException(String message) { + super(message, CelErrorCode.INVALID_ARGUMENT); + } } diff --git a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel index 3ac7ffcba..375cd8bfa 100644 --- a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -175,8 +175,6 @@ _TESTS_TO_SKIP_PLANNER = [ "timestamps/timestamp_range/sub_time_duration_under", # Skip until fixed. - "fields/qualified_identifier_resolution/map_key_float", - "fields/qualified_identifier_resolution/map_key_null", "fields/qualified_identifier_resolution/map_value_repeat_key_heterogeneous", "optionals/optionals/map_null_entry_no_such_key", "optionals/optionals/map_present_key_invalid_field", diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index b827afed5..f7912cff2 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -334,6 +334,7 @@ java_library( ":localized_evaluation_exception", ":planned_interpretable", "//common/exceptions:duplicate_key", + "//common/exceptions:invalid_argument", "//runtime:evaluation_exception", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java index 1ab0f7e5b..c09c19987 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java @@ -17,8 +17,10 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; +import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.Immutable; import dev.cel.common.exceptions.CelDuplicateKeyException; +import dev.cel.common.exceptions.CelInvalidArgumentException; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.GlobalResolver; import java.util.HashSet; @@ -46,11 +48,22 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval HashSet keysSeen = Sets.newHashSetWithExpectedSize(keys.length); for (int i = 0; i < keys.length; i++) { - Object key = keys[i].eval(resolver, frame); + PlannedInterpretable keyInterpretable = keys[i]; + Object key = keyInterpretable.eval(resolver, frame); + if (!(key instanceof String + || key instanceof Long + || key instanceof UnsignedLong + || key instanceof Boolean)) { + throw new LocalizedEvaluationException( + new CelInvalidArgumentException("Unsupported key type: " + key), + keyInterpretable.exprId()); + } + Object val = values[i].eval(resolver, frame); if (!keysSeen.add(key)) { - throw new LocalizedEvaluationException(CelDuplicateKeyException.of(key), keys[i].exprId()); + throw new LocalizedEvaluationException( + CelDuplicateKeyException.of(key), keyInterpretable.exprId()); } if (isOptional[i]) { diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index 7bdcaaac1..30b100bc2 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -390,6 +390,17 @@ public void plan_createMap_containsDuplicateKey_throws() throws Exception { .contains("evaluation error at :20: duplicate map key [true]"); } + @Test + public void plan_createMap_unsupportedKeyType_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("{1.0: 'foo'}"); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); + assertThat(e) + .hasMessageThat() + .contains("evaluation error at :1: Unsupported key type: 1.0"); + } + @Test public void plan_createStruct() throws Exception { CelAbstractSyntaxTree ast = compile("cel.expr.conformance.proto3.TestAllTypes{}"); From 3cebc1a61eef4f0858e99271f18458eb4b2aa43d Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 26 Feb 2026 13:36:58 -0800 Subject: [PATCH 007/204] Expose planner based runtimes through experimental factories PiperOrigin-RevId: 875869202 --- bundle/BUILD.bazel | 8 +++ .../src/main/java/dev/cel/bundle/BUILD.bazel | 18 +++++- .../cel/bundle/CelExperimentalFactory.java | 61 +++++++++++++++++++ .../src/main/java/dev/cel/bundle/CelImpl.java | 13 +--- .../test/java/dev/cel/extensions/BUILD.bazel | 5 +- .../extensions/CelOptionalLibraryTest.java | 19 +----- runtime/BUILD.bazel | 8 +++ .../src/main/java/dev/cel/runtime/BUILD.bazel | 13 ++++ .../CelRuntimeExperimentalFactory.java | 52 ++++++++++++++++ .../src/test/java/dev/cel/runtime/BUILD.bazel | 2 +- .../cel/runtime/PlannerInterpreterTest.java | 3 +- 11 files changed, 166 insertions(+), 36 deletions(-) create mode 100644 bundle/src/main/java/dev/cel/bundle/CelExperimentalFactory.java create mode 100644 runtime/src/main/java/dev/cel/runtime/CelRuntimeExperimentalFactory.java diff --git a/bundle/BUILD.bazel b/bundle/BUILD.bazel index 1eaf0bec8..11e0b8a6d 100644 --- a/bundle/BUILD.bazel +++ b/bundle/BUILD.bazel @@ -13,6 +13,14 @@ java_library( ], ) +java_library( + name = "cel_experimental_factory", + visibility = ["//:internal"], + exports = [ + "//bundle/src/main/java/dev/cel/bundle:cel_experimental_factory", + ], +) + java_library( name = "environment", exports = ["//bundle/src/main/java/dev/cel/bundle:environment"], diff --git a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel index 0a014ec73..822511e4c 100644 --- a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel +++ b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel @@ -57,6 +57,23 @@ java_library( ], ) +java_library( + name = "cel_experimental_factory", + srcs = ["CelExperimentalFactory.java"], + tags = [ + ], + deps = [ + ":cel", + ":cel_impl", + "//checker", + "//common:options", + "//common/annotations", + "//compiler", + "//parser", + "//runtime:runtime_planner_impl", + ], +) + java_library( name = "cel_impl", srcs = ["CelImpl.java"], @@ -73,7 +90,6 @@ java_library( "//common:compiler_common", "//common:container", "//common:options", - "//common/annotations", "//common/internal:env_visitor", "//common/internal:file_descriptor_converter", "//common/types:cel_proto_types", diff --git a/bundle/src/main/java/dev/cel/bundle/CelExperimentalFactory.java b/bundle/src/main/java/dev/cel/bundle/CelExperimentalFactory.java new file mode 100644 index 000000000..2275d1c56 --- /dev/null +++ b/bundle/src/main/java/dev/cel/bundle/CelExperimentalFactory.java @@ -0,0 +1,61 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.bundle; + +import dev.cel.checker.CelCheckerLegacyImpl; +import dev.cel.common.CelOptions; +import dev.cel.common.annotations.Beta; +import dev.cel.compiler.CelCompilerImpl; +import dev.cel.parser.CelParserImpl; +import dev.cel.runtime.CelRuntimeImpl; + +/** + * Experimental helper class to configure the entire CEL stack in a common interface, backed by the + * new {@code ProgramPlanner} architecture. + * + *

All APIs and behaviors surfaced here are subject to change. + */ +@Beta +public final class CelExperimentalFactory { + + /** + * Creates a builder for configuring CEL for the parsing, optional type-checking, and evaluation + * of expressions using the Program Planner. + * + *

The {@code ProgramPlanner} architecture provides key benefits over the legacy runtime: + * + *

    + *
  • Performance: Programs can be cached for improving evaluation speed. + *
  • Parsed-only expression evaluation: Unlike the traditional stack which required + * supplying type-checked expressions, this architecture handles both parsed-only and + * type-checked expressions. + *
+ */ + public static CelBuilder plannerCelBuilder() { + return CelImpl.newBuilder( + CelCompilerImpl.newBuilder( + CelParserImpl.newBuilder(), + CelCheckerLegacyImpl.newBuilder().setStandardEnvironmentEnabled(true)), + CelRuntimeImpl.newBuilder()) + // CEL-Internal-2 + .setOptions( + CelOptions.current() + .enableHeterogeneousNumericComparisons(true) + .enableTimestampEpoch(true) + .build()); + } + + private CelExperimentalFactory() {} +} diff --git a/bundle/src/main/java/dev/cel/bundle/CelImpl.java b/bundle/src/main/java/dev/cel/bundle/CelImpl.java index 51fe2dc38..ae0ab2395 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelImpl.java +++ b/bundle/src/main/java/dev/cel/bundle/CelImpl.java @@ -38,7 +38,6 @@ import dev.cel.common.CelSource; import dev.cel.common.CelValidationResult; import dev.cel.common.CelVarDecl; -import dev.cel.common.annotations.Internal; import dev.cel.common.internal.EnvVisitable; import dev.cel.common.internal.EnvVisitor; import dev.cel.common.internal.FileDescriptorSetConverter; @@ -65,14 +64,9 @@ * Implementation of the synchronous CEL stack. * *

Note, the underlying {@link CelCompiler} and {@link CelRuntime} values are constructed lazily. - * - *

CEL Library Internals. Do Not Use. Consumers should use {@code CelFactory} instead. - * - *

TODO: Restrict visibility once factory is introduced */ @Immutable -@Internal -public final class CelImpl implements Cel, EnvVisitable { +final class CelImpl implements Cel, EnvVisitable { // The lazily constructed compiler and runtime values are memoized and guaranteed to be // constructed only once without side effects, thus making them effectively immutable. @@ -151,11 +145,8 @@ static CelImpl combine(CelCompiler compiler, CelRuntime runtime) { *

By default, {@link CelOptions#DEFAULT} are enabled, as is the CEL standard environment. * *

CEL Library Internals. Do Not Use. Consumers should use {@code CelFactory} instead. - * - *

TODO: Restrict visibility once factory is introduced */ - @Internal - public static CelBuilder newBuilder( + static CelBuilder newBuilder( CelCompilerBuilder compilerBuilder, CelRuntimeBuilder celRuntimeBuilder) { return new CelImpl.Builder(compilerBuilder, celRuntimeBuilder); } diff --git a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel index b441c33cf..48915fd02 100644 --- a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel @@ -10,8 +10,7 @@ java_library( deps = [ "//:java_truth", "//bundle:cel", - "//bundle:cel_impl", - "//checker", + "//bundle:cel_experimental_factory", "//common:cel_ast", "//common:compiler_common", "//common:container", @@ -32,7 +31,6 @@ java_library( "//extensions:sets", "//extensions:sets_function", "//extensions:strings", - "//parser", "//parser:macro", "//parser:unparser", "//runtime", @@ -40,7 +38,6 @@ java_library( "//runtime:interpreter_util", "//runtime:lite_runtime", "//runtime:lite_runtime_factory", - "//runtime:runtime_planner_impl", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/test:simple_java_proto", diff --git a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java index 0f0c649f8..24e9d6d86 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java @@ -26,9 +26,8 @@ import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.bundle.Cel; import dev.cel.bundle.CelBuilder; +import dev.cel.bundle.CelExperimentalFactory; import dev.cel.bundle.CelFactory; -import dev.cel.bundle.CelImpl; -import dev.cel.checker.CelCheckerLegacyImpl; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; @@ -46,16 +45,13 @@ import dev.cel.common.values.CelByteString; import dev.cel.common.values.NullValue; import dev.cel.compiler.CelCompiler; -import dev.cel.compiler.CelCompilerImpl; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import dev.cel.parser.CelMacro; -import dev.cel.parser.CelParserImpl; import dev.cel.parser.CelStandardMacro; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelRuntime; -import dev.cel.runtime.CelRuntimeImpl; import dev.cel.runtime.InterpreterUtil; import java.time.Duration; import java.time.Instant; @@ -106,17 +102,6 @@ private enum ConstantTestCases { } } - private static CelBuilder plannerCelBuilder() { - // TODO: Replace with factory once available. - return CelImpl.newBuilder( - CelCompilerImpl.newBuilder( - CelParserImpl.newBuilder(), - CelCheckerLegacyImpl.newBuilder().setStandardEnvironmentEnabled(true)), - CelRuntimeImpl.newBuilder()) - // CEL-Internal-2 - .setOptions(CelOptions.current().build()); - } - private CelBuilder newCelBuilder() { return newCelBuilder(Integer.MAX_VALUE); } @@ -126,7 +111,7 @@ private CelBuilder newCelBuilder(int version) { switch (testMode) { case PLANNER_PARSE_ONLY: case PLANNER_CHECKED: - celBuilder = plannerCelBuilder(); + celBuilder = CelExperimentalFactory.plannerCelBuilder(); break; case LEGACY_CHECKED: celBuilder = CelFactory.standardCelBuilder(); diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index 72ec02d12..55ee241a0 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -29,6 +29,14 @@ java_library( ], ) +java_library( + name = "runtime_experimental_factory", + visibility = ["//:internal"], + exports = [ + "//runtime/src/main/java/dev/cel/runtime:runtime_experimental_factory", + ], +) + java_library( name = "runtime_legacy_impl", visibility = ["//:internal"], diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 70568cd90..10dca9ece 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -912,6 +912,19 @@ java_library( ], ) +java_library( + name = "runtime_experimental_factory", + srcs = ["CelRuntimeExperimentalFactory.java"], + tags = [ + ], + deps = [ + ":runtime", + ":runtime_planner_impl", + "//common:options", + "//common/annotations", + ], +) + java_library( name = "runtime", srcs = RUNTIME_SOURCES, diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeExperimentalFactory.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeExperimentalFactory.java new file mode 100644 index 000000000..d0089e48d --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeExperimentalFactory.java @@ -0,0 +1,52 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import dev.cel.common.CelOptions; +import dev.cel.common.annotations.Beta; + +/** + * Experimental helper class to construct new {@code CelRuntime} instances backed by the new {@code + * ProgramPlanner} architecture. + * + *

All APIs and behaviors surfaced here are subject to change. + */ +@Beta +public final class CelRuntimeExperimentalFactory { + + /** + * Create a new builder for constructing a {@code CelRuntime} instance. + * + *

The {@code ProgramPlanner} architecture provides key benefits over the legacy runtime: + * + *

    + *
  • Performance: Programs can be cached for improving evaluation speed. + *
  • Parsed-only expression evaluation: Unlike the traditional legacy runtime, which + * only supported evaluating type-checked expressions, this architecture handles both + * parsed-only and type-checked expressions. + *
+ */ + public static CelRuntimeBuilder plannerRuntimeBuilder() { + return CelRuntimeImpl.newBuilder() + // CEL-Internal-2 + .setOptions( + CelOptions.current() + .enableTimestampEpoch(true) + .enableHeterogeneousNumericComparisons(true) + .build()); + } + + private CelRuntimeExperimentalFactory() {} +} diff --git a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel index 569d7372d..8a0b1f9de 100644 --- a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel @@ -138,7 +138,7 @@ java_library( "//common/types:type_providers", "//extensions", "//runtime", - "//runtime:runtime_planner_impl", + "//runtime:runtime_experimental_factory", "//testing:base_interpreter_test", "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", diff --git a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java index 7518951c7..3254855c7 100644 --- a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java +++ b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java @@ -33,9 +33,8 @@ public class PlannerInterpreterTest extends BaseInterpreterTest { @Override protected CelRuntimeBuilder newBaseRuntimeBuilder(CelOptions celOptions) { - return CelRuntimeImpl.newBuilder() + return CelRuntimeExperimentalFactory.plannerRuntimeBuilder() .addLateBoundFunctions("record") - // CEL-Internal-2 .setOptions(celOptions) .addLibraries(CelExtensions.optional()) .addFileTypes(TEST_FILE_DESCRIPTORS); From d499ba8e36e974e59e4b36d74299d5ef56f1399f Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 26 Feb 2026 15:16:48 -0800 Subject: [PATCH 008/204] Prevent planning createMap for heterogeneous duplicate keys PiperOrigin-RevId: 875912898 --- .../test/java/dev/cel/conformance/BUILD.bazel | 1 - .../dev/cel/runtime/planner/EvalCreateMap.java | 17 +++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel index 375cd8bfa..c8cc06cb0 100644 --- a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -175,7 +175,6 @@ _TESTS_TO_SKIP_PLANNER = [ "timestamps/timestamp_range/sub_time_duration_under", # Skip until fixed. - "fields/qualified_identifier_resolution/map_value_repeat_key_heterogeneous", "optionals/optionals/map_null_entry_no_such_key", "optionals/optionals/map_present_key_invalid_field", "parse/receiver_function_names", diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java index c09c19987..f6f73e842 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java @@ -59,13 +59,26 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval keyInterpretable.exprId()); } - Object val = values[i].eval(resolver, frame); + boolean isDuplicate = !keysSeen.add(key); + if (!isDuplicate) { + if (key instanceof Long) { + long longVal = (Long) key; + if (longVal >= 0) { + isDuplicate = keysSeen.contains(UnsignedLong.valueOf(longVal)); + } + } else if (key instanceof UnsignedLong) { + UnsignedLong ulongVal = (UnsignedLong) key; + isDuplicate = keysSeen.contains(ulongVal.longValue()); + } + } - if (!keysSeen.add(key)) { + if (isDuplicate) { throw new LocalizedEvaluationException( CelDuplicateKeyException.of(key), keyInterpretable.exprId()); } + Object val = values[i].eval(resolver, frame); + if (isOptional[i]) { if (!(val instanceof Optional)) { throw new IllegalArgumentException( From 5103b301547226801c41c158dea4ee751bfb5a7d Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 26 Feb 2026 16:15:47 -0800 Subject: [PATCH 009/204] Fix optionals to properly error on invalid qualification PiperOrigin-RevId: 875937520 --- .../test/java/dev/cel/conformance/BUILD.bazel | 2 -- .../cel/runtime/planner/StringQualifier.java | 32 +++++++++++++------ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel index c8cc06cb0..717f7aaa0 100644 --- a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -175,8 +175,6 @@ _TESTS_TO_SKIP_PLANNER = [ "timestamps/timestamp_range/sub_time_duration_under", # Skip until fixed. - "optionals/optionals/map_null_entry_no_such_key", - "optionals/optionals/map_present_key_invalid_field", "parse/receiver_function_names", "proto2/extensions_get/package_scoped_test_all_types_ext", "proto2/extensions_get/package_scoped_repeated_test_all_types", diff --git a/runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java b/runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java index 4ceaa0e51..293ca5c7d 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java @@ -15,6 +15,7 @@ package dev.cel.runtime.planner; import dev.cel.common.exceptions.CelAttributeNotFoundException; +import dev.cel.common.values.OptionalValue; import dev.cel.common.values.SelectableValue; import java.util.Map; @@ -31,21 +32,34 @@ public String value() { @Override @SuppressWarnings("unchecked") // Qualifications on maps/structs must be a string public Object qualify(Object obj) { + if (obj instanceof OptionalValue) { + OptionalValue opt = (OptionalValue) obj; + if (!opt.isZeroValue()) { + Object inner = opt.value(); + if (!(inner instanceof SelectableValue) && !(inner instanceof Map)) { + throw CelAttributeNotFoundException.forFieldResolution(value); + } + } + } + if (obj instanceof SelectableValue) { return ((SelectableValue) obj).select(value); - } else if (obj instanceof Map) { - Map map = (Map) obj; - if (!map.containsKey(value)) { - throw CelAttributeNotFoundException.forMissingMapKey(value); - } + } + if (obj instanceof Map) { + Map map = (Map) obj; Object mapVal = map.get(value); - if (mapVal == null) { - throw CelAttributeNotFoundException.of( - String.format("Map value cannot be null for key: %s", value)); + if (mapVal != null) { + return mapVal; + } + + if (!map.containsKey(value)) { + throw CelAttributeNotFoundException.forMissingMapKey(value); } - return map.get(value); + + throw CelAttributeNotFoundException.of( + String.format("Map value cannot be null for key: %s", value)); } throw CelAttributeNotFoundException.forFieldResolution(value); From 9e1b5eeda3b7fb89b792eaee5d8d339585267676 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 27 Feb 2026 17:26:41 -0800 Subject: [PATCH 010/204] Fix CelValue adaptation for lists/maps PiperOrigin-RevId: 876477992 --- .../cel/common/values/CelValueConverter.java | 57 +++++++++++++++---- .../common/values/CelValueConverterTest.java | 5 +- .../values/ProtoCelValueConverterTest.java | 2 +- .../test/java/dev/cel/conformance/BUILD.bazel | 6 -- .../runtime/CelValueRuntimeTypeProvider.java | 19 +------ .../java/dev/cel/runtime/planner/BUILD.bazel | 2 - .../dev/cel/runtime/planner/EvalHelpers.java | 19 +++---- .../runtime/planner/NamespacedAttribute.java | 7 +-- .../runtime/planner/RelativeAttribute.java | 6 +- .../runtime/planner/ProgramPlannerTest.java | 29 ++++++++++ 10 files changed, 92 insertions(+), 60 deletions(-) diff --git a/common/src/main/java/dev/cel/common/values/CelValueConverter.java b/common/src/main/java/dev/cel/common/values/CelValueConverter.java index fda014f31..2af0a76cb 100644 --- a/common/src/main/java/dev/cel/common/values/CelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/CelValueConverter.java @@ -41,24 +41,39 @@ public static CelValueConverter getDefaultInstance() { return DEFAULT_INSTANCE; } - /** Adapts a {@link CelValue} to a plain old Java Object. */ - public Object unwrap(CelValue celValue) { - Preconditions.checkNotNull(celValue); + /** + * Unwraps the {@code value} into its plain old Java Object representation. + * + *

The value may be a {@link CelValue}, a {@link Collection} or a {@link Map}. + */ + public Object maybeUnwrap(Object value) { + if (value instanceof CelValue) { + return unwrap((CelValue) value); + } - if (celValue instanceof OptionalValue) { - OptionalValue optionalValue = (OptionalValue) celValue; - if (optionalValue.isZeroValue()) { - return Optional.empty(); + if (value instanceof Collection) { + Collection collection = (Collection) value; + ImmutableList.Builder builder = + ImmutableList.builderWithExpectedSize(collection.size()); + for (Object element : collection) { + builder.add(maybeUnwrap(element)); } - return Optional.of(optionalValue.value()); + return builder.build(); } - if (celValue instanceof ErrorValue) { - return celValue; + if (value instanceof Map) { + Map map = (Map) value; + ImmutableMap.Builder builder = + ImmutableMap.builderWithExpectedSize(map.size()); + for (Map.Entry entry : map.entrySet()) { + builder.put(maybeUnwrap(entry.getKey()), maybeUnwrap(entry.getValue())); + } + + return builder.buildOrThrow(); } - return celValue.value(); + return value; } /** @@ -101,6 +116,26 @@ protected Object normalizePrimitive(Object value) { return value; } + /** Adapts a {@link CelValue} to a plain old Java Object. */ + private static Object unwrap(CelValue celValue) { + Preconditions.checkNotNull(celValue); + + if (celValue instanceof OptionalValue) { + OptionalValue optionalValue = (OptionalValue) celValue; + if (optionalValue.isZeroValue()) { + return Optional.empty(); + } + + return Optional.of(optionalValue.value()); + } + + if (celValue instanceof ErrorValue) { + return celValue; + } + + return celValue.value(); + } + private ImmutableList toListValue(Collection iterable) { Preconditions.checkNotNull(iterable); diff --git a/common/src/test/java/dev/cel/common/values/CelValueConverterTest.java b/common/src/test/java/dev/cel/common/values/CelValueConverterTest.java index 308d7b510..ccb8e605f 100644 --- a/common/src/test/java/dev/cel/common/values/CelValueConverterTest.java +++ b/common/src/test/java/dev/cel/common/values/CelValueConverterTest.java @@ -37,7 +37,8 @@ public void toRuntimeValue_optionalValue() { @Test @SuppressWarnings("unchecked") // Test only public void unwrap_optionalValue() { - Optional result = (Optional) CEL_VALUE_CONVERTER.unwrap(OptionalValue.create(2L)); + Optional result = + (Optional) CEL_VALUE_CONVERTER.maybeUnwrap(OptionalValue.create(2L)); assertThat(result).isEqualTo(Optional.of(2L)); } @@ -45,7 +46,7 @@ public void unwrap_optionalValue() { @Test @SuppressWarnings("unchecked") // Test only public void unwrap_emptyOptionalValue() { - Optional result = (Optional) CEL_VALUE_CONVERTER.unwrap(OptionalValue.EMPTY); + Optional result = (Optional) CEL_VALUE_CONVERTER.maybeUnwrap(OptionalValue.EMPTY); assertThat(result).isEqualTo(Optional.empty()); } diff --git a/common/src/test/java/dev/cel/common/values/ProtoCelValueConverterTest.java b/common/src/test/java/dev/cel/common/values/ProtoCelValueConverterTest.java index a517931c2..17c012db7 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoCelValueConverterTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoCelValueConverterTest.java @@ -35,7 +35,7 @@ public class ProtoCelValueConverterTest { @Test public void unwrap_nullValue() { - NullValue nullValue = (NullValue) PROTO_CEL_VALUE_CONVERTER.unwrap(NullValue.NULL_VALUE); + NullValue nullValue = (NullValue) PROTO_CEL_VALUE_CONVERTER.maybeUnwrap(NullValue.NULL_VALUE); // Note: No conversion is attempted. We're using dev.cel.common.values.NullValue.NULL_VALUE as // the diff --git a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel index 717f7aaa0..d6b2296e5 100644 --- a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -176,12 +176,6 @@ _TESTS_TO_SKIP_PLANNER = [ # Skip until fixed. "parse/receiver_function_names", - "proto2/extensions_get/package_scoped_test_all_types_ext", - "proto2/extensions_get/package_scoped_repeated_test_all_types", - "proto2/extensions_get/message_scoped_nested_ext", - "proto2/extensions_get/message_scoped_repeated_test_all_types", - "proto2_ext/get_ext/package_scoped_repeated_test_all_types", - "proto2_ext/get_ext/message_scoped_repeated_test_all_types", # TODO: Fix null assignment to a field "proto2/set_null/single_message", diff --git a/runtime/src/main/java/dev/cel/runtime/CelValueRuntimeTypeProvider.java b/runtime/src/main/java/dev/cel/runtime/CelValueRuntimeTypeProvider.java index e071289ca..38365127c 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelValueRuntimeTypeProvider.java +++ b/runtime/src/main/java/dev/cel/runtime/CelValueRuntimeTypeProvider.java @@ -22,7 +22,6 @@ import dev.cel.common.exceptions.CelAttributeNotFoundException; import dev.cel.common.values.BaseProtoCelValueConverter; import dev.cel.common.values.BaseProtoMessageValueProvider; -import dev.cel.common.values.CelValue; import dev.cel.common.values.CelValueProvider; import dev.cel.common.values.CombinedCelValueProvider; import dev.cel.common.values.SelectableValue; @@ -64,7 +63,7 @@ static CelValueRuntimeTypeProvider newInstance(CelValueProvider valueProvider) { @Override public Object createMessage(String messageName, Map values) { - return maybeUnwrapCelValue( + return protoCelValueConverter.maybeUnwrap( valueProvider .newValue(messageName, values) .orElseThrow( @@ -87,7 +86,7 @@ public Object selectField(Object message, String fieldName) { SelectableValue selectableValue = getSelectableValueOrThrow(message, fieldName); Object value = selectableValue.select(fieldName); - return maybeUnwrapCelValue(value); + return protoCelValueConverter.maybeUnwrap(value); } @Override @@ -120,24 +119,12 @@ public Object adapt(String messageName, Object message) { } if (message instanceof MessageLite) { - return maybeUnwrapCelValue(protoCelValueConverter.toRuntimeValue(message)); + return protoCelValueConverter.maybeUnwrap(protoCelValueConverter.toRuntimeValue(message)); } return message; } - /** - * DefaultInterpreter cannot handle CelValue and instead expects plain Java objects. - * - *

This will become unnecessary once we introduce a rewrite of a Cel runtime. - */ - private Object maybeUnwrapCelValue(Object object) { - if (object instanceof CelValue) { - return protoCelValueConverter.unwrap((CelValue) object); - } - return object; - } - private static void throwInvalidFieldSelection(String fieldName) { throw CelAttributeNotFoundException.forFieldResolution(fieldName); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index f7912cff2..6561e4e5c 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -128,7 +128,6 @@ java_library( "//common/types", "//common/types:type_providers", "//common/values", - "//common/values:cel_value", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -380,7 +379,6 @@ java_library( "//common:error_codes", "//common/exceptions:runtime_exception", "//common/values", - "//common/values:cel_value", "//runtime:evaluation_exception", "//runtime:interpretable", "//runtime:resolved_overload", diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java index 08f4fa8a8..92d234acc 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java @@ -17,7 +17,6 @@ import com.google.common.base.Joiner; import dev.cel.common.CelErrorCode; import dev.cel.common.exceptions.CelRuntimeException; -import dev.cel.common.values.CelValue; import dev.cel.common.values.CelValueConverter; import dev.cel.common.values.ErrorValue; import dev.cel.runtime.CelEvaluationException; @@ -56,23 +55,21 @@ static Object evalStrictly( } } - static Object dispatch(CelResolvedOverload overload, CelValueConverter valueConverter, Object[] args) throws CelEvaluationException { + static Object dispatch( + CelResolvedOverload overload, CelValueConverter valueConverter, Object[] args) + throws CelEvaluationException { try { Object result = overload.getDefinition().apply(args); - Object runtimeValue = valueConverter.toRuntimeValue(result); - if (runtimeValue instanceof CelValue) { - return valueConverter.unwrap((CelValue) runtimeValue); - } - - return runtimeValue; + return valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result)); } catch (CelRuntimeException e) { // Function dispatch failure that's already been handled -- just propagate. throw e; } catch (RuntimeException e) { // Unexpected function dispatch failure. - throw new IllegalArgumentException(String.format( - "Function '%s' failed with arg(s) '%s'", - overload.getOverloadId(), Joiner.on(", ").join(args)), + throw new IllegalArgumentException( + String.format( + "Function '%s' failed with arg(s) '%s'", + overload.getOverloadId(), Joiner.on(", ").join(args)), e); } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java index de1a90291..cc8ca1d97 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java @@ -22,7 +22,6 @@ import dev.cel.common.types.EnumType; import dev.cel.common.types.SimpleType; import dev.cel.common.types.TypeType; -import dev.cel.common.values.CelValue; import dev.cel.common.values.CelValueConverter; import dev.cel.runtime.GlobalResolver; import java.util.NoSuchElementException; @@ -148,11 +147,7 @@ private static Object applyQualifiers( obj = qualifier.qualify(obj); } - if (obj instanceof CelValue) { - obj = celValueConverter.unwrap((CelValue) obj); - } - - return obj; + return celValueConverter.maybeUnwrap(obj); } static NamespacedAttribute create( diff --git a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java index 54eb26f21..b3d83c390 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java @@ -16,7 +16,6 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; -import dev.cel.common.values.CelValue; import dev.cel.common.values.CelValueConverter; import dev.cel.runtime.GlobalResolver; @@ -41,10 +40,7 @@ public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { } // TODO: Handle unknowns - if (obj instanceof CelValue) { - obj = celValueConverter.unwrap((CelValue) obj); - } - return obj; + return celValueConverter.maybeUnwrap(obj); } @Override diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index 30b100bc2..20b4e641a 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -316,6 +316,35 @@ public void plan_ident_variable() throws Exception { assertThat(result).isEqualTo(1); } + @Test + public void plan_ident_variableWithStructInList() throws Exception { + CelAbstractSyntaxTree ast = compile("dyn_var"); + Program program = PLANNER.plan(ast); + + Object result = + program.eval( + ImmutableMap.of( + "dyn_var", ImmutableList.of(TestAllTypes.newBuilder().setSingleInt32(42).build()))); + + assertThat(result) + .isEqualTo(ImmutableList.of(TestAllTypes.newBuilder().setSingleInt32(42).build())); + } + + @Test + public void plan_ident_variableWithStructInMap() throws Exception { + CelAbstractSyntaxTree ast = compile("dyn_var"); + Program program = PLANNER.plan(ast); + + Object result = + program.eval( + ImmutableMap.of( + "dyn_var", + ImmutableMap.of("foo", TestAllTypes.newBuilder().setSingleInt32(42).build()))); + + assertThat(result) + .isEqualTo(ImmutableMap.of("foo", TestAllTypes.newBuilder().setSingleInt32(42).build())); + } + @Test public void planIdent_typeLiteral(@TestParameter TypeLiteralTestCase testCase) throws Exception { CelAbstractSyntaxTree ast = compile(testCase.expression); From 3fd7b96241ce54155fcd515a3bbb58b3f83b5ed3 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 27 Feb 2026 18:50:10 -0800 Subject: [PATCH 011/204] Fix null assignment to fields PiperOrigin-RevId: 876500682 --- .../dev/cel/common/internal/ProtoAdapter.java | 33 +++++++++++------ .../common/values/ProtoCelValueConverter.java | 2 +- .../cel/common/internal/ProtoAdapterTest.java | 17 ++++++--- .../test/java/dev/cel/conformance/BUILD.bazel | 17 --------- .../dev/cel/conformance/ConformanceTest.java | 2 +- .../cel/runtime/CelLiteInterpreterTest.java | 5 +++ .../test/resources/nullAssignability.baseline | 35 +++++++++++++++++++ .../dev/cel/testing/BaseInterpreterTest.java | 25 +++++++++++++ 8 files changed, 103 insertions(+), 33 deletions(-) create mode 100644 runtime/src/test/resources/nullAssignability.baseline diff --git a/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java b/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java index 962a9d2e9..b6648a5b8 100644 --- a/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java +++ b/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java @@ -204,8 +204,29 @@ public Optional adaptFieldToValue(FieldDescriptor fieldDescriptor, Objec @SuppressWarnings({"unchecked", "rawtypes"}) public Optional adaptValueToFieldType( FieldDescriptor fieldDescriptor, Object fieldValue) { - if (isWrapperType(fieldDescriptor) && fieldValue.equals(NullValue.NULL_VALUE)) { - return Optional.empty(); + if (fieldValue instanceof NullValue) { + // `null` assignment to fields indicate that the field would not be set + // in a protobuf message (e.g: Message{msg_field: null} -> Message{}) + // + // We explicitly check below for invalid null assignments, such as repeated + // or map fields. (e.g: Message{repeated_field: null} -> Error) + if (fieldDescriptor.isMapField() + || fieldDescriptor.isRepeated() + || fieldDescriptor.getJavaType() != FieldDescriptor.JavaType.MESSAGE + || WellKnownProto.JSON_STRUCT_VALUE + .typeName() + .equals(fieldDescriptor.getMessageType().getFullName()) + || WellKnownProto.JSON_LIST_VALUE + .typeName() + .equals(fieldDescriptor.getMessageType().getFullName())) { + throw new IllegalArgumentException("Unsupported field type"); + } + + String typeFullName = fieldDescriptor.getMessageType().getFullName(); + if (!WellKnownProto.ANY_VALUE.typeName().equals(typeFullName) + && !WellKnownProto.JSON_VALUE.typeName().equals(typeFullName)) { + return Optional.empty(); + } } if (fieldDescriptor.isMapField()) { Descriptor entryDescriptor = fieldDescriptor.getMessageType(); @@ -370,14 +391,6 @@ private static String typeName(Descriptor protoType) { return protoType.getFullName(); } - private static boolean isWrapperType(FieldDescriptor fieldDescriptor) { - if (fieldDescriptor.getJavaType() != FieldDescriptor.JavaType.MESSAGE) { - return false; - } - String fieldTypeName = fieldDescriptor.getMessageType().getFullName(); - return WellKnownProto.isWrapperType(fieldTypeName); - } - private static int intCheckedCast(long value) { try { return Ints.checkedCast(value); diff --git a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java index c7b829e13..565c65438 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java @@ -67,7 +67,7 @@ protected Object fromWellKnownProto(MessageLiteOrBuilder msg, WellKnownProto wel try { unpackedMessage = dynamicProto.unpack((Any) message); } catch (InvalidProtocolBufferException e) { - throw new IllegalStateException( + throw new IllegalArgumentException( "Unpacking failed for message: " + message.getDescriptorForType().getFullName(), e); } return toRuntimeValue(unpackedMessage); diff --git a/common/src/test/java/dev/cel/common/internal/ProtoAdapterTest.java b/common/src/test/java/dev/cel/common/internal/ProtoAdapterTest.java index 61c71e4a6..91e0e22db 100644 --- a/common/src/test/java/dev/cel/common/internal/ProtoAdapterTest.java +++ b/common/src/test/java/dev/cel/common/internal/ProtoAdapterTest.java @@ -150,10 +150,7 @@ public static List data() { @Test public void adaptValueToProto_bidirectionalConversion() { DynamicProto dynamicProto = DynamicProto.create(DefaultMessageFactory.INSTANCE); - ProtoAdapter protoAdapter = - new ProtoAdapter( - dynamicProto, - CelOptions.current().build()); + ProtoAdapter protoAdapter = new ProtoAdapter(dynamicProto, CelOptions.current().build()); assertThat(protoAdapter.adaptValueToProto(value, proto.getDescriptorForType().getFullName())) .isEqualTo(proto); assertThat(protoAdapter.adaptProtoToValue(proto)).isEqualTo(value); @@ -181,6 +178,18 @@ public void adaptAnyValue_hermeticTypes_bidirectionalConversion() { @RunWith(JUnit4.class) public static class AsymmetricConversionTest { + + @Test + public void unpackAny_celNullValue() throws Exception { + ProtoAdapter protoAdapter = new ProtoAdapter(DYNAMIC_PROTO, CelOptions.DEFAULT); + Any any = + (Any) + protoAdapter.adaptValueToProto( + dev.cel.common.values.NullValue.NULL_VALUE, "google.protobuf.Any"); + Object unpacked = protoAdapter.adaptProtoToValue(any); + assertThat(unpacked).isEqualTo(dev.cel.common.values.NullValue.NULL_VALUE); + } + @Test public void adaptValueToProto_asymmetricFloatConversion() { ProtoAdapter protoAdapter = new ProtoAdapter(DYNAMIC_PROTO, CelOptions.DEFAULT); diff --git a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel index d6b2296e5..fb2b1a159 100644 --- a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -124,14 +124,6 @@ _TESTS_TO_SKIP_LEGACY = [ "string_ext/format", "string_ext/format_errors", - # TODO: Fix null assignment to a field - "proto2/set_null/single_message", - "proto2/set_null/single_duration", - "proto2/set_null/single_timestamp", - "proto3/set_null/single_message", - "proto3/set_null/single_duration", - "proto3/set_null/single_timestamp", - # Future features for CEL 1.0 # TODO: Strong typing support for enums, specified but not implemented. "enums/strong_proto2", @@ -162,7 +154,6 @@ _TESTS_TO_SKIP_PLANNER = [ "string_ext/format_errors", # TODO: Check behavior for go/cpp - "basic/functions/unbound", "basic/functions/unbound_is_runtime_error", # TODO: Ensure overflow occurs on conversions of double values which might not work properly on all platforms. @@ -177,14 +168,6 @@ _TESTS_TO_SKIP_PLANNER = [ # Skip until fixed. "parse/receiver_function_names", - # TODO: Fix null assignment to a field - "proto2/set_null/single_message", - "proto2/set_null/single_duration", - "proto2/set_null/single_timestamp", - "proto3/set_null/single_message", - "proto3/set_null/single_duration", - "proto3/set_null/single_timestamp", - # Type inference edgecases around null(able) assignability. # These type check, but resolve to a different type. # list(int), want list(wrapper(int)) diff --git a/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java index 437e50fea..5a25fb9d9 100644 --- a/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java +++ b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java @@ -210,10 +210,10 @@ public void evaluate() throws Throwable { } CelRuntime runtime = getRuntime(test, usePlanner); - Program program = runtime.createProgram(response.getAst()); ExprValue result = null; CelEvaluationException error = null; try { + Program program = runtime.createProgram(response.getAst()); result = toExprValue(program.eval(getBindings(test)), response.getAst().getResultType()); } catch (CelEvaluationException e) { error = e; diff --git a/runtime/src/test/java/dev/cel/runtime/CelLiteInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/CelLiteInterpreterTest.java index b3a1f2efa..1d1a316c0 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelLiteInterpreterTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelLiteInterpreterTest.java @@ -54,6 +54,11 @@ public void dynamicMessage_dynamicDescriptor() throws Exception { // All the tests below rely on message creation with fields populated. They are excluded for time // being until this support is added. + @Override + public void nullAssignability() throws Exception { + skipBaselineVerification(); + } + @Override public void wrappers() throws Exception { skipBaselineVerification(); diff --git a/runtime/src/test/resources/nullAssignability.baseline b/runtime/src/test/resources/nullAssignability.baseline new file mode 100644 index 000000000..47b9c7a0d --- /dev/null +++ b/runtime/src/test/resources/nullAssignability.baseline @@ -0,0 +1,35 @@ +Source: TestAllTypes{single_int64_wrapper: null}.single_int64_wrapper == null +=====> +bindings: {} +result: true + +Source: TestAllTypes{}.single_int64_wrapper == null +=====> +bindings: {} +result: true + +Source: has(TestAllTypes{single_int64_wrapper: null}.single_int64_wrapper) +=====> +bindings: {} +result: false + +Source: TestAllTypes{single_value: null}.single_value == null +=====> +bindings: {} +result: true + +Source: has(TestAllTypes{single_value: null}.single_value) +=====> +bindings: {} +result: true + +Source: TestAllTypes{single_timestamp: null}.single_timestamp == timestamp(0) +=====> +bindings: {} +result: true + +Source: has(TestAllTypes{single_timestamp: null}.single_timestamp) +=====> +bindings: {} +result: false + diff --git a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java index bc67e8218..144ada5a8 100644 --- a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java +++ b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java @@ -2122,6 +2122,31 @@ public void wrappers() throws Exception { runTest(); } + @Test + public void nullAssignability() throws Exception { + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); + source = "TestAllTypes{single_int64_wrapper: null}.single_int64_wrapper == null"; + runTest(); + + source = "TestAllTypes{}.single_int64_wrapper == null"; + runTest(); + + source = "has(TestAllTypes{single_int64_wrapper: null}.single_int64_wrapper)"; + runTest(); + + source = "TestAllTypes{single_value: null}.single_value == null"; + runTest(); + + source = "has(TestAllTypes{single_value: null}.single_value)"; + runTest(); + + source = "TestAllTypes{single_timestamp: null}.single_timestamp == timestamp(0)"; + runTest(); + + source = "has(TestAllTypes{single_timestamp: null}.single_timestamp)"; + runTest(); + } + @Test public void longComprehension() { ImmutableList l = LongStream.range(0L, 1000L).boxed().collect(toImmutableList()); From a881ed4f02f74bd418a08046886a89b1aed9517b Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 2 Mar 2026 09:55:25 -0800 Subject: [PATCH 012/204] Prepare 0.12.0 Release PiperOrigin-RevId: 877448588 --- MODULE.bazel | 2 +- README.md | 4 ++-- publish/BUILD.bazel | 3 +++ publish/cel_version.bzl | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index fd35e41a2..0b67c825c 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -46,7 +46,7 @@ TRUTH_VERSION = "1.4.4" PROTOBUF_JAVA_VERSION = "4.33.5" -CEL_VERSION = "0.12.0-SNAPSHOT" +CEL_VERSION = "0.12.0" # Compile only artifacts [ diff --git a/README.md b/README.md index f46a1f8c6..40bd9deac 100644 --- a/README.md +++ b/README.md @@ -55,14 +55,14 @@ CEL-Java is available in Maven Central Repository. [Download the JARs here][8] o dev.cel cel - 0.11.1 + 0.12.0 ``` **Gradle** ```gradle -implementation 'dev.cel:cel:0.11.1' +implementation 'dev.cel:cel:0.12.0' ``` Then run this example: diff --git a/publish/BUILD.bazel b/publish/BUILD.bazel index cb13a70b5..d905edc4b 100644 --- a/publish/BUILD.bazel +++ b/publish/BUILD.bazel @@ -21,6 +21,7 @@ COMMON_TARGETS = [ "//common/src/main/java/dev/cel/common/internal:file_descriptor_converter", "//common/src/main/java/dev/cel/common/internal:safe_string_formatter", "//common/src/main/java/dev/cel/common/types:cel_types", + "//common/src/main/java/dev/cel/common/types:message_type_provider", "//common/src/main/java/dev/cel/common/values", "//common/src/main/java/dev/cel/common/values:cel_value", ] @@ -31,6 +32,7 @@ RUNTIME_TARGETS = [ "//runtime/src/main/java/dev/cel/runtime:base", "//runtime/src/main/java/dev/cel/runtime:interpreter", "//runtime/src/main/java/dev/cel/runtime:late_function_binding", + "//runtime/src/main/java/dev/cel/runtime:runtime_experimental_factory", "//runtime/src/main/java/dev/cel/runtime:runtime_factory", "//runtime/src/main/java/dev/cel/runtime:runtime_helpers", "//runtime/src/main/java/dev/cel/runtime:runtime_legacy_impl", @@ -123,6 +125,7 @@ EXTENSION_TARGETS = [ # keep sorted BUNDLE_TARGETS = [ "//bundle/src/main/java/dev/cel/bundle:cel", + "//bundle/src/main/java/dev/cel/bundle:cel_experimental_factory", "//bundle/src/main/java/dev/cel/bundle:environment", "//bundle/src/main/java/dev/cel/bundle:environment_yaml_parser", ] diff --git a/publish/cel_version.bzl b/publish/cel_version.bzl index ea793eee5..b40addd73 100644 --- a/publish/cel_version.bzl +++ b/publish/cel_version.bzl @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. """Maven artifact version for CEL.""" -CEL_VERSION = "0.12.0-SNAPSHOT" +CEL_VERSION = "0.12.0" From 84b5c5d60bab86c1d758df72a7a66fedf6d8bd97 Mon Sep 17 00:00:00 2001 From: Jonathan Tatum Date: Thu, 5 Mar 2026 10:53:01 -0800 Subject: [PATCH 013/204] Use structured types in YAML export. Updates Environment export to render types in variable and function declarations as structured maps instead of a string of the formatted name. PiperOrigin-RevId: 879147753 --- .../cel/bundle/CelEnvironmentExporter.java | 9 ++- .../bundle/CelEnvironmentExporterTest.java | 78 +++++++++++++++++++ .../CelEnvironmentYamlSerializerTest.java | 62 +++++++++++++++ .../test/resources/environment/dump_env.yaml | 35 +++++++++ 4 files changed, 182 insertions(+), 2 deletions(-) diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java index f86787090..d233fd36f 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java @@ -40,9 +40,9 @@ import dev.cel.common.CelVarDecl; import dev.cel.common.internal.EnvVisitable; import dev.cel.common.internal.EnvVisitor; +import dev.cel.common.types.CelKind; import dev.cel.common.types.CelProtoTypes; import dev.cel.common.types.CelType; -import dev.cel.common.types.CelTypes; import dev.cel.compiler.CelCompiler; import dev.cel.extensions.CelExtensionLibrary; import dev.cel.extensions.CelExtensions; @@ -484,7 +484,12 @@ private CelEnvironment.OverloadDecl toCelEnvOverloadDecl(CelOverloadDecl overloa } private CelEnvironment.TypeDecl toCelEnvTypeDecl(CelType type) { - return CelEnvironment.TypeDecl.create(CelTypes.format(type)); + return CelEnvironment.TypeDecl.newBuilder() + .setName(type.name()) + .setIsTypeParam(type.kind() == CelKind.TYPE_PARAM) + .addParams( + type.parameters().stream().map(this::toCelEnvTypeDecl).collect(toImmutableList())) + .build(); } /** Wrapper for CelOverloadDecl, associating it with the corresponding function name. */ diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java index f70f1d466..10b9dee8e 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java @@ -36,8 +36,10 @@ import dev.cel.common.CelOptions; import dev.cel.common.CelOverloadDecl; import dev.cel.common.CelVarDecl; +import dev.cel.common.types.ListType; import dev.cel.common.types.OpaqueType; import dev.cel.common.types.SimpleType; +import dev.cel.common.types.TypeParamType; import dev.cel.extensions.CelExtensions; import java.net.URL; import java.util.HashSet; @@ -176,6 +178,20 @@ public void customFunctions() { "math.isFinite", CelOverloadDecl.newGlobalOverload( "math_isFinite_int64", SimpleType.BOOL, SimpleType.INT)), + CelFunctionDecl.newFunctionDeclaration( + "zipGeneric", + CelOverloadDecl.newGlobalOverload( + "zip_list_list", + ListType.create(ListType.create(TypeParamType.create("T"))), + ListType.create(TypeParamType.create("T")), + ListType.create(TypeParamType.create("T")))), + CelFunctionDecl.newFunctionDeclaration( + "zip", + CelOverloadDecl.newGlobalOverload( + "zip_list_int_list_int", + ListType.create(ListType.create(SimpleType.INT)), + ListType.create(SimpleType.INT), + ListType.create(SimpleType.INT))), CelFunctionDecl.newFunctionDeclaration( "addWeeks", CelOverloadDecl.newMemberOverload( @@ -207,6 +223,68 @@ public void customFunctions() { .setTarget(TypeDecl.create("google.protobuf.Timestamp")) .setArguments(ImmutableList.of(TypeDecl.create("int"))) .setReturnType(TypeDecl.create("bool")) + .build())), + FunctionDecl.create( + "zipGeneric", + ImmutableSet.of( + OverloadDecl.newBuilder() + .setId("zip_list_list") + .setArguments( + ImmutableList.of( + TypeDecl.newBuilder() + .setName("list") + .addParams( + TypeDecl.newBuilder() + .setName("T") + .setIsTypeParam(true) + .build()) + .build(), + TypeDecl.newBuilder() + .setName("list") + .addParams( + TypeDecl.newBuilder() + .setName("T") + .setIsTypeParam(true) + .build()) + .build())) + .setReturnType( + TypeDecl.newBuilder() + .setName("list") + .addParams( + TypeDecl.newBuilder() + .setName("list") + .addParams( + TypeDecl.newBuilder() + .setName("T") + .setIsTypeParam(true) + .build()) + .build()) + .build()) + .build())), + FunctionDecl.create( + "zip", + ImmutableSet.of( + OverloadDecl.newBuilder() + .setId("zip_list_int_list_int") + .setArguments( + ImmutableList.of( + TypeDecl.newBuilder() + .setName("list") + .addParams(TypeDecl.create("int")) + .build(), + TypeDecl.newBuilder() + .setName("list") + .addParams(TypeDecl.create("int")) + .build())) + .setReturnType( + TypeDecl.newBuilder() + .setName("list") + .addParams( + TypeDecl.newBuilder() + .setName("list") + .addParams(TypeDecl.create("int")) + .build()) + .build()) .build()))); // Random-check some standard functions: we don't want to see them explicitly defined. diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java index 0235cb2f4..aad72a578 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java @@ -106,6 +106,68 @@ public void toYaml_success() throws Exception { .setReturnType( TypeDecl.newBuilder().setName("V").setIsTypeParam(true).build()) .build())), + FunctionDecl.create( + "zip", + ImmutableSet.of( + OverloadDecl.newBuilder() + .setId("zip_list_int_list_int") + .setArguments( + ImmutableList.of( + TypeDecl.newBuilder() + .setName("list") + .addParams(TypeDecl.create("int")) + .build(), + TypeDecl.newBuilder() + .setName("list") + .addParams(TypeDecl.create("int")) + .build())) + .setReturnType( + TypeDecl.newBuilder() + .setName("list") + .addParams( + TypeDecl.newBuilder() + .setName("list") + .addParams(TypeDecl.create("int")) + .build()) + .build()) + .build())), + FunctionDecl.create( + "zipGeneric", + ImmutableSet.of( + OverloadDecl.newBuilder() + .setId("zip_list_list") + .setArguments( + ImmutableList.of( + TypeDecl.newBuilder() + .setName("list") + .addParams( + TypeDecl.newBuilder() + .setName("T") + .setIsTypeParam(true) + .build()) + .build(), + TypeDecl.newBuilder() + .setName("list") + .addParams( + TypeDecl.newBuilder() + .setName("T") + .setIsTypeParam(true) + .build()) + .build())) + .setReturnType( + TypeDecl.newBuilder() + .setName("list") + .addParams( + TypeDecl.newBuilder() + .setName("list") + .addParams( + TypeDecl.newBuilder() + .setName("T") + .setIsTypeParam(true) + .build()) + .build()) + .build()) + .build())), FunctionDecl.create( "coalesce", ImmutableSet.of( diff --git a/testing/src/test/resources/environment/dump_env.yaml b/testing/src/test/resources/environment/dump_env.yaml index 1e7e7880b..a5ed23753 100644 --- a/testing/src/test/resources/environment/dump_env.yaml +++ b/testing/src/test/resources/environment/dump_env.yaml @@ -61,6 +61,41 @@ functions: return: type_name: V is_type_param: true +- name: zip + overloads: + - id: zip_list_int_list_int + args: + - type_name: list + params: + - type_name: int + - type_name: list + params: + - type_name: int + return: + type_name: list + params: + - type_name: list + params: + - type_name: int +- name: zipGeneric + overloads: + - id: zip_list_list + args: + - type_name: list + params: + - type_name: T + is_type_param: true + - type_name: list + params: + - type_name: T + is_type_param: true + return: + type_name: list + params: + - type_name: list + params: + - type_name: T + is_type_param: true - name: coalesce overloads: - id: coalesce_null_int From a5a1ef1c75c06574e1f9548cf576f2edf2a6d5e7 Mon Sep 17 00:00:00 2001 From: Jonathan Tatum Date: Tue, 10 Mar 2026 14:20:54 -0700 Subject: [PATCH 014/204] Add test cases around type-checking gp.NullValue google.protobuf.NullValue is represented as an enum (int in CEL), but is interpreted to mean a null literal when set as the alternative in google.protobuf.Value. It is not normally referenced directly, but should behave as an int when it is. PiperOrigin-RevId: 881621568 --- .../java/dev/cel/checker/ExprCheckerTest.java | 65 +++++++++++++++++++ .../resources/jsonTypeNullAccess.baseline | 54 +++++++++++++++ .../jsonTypeNullConstruction.baseline | 35 ++++++++++ .../test/resources/jsonValueTypes.baseline | 43 +++++++++++- .../dev/cel/testing/BaseInterpreterTest.java | 17 +++++ 5 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 checker/src/test/resources/jsonTypeNullAccess.baseline create mode 100644 checker/src/test/resources/jsonTypeNullConstruction.baseline diff --git a/checker/src/test/java/dev/cel/checker/ExprCheckerTest.java b/checker/src/test/java/dev/cel/checker/ExprCheckerTest.java index d5d5d9a3a..846201d32 100644 --- a/checker/src/test/java/dev/cel/checker/ExprCheckerTest.java +++ b/checker/src/test/java/dev/cel/checker/ExprCheckerTest.java @@ -517,6 +517,71 @@ public void jsonType() throws Exception { runTest(); } + @Test + public void jsonTypeNullConstruction() throws Exception { + // Ok + source = "google.protobuf.Value{null_value: google.protobuf.NullValue.NULL_VALUE}"; + runTest(); + + // Error + source = "google.protobuf.Value{null_value: null}"; + runTest(); + + // Ok + source = "cel.expr.conformance.proto3.TestAllTypes{single_value: null}"; + runTest(); + + // Ok but not expected (int coerced to double/json number 0.0) + source = + "cel.expr.conformance.proto3.TestAllTypes{single_value:" + + " google.protobuf.NullValue.NULL_VALUE}"; + runTest(); + + // Error + source = "cel.expr.conformance.proto3.TestAllTypes{null_value: null}"; + runTest(); + + // Ok + source = + "cel.expr.conformance.proto3.TestAllTypes{null_value:" + + " google.protobuf.NullValue.NULL_VALUE}"; + runTest(); + } + + @Test + public void jsonTypeNullAccess() throws Exception { + source = "google.protobuf.Value{null_value: google.protobuf.NullValue.NULL_VALUE} == null"; + runTest(); + + source = "cel.expr.conformance.proto3.TestAllTypes{single_value: null}.single_value == null"; + runTest(); + + source = + "cel.expr.conformance.proto3.TestAllTypes{single_value:" + + " google.protobuf.NullValue.NULL_VALUE}.single_value == null"; + runTest(); + + // Error + source = + "cel.expr.conformance.proto3.TestAllTypes{null_value:" + + " google.protobuf.NullValue.NULL_VALUE}.null_value == null"; + runTest(); + + // Ok + source = + "cel.expr.conformance.proto3.TestAllTypes{null_value:" + + " google.protobuf.NullValue.NULL_VALUE}.null_value == 0"; + runTest(); + + // Error + source = "google.protobuf.NullValue.NULL_VALUE == null"; + runTest(); + + // Ok + source = "google.protobuf.NullValue.NULL_VALUE == 0"; + runTest(); + } + // Call Style and User Functions // ============================= diff --git a/checker/src/test/resources/jsonTypeNullAccess.baseline b/checker/src/test/resources/jsonTypeNullAccess.baseline new file mode 100644 index 000000000..834b8fde8 --- /dev/null +++ b/checker/src/test/resources/jsonTypeNullAccess.baseline @@ -0,0 +1,54 @@ +Source: google.protobuf.Value{null_value: google.protobuf.NullValue.NULL_VALUE} == null +=====> +_==_( + google.protobuf.Value{ + null_value:google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE + }~dyn^google.protobuf.Value, + null~null +)~bool^equals + +Source: cel.expr.conformance.proto3.TestAllTypes{single_value: null}.single_value == null +=====> +_==_( + cel.expr.conformance.proto3.TestAllTypes{ + single_value:null~null + }~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes.single_value~dyn, + null~null +)~bool^equals + +Source: cel.expr.conformance.proto3.TestAllTypes{single_value: google.protobuf.NullValue.NULL_VALUE}.single_value == null +=====> +_==_( + cel.expr.conformance.proto3.TestAllTypes{ + single_value:google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE + }~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes.single_value~dyn, + null~null +)~bool^equals + +Source: cel.expr.conformance.proto3.TestAllTypes{null_value: google.protobuf.NullValue.NULL_VALUE}.null_value == null +=====> +ERROR: test_location:1:103: found no matching overload for '_==_' applied to '(int, null)' (candidates: (%A0, %A0)) + | cel.expr.conformance.proto3.TestAllTypes{null_value: google.protobuf.NullValue.NULL_VALUE}.null_value == null + | ......................................................................................................^ + +Source: cel.expr.conformance.proto3.TestAllTypes{null_value: google.protobuf.NullValue.NULL_VALUE}.null_value == 0 +=====> +_==_( + cel.expr.conformance.proto3.TestAllTypes{ + null_value:google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE + }~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes.null_value~int, + 0~int +)~bool^equals + +Source: google.protobuf.NullValue.NULL_VALUE == null +=====> +ERROR: test_location:1:38: found no matching overload for '_==_' applied to '(int, null)' (candidates: (%A0, %A0)) + | google.protobuf.NullValue.NULL_VALUE == null + | .....................................^ + +Source: google.protobuf.NullValue.NULL_VALUE == 0 +=====> +_==_( + google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE, + 0~int +)~bool^equals \ No newline at end of file diff --git a/checker/src/test/resources/jsonTypeNullConstruction.baseline b/checker/src/test/resources/jsonTypeNullConstruction.baseline new file mode 100644 index 000000000..5b9b211a8 --- /dev/null +++ b/checker/src/test/resources/jsonTypeNullConstruction.baseline @@ -0,0 +1,35 @@ +Source: google.protobuf.Value{null_value: google.protobuf.NullValue.NULL_VALUE} +=====> +google.protobuf.Value{ + null_value:google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE +}~dyn^google.protobuf.Value + +Source: google.protobuf.Value{null_value: null} +=====> +ERROR: test_location:1:33: expected type of field 'null_value' is 'int' but provided type is 'null' + | google.protobuf.Value{null_value: null} + | ................................^ + +Source: cel.expr.conformance.proto3.TestAllTypes{single_value: null} +=====> +cel.expr.conformance.proto3.TestAllTypes{ + single_value:null~null +}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes + +Source: cel.expr.conformance.proto3.TestAllTypes{single_value: google.protobuf.NullValue.NULL_VALUE} +=====> +cel.expr.conformance.proto3.TestAllTypes{ + single_value:google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE +}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes + +Source: cel.expr.conformance.proto3.TestAllTypes{null_value: null} +=====> +ERROR: test_location:1:52: expected type of field 'null_value' is 'int' but provided type is 'null' + | cel.expr.conformance.proto3.TestAllTypes{null_value: null} + | ...................................................^ + +Source: cel.expr.conformance.proto3.TestAllTypes{null_value: google.protobuf.NullValue.NULL_VALUE} +=====> +cel.expr.conformance.proto3.TestAllTypes{ + null_value:google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE +}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes \ No newline at end of file diff --git a/runtime/src/test/resources/jsonValueTypes.baseline b/runtime/src/test/resources/jsonValueTypes.baseline index ff406cf94..cc840b24b 100644 --- a/runtime/src/test/resources/jsonValueTypes.baseline +++ b/runtime/src/test/resources/jsonValueTypes.baseline @@ -53,6 +53,46 @@ bindings: {x=single_value { } result: true +Source: google.protobuf.Value{string_value: 'hello'} == 'hello' +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {} +result: true + +Source: google.protobuf.Value{number_value: 1.1} == 1.1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {} +result: true + +Source: google.protobuf.Value{null_value: google.protobuf.NullValue.NULL_VALUE} == null +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {} +result: true + +Source: TestAllTypes{null_value: google.protobuf.NullValue.NULL_VALUE}.null_value == 0 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {} +result: true + +Source: TestAllTypes{null_value: google.protobuf.NullValue.NULL_VALUE}.null_value != dyn(null) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {} +result: true + Source: x.single_value[0] == [['hello'], -1.1][0] declare x { value cel.expr.conformance.proto3.TestAllTypes @@ -148,5 +188,4 @@ bindings: {x=single_struct { } } } -result: {hello=val} - +result: {hello=val} \ No newline at end of file diff --git a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java index 144ada5a8..b3c9af423 100644 --- a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java +++ b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java @@ -1850,6 +1850,23 @@ public void jsonValueTypes() { source = "x.single_value == 'hello'"; runTest(ImmutableMap.of("x", xString)); + // json manual construction + source = "google.protobuf.Value{string_value: 'hello'} == 'hello'"; + runTest(); + + source = "google.protobuf.Value{number_value: 1.1} == 1.1"; + runTest(); + + source = "google.protobuf.Value{null_value: google.protobuf.NullValue.NULL_VALUE} == null"; + runTest(); + + // NULL_VALUE is not the same as null. + source = "TestAllTypes{null_value: google.protobuf.NullValue.NULL_VALUE}.null_value == 0"; + runTest(); + source = + "TestAllTypes{null_value: google.protobuf.NullValue.NULL_VALUE}.null_value != dyn(null)"; + runTest(); + // JSON list equality. TestAllTypes xList = TestAllTypes.newBuilder() From 62c1f111d0b9d02f44185dd9a720bab768528916 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 12 Mar 2026 11:08:12 -0700 Subject: [PATCH 015/204] Null assignability fix for repeated and map fields PiperOrigin-RevId: 882683464 --- .../dev/cel/common/internal/ProtoAdapter.java | 51 +++++++++++++++++-- .../test/resources/nullAssignability.baseline | 29 +++++++++++ .../dev/cel/testing/BaseInterpreterTest.java | 27 ++++++++++ 3 files changed, 102 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java b/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java index b6648a5b8..7e3910433 100644 --- a/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java +++ b/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java @@ -222,9 +222,7 @@ public Optional adaptValueToFieldType( throw new IllegalArgumentException("Unsupported field type"); } - String typeFullName = fieldDescriptor.getMessageType().getFullName(); - if (!WellKnownProto.ANY_VALUE.typeName().equals(typeFullName) - && !WellKnownProto.JSON_VALUE.typeName().equals(typeFullName)) { + if (!isFieldAnyOrJson(fieldDescriptor)) { return Optional.empty(); } } @@ -242,7 +240,11 @@ public Optional adaptValueToFieldType( getDefaultValueForMaybeMessage(keyDescriptor), valueDescriptor.getLiteType(), getDefaultValueForMaybeMessage(valueDescriptor)); + boolean isValueAnyOrJson = isFieldAnyOrJson(valueDescriptor); for (Map.Entry entry : ((Map) fieldValue).entrySet()) { + if (!isValueAnyOrJson && entry.getValue() instanceof NullValue) { + continue; + } mapEntries.add( protoMapEntry.toBuilder() .setKey(keyConverter.backwardConverter().convert(entry.getKey())) @@ -252,15 +254,54 @@ public Optional adaptValueToFieldType( return Optional.of(mapEntries); } if (fieldDescriptor.isRepeated()) { + List listValue = (List) fieldValue; + + if (!isFieldAnyOrJson(fieldDescriptor)) { + listValue = filterOutNullValues(listValue); + } + return Optional.of( - AdaptingTypes.adaptingList( - (List) fieldValue, fieldToValueConverter(fieldDescriptor).reverse())); + AdaptingTypes.adaptingList(listValue, fieldToValueConverter(fieldDescriptor).reverse())); } return Optional.of( fieldToValueConverter(fieldDescriptor).backwardConverter().convert(fieldValue)); } + private static List filterOutNullValues(List originalList) { + List filteredList = null; + + for (int i = 0; i < originalList.size(); i++) { + Object elem = originalList.get(i); + + if (elem instanceof NullValue) { + if (filteredList == null) { + filteredList = new ArrayList<>(originalList.size() - 1); + if (i > 0) { + filteredList.addAll(originalList.subList(0, i)); + } + } + } else if (filteredList != null) { + filteredList.add(elem); + } + } + + // Return the original list if no nulls were found to avoid unnecessary allocations + return filteredList != null ? filteredList : originalList; + } + + private static boolean isFieldAnyOrJson(FieldDescriptor fieldDescriptor) { + if (!fieldDescriptor.getType().equals(FieldDescriptor.Type.MESSAGE)) { + return false; + } + + String typeFullName = fieldDescriptor.getMessageType().getFullName(); + + return WellKnownProto.getByTypeName(typeFullName) + .map(wkp -> wkp.equals(WellKnownProto.ANY_VALUE) || wkp.equals(WellKnownProto.JSON_VALUE)) + .orElse(false); + } + @SuppressWarnings("rawtypes") private BidiConverter fieldToValueConverter(FieldDescriptor fieldDescriptor) { switch (fieldDescriptor.getType()) { diff --git a/runtime/src/test/resources/nullAssignability.baseline b/runtime/src/test/resources/nullAssignability.baseline index 47b9c7a0d..b60f434ea 100644 --- a/runtime/src/test/resources/nullAssignability.baseline +++ b/runtime/src/test/resources/nullAssignability.baseline @@ -33,3 +33,32 @@ Source: has(TestAllTypes{single_timestamp: null}.single_timestamp) bindings: {} result: false +Source: TestAllTypes{repeated_timestamp: [timestamp(1), null]}.repeated_timestamp == [timestamp(1)] +=====> +bindings: {} +result: true + +Source: TestAllTypes{map_bool_timestamp: {true: null, false: timestamp(1)}}.map_bool_timestamp == {false: timestamp(1)} +=====> +bindings: {} +result: true + +Source: TestAllTypes{repeated_any: [1, null]}.repeated_any == [1, null] +=====> +bindings: {} +result: true + +Source: TestAllTypes{map_bool_any: {true: null, false: 1}}.map_bool_any == {true: null, false: 1} +=====> +bindings: {} +result: true + +Source: TestAllTypes{repeated_value: [google.protobuf.Value{bool_value: true}, null]}.repeated_value == [true, null] +=====> +bindings: {} +result: true + +Source: TestAllTypes{map_bool_value: {true: null, false: google.protobuf.Value{bool_value: true}}}.map_bool_value == {true: null, false: true} +=====> +bindings: {} +result: true \ No newline at end of file diff --git a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java index b3c9af423..f3c1cf398 100644 --- a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java +++ b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java @@ -2162,6 +2162,33 @@ public void nullAssignability() throws Exception { source = "has(TestAllTypes{single_timestamp: null}.single_timestamp)"; runTest(); + + source = + "TestAllTypes{repeated_timestamp: [timestamp(1), null]}.repeated_timestamp ==" + + " [timestamp(1)]"; + runTest(); + + source = + "TestAllTypes{map_bool_timestamp: {true: null, false: timestamp(1)}}.map_bool_timestamp ==" + + " {false: timestamp(1)}"; + runTest(); + + source = "TestAllTypes{repeated_any: [1, null]}.repeated_any == [1, null]"; + runTest(); + + source = + "TestAllTypes{map_bool_any: {true: null, false: 1}}.map_bool_any == {true: null, false: 1}"; + runTest(); + + source = + "TestAllTypes{repeated_value: [google.protobuf.Value{bool_value: true}," + + " null]}.repeated_value == [true, null]"; + runTest(); + + source = + "TestAllTypes{map_bool_value: {true: null, false: google.protobuf.Value{bool_value:" + + " true}}}.map_bool_value == {true: null, false: true}"; + runTest(); } @Test From 86ddee361843e561bc66e28a4beae742be42d490 Mon Sep 17 00:00:00 2001 From: Salman Muin Kayser Chishti <13schishti@gmail.com> Date: Fri, 13 Mar 2026 07:54:27 +0000 Subject: [PATCH 016/204] Upgrade GitHub Actions to latest versions Signed-off-by: Salman Muin Kayser Chishti <13schishti@gmail.com> --- .github/workflows/workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 4b206e3ec..b172788c3 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -82,7 +82,7 @@ jobs: uses: actions/checkout@v6 - name: Get changed files id: changed_file - uses: tj-actions/changed-files@v46 + uses: tj-actions/changed-files@v47 with: files: publish/cel_version.bzl - name: Setup Bazel From 4f6a571bfed4efa533d7ee6c3e02fb43fb4900cf Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 17 Mar 2026 14:39:44 -0700 Subject: [PATCH 017/204] Support partial evaluation via unknowns in planner PiperOrigin-RevId: 885217146 --- .../test/java/dev/cel/extensions/BUILD.bazel | 2 + .../extensions/CelOptionalLibraryTest.java | 49 +- runtime/BUILD.bazel | 14 + .../dev/cel/runtime/AccumulatedUnknowns.java | 25 +- .../src/main/java/dev/cel/runtime/BUILD.bazel | 45 +- .../java/dev/cel/runtime/CelRuntimeImpl.java | 5 + .../java/dev/cel/runtime/CelUnknownSet.java | 2 +- .../java/dev/cel/runtime/InterpreterUtil.java | 17 +- .../java/dev/cel/runtime/LiteProgramImpl.java | 6 + .../java/dev/cel/runtime/PartialVars.java | 70 +++ .../main/java/dev/cel/runtime/Program.java | 3 + .../java/dev/cel/runtime/ProgramImpl.java | 8 + .../dev/cel/runtime/planner/Attribute.java | 2 +- .../java/dev/cel/runtime/planner/BUILD.bazel | 22 +- .../java/dev/cel/runtime/planner/EvalAnd.java | 139 +++--- .../cel/runtime/planner/EvalAttribute.java | 4 +- .../cel/runtime/planner/EvalConditional.java | 5 +- .../cel/runtime/planner/EvalCreateList.java | 12 + .../cel/runtime/planner/EvalCreateMap.java | 62 ++- .../cel/runtime/planner/EvalCreateStruct.java | 22 +- .../dev/cel/runtime/planner/EvalFold.java | 4 + .../dev/cel/runtime/planner/EvalHelpers.java | 156 +++--- .../runtime/planner/EvalLateBoundCall.java | 8 + .../cel/runtime/planner/EvalOptionalOr.java | 5 + .../runtime/planner/EvalOptionalOrValue.java | 5 + .../planner/EvalOptionalSelectField.java | 9 + .../java/dev/cel/runtime/planner/EvalOr.java | 139 +++--- .../cel/runtime/planner/EvalVarArgsCall.java | 8 + .../cel/runtime/planner/ExecutionFrame.java | 15 +- .../cel/runtime/planner/MaybeAttribute.java | 4 +- .../cel/runtime/planner/MissingAttribute.java | 2 +- .../runtime/planner/NamespacedAttribute.java | 75 ++- .../cel/runtime/planner/PlannedProgram.java | 32 +- .../runtime/planner/RelativeAttribute.java | 8 +- .../src/test/java/dev/cel/runtime/BUILD.bazel | 9 + .../cel/runtime/PlannerInterpreterTest.java | 255 +++++++++- .../java/dev/cel/runtime/planner/BUILD.bazel | 2 + .../runtime/planner/ProgramPlannerTest.java | 33 ++ .../planner_unknownFieldSelection.baseline | 111 +++++ .../planner_unknownResultSet_errors.baseline | 81 +++ .../planner_unknownResultSet_success.baseline | 461 ++++++++++++++++++ .../src/test/resources/unknownField.baseline | 2 +- .../src/main/java/dev/cel/testing/BUILD.bazel | 2 +- .../dev/cel/testing/BaseInterpreterTest.java | 46 +- 44 files changed, 1665 insertions(+), 321 deletions(-) create mode 100644 runtime/src/main/java/dev/cel/runtime/PartialVars.java create mode 100644 runtime/src/test/resources/planner_unknownFieldSelection.baseline create mode 100644 runtime/src/test/resources/planner_unknownResultSet_errors.baseline create mode 100644 runtime/src/test/resources/planner_unknownResultSet_success.baseline diff --git a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel index 48915fd02..a9dbfaca2 100644 --- a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel @@ -38,6 +38,8 @@ java_library( "//runtime:interpreter_util", "//runtime:lite_runtime", "//runtime:lite_runtime_factory", + "//runtime:partial_vars", + "//runtime:unknown_attributes", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/test:simple_java_proto", diff --git a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java index 24e9d6d86..ab412fb39 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java @@ -49,10 +49,12 @@ import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import dev.cel.parser.CelMacro; import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelRuntime; import dev.cel.runtime.InterpreterUtil; +import dev.cel.runtime.PartialVars; import java.time.Duration; import java.time.Instant; import java.util.List; @@ -897,14 +899,12 @@ public void optionalIndex_onMap_returnsOptionalValue() throws Exception { @TestParameters("{source: '{?x: x}'}") public void optionalIndex_onMapWithUnknownInput_returnsUnknownResult(String source) throws Exception { - if (testMode.equals(TestMode.PLANNER_CHECKED) || testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { - // TODO: Uncomment once unknowns is implemented - return; - } Cel cel = newCelBuilder().addVar("x", OptionalType.create(SimpleType.INT)).build(); CelAbstractSyntaxTree ast = compile(cel, source); - Object result = cel.createProgram(ast).eval(); + Object result = + cel.createProgram(ast) + .eval(PartialVars.of(CelAttributePattern.fromQualifiedIdentifier("x"))); assertThat(InterpreterUtil.isUnknown(result)).isTrue(); } @@ -987,10 +987,6 @@ public void optionalIndex_onOptionalList_returnsOptionalValue() throws Exception @Test public void optionalIndex_onListWithUnknownInput_returnsUnknownResult() throws Exception { - if (testMode.equals(TestMode.PLANNER_CHECKED) || testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { - // TODO: Uncomment once unknowns is implemented - return; - } Cel cel = newCelBuilder() .addVar("x", OptionalType.create(SimpleType.INT)) @@ -998,7 +994,9 @@ public void optionalIndex_onListWithUnknownInput_returnsUnknownResult() throws E .build(); CelAbstractSyntaxTree ast = compile(cel, "[?x]"); - Object result = cel.createProgram(ast).eval(); + Object result = + cel.createProgram(ast) + .eval(PartialVars.of(CelAttributePattern.fromQualifiedIdentifier("x"))); assertThat(InterpreterUtil.isUnknown(result)).isTrue(); } @@ -1017,6 +1015,29 @@ public void traditionalIndex_onOptionalList_returnsOptionalEmpty() throws Except assertThat(result).isEqualTo(Optional.empty()); } + @Test + public void optionalFieldSelect_fieldMarkedUnknown_returnsUnknownSet() throws Exception { + if (testMode.equals(TestMode.LEGACY_CHECKED)) { + // This case is not possible to setup for legacy runtime + return; + } + + Cel cel = + newCelBuilder() + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "msg.?single_int32"); + + Object result = + cel.createProgram(ast) + .eval( + PartialVars.of( + ImmutableMap.of("msg", TestAllTypes.newBuilder().setSingleInt32(42).build()), + CelAttributePattern.fromQualifiedIdentifier("msg.single_int32"))); + + assertThat(InterpreterUtil.isUnknown(result)).isTrue(); + } + @Test // LHS @TestParameters("{expression: 'optx.or(optional.of(1))'}") @@ -1026,10 +1047,6 @@ public void traditionalIndex_onOptionalList_returnsOptionalEmpty() throws Except @TestParameters("{expression: 'optional.none().orValue(optx)'}") public void optionalChainedFunctions_lhsIsUnknown_returnsUnknown(String expression) throws Exception { - if (testMode.equals(TestMode.PLANNER_CHECKED) || testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { - // TODO: Uncomment once unknowns is implemented - return; - } Cel cel = newCelBuilder() .addVar("optx", OptionalType.create(SimpleType.INT)) @@ -1037,7 +1054,9 @@ public void optionalChainedFunctions_lhsIsUnknown_returnsUnknown(String expressi .build(); CelAbstractSyntaxTree ast = compile(cel, expression); - Object result = cel.createProgram(ast).eval(); + Object result = + cel.createProgram(ast) + .eval(PartialVars.of(CelAttributePattern.fromQualifiedIdentifier("optx"))); assertThat(InterpreterUtil.isUnknown(result)).isTrue(); } diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index 55ee241a0..3e183d236 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -351,3 +351,17 @@ java_library( "//runtime/src/main/java/dev/cel/runtime:runtime_planner_impl", ], ) + +java_library( + name = "accumulated_unknowns", + visibility = ["//:internal"], + exports = [ + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns", + ], +) + +java_library( + name = "partial_vars", + visibility = ["//:internal"], + exports = ["//runtime/src/main/java/dev/cel/runtime:partial_vars"], +) diff --git a/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java b/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java index d27de2da2..d4d54c71f 100644 --- a/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java +++ b/runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java @@ -15,18 +15,23 @@ package dev.cel.runtime; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import dev.cel.common.annotations.Internal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; /** * An internal representation used for fast accumulation of unknown expr IDs and attributes. For * safety, this object should never be returned as an evaluated result and instead be adapted into * an immutable CelUnknownSet. + * + *

CEL Library Internals. Do Not Use. */ -final class AccumulatedUnknowns { +@Internal +public final class AccumulatedUnknowns { private static final int MAX_UNKNOWN_ATTRIBUTE_SIZE = 500_000; private final Set exprIds; private final Set attributes; @@ -39,8 +44,21 @@ Set attributes() { return attributes; } + /** + * Evaluates if the right hand side is an accumulated unknown, and if so, merges it into the + * accumulator. + */ + public static @Nullable AccumulatedUnknowns maybeMerge( + @Nullable AccumulatedUnknowns accumulator, Object newValue) { + if (newValue instanceof AccumulatedUnknowns) { + AccumulatedUnknowns newUnknowns = (AccumulatedUnknowns) newValue; + return accumulator == null ? newUnknowns : accumulator.merge(newUnknowns); + } + return accumulator; + } + @CanIgnoreReturnValue - AccumulatedUnknowns merge(AccumulatedUnknowns arg) { + public AccumulatedUnknowns merge(AccumulatedUnknowns arg) { enforceMaxAttributeSize(this.attributes, arg.attributes); this.exprIds.addAll(arg.exprIds); this.attributes.addAll(arg.attributes); @@ -55,7 +73,8 @@ static AccumulatedUnknowns create(Collection ids) { return create(ids, new ArrayList<>()); } - static AccumulatedUnknowns create(Collection exprIds, Collection attributes) { + public static AccumulatedUnknowns create( + Collection exprIds, Collection attributes) { return new AccumulatedUnknowns(new HashSet<>(exprIds), new HashSet<>(attributes)); } diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 10dca9ece..2681c17de 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -826,6 +826,7 @@ java_library( ":evaluation_listener", ":function_binding", ":function_resolver", + ":partial_vars", ":program", ":proto_message_runtime_equality", ":runtime", @@ -938,6 +939,7 @@ java_library( ":function_resolver", ":interpretable", ":interpreter", + ":partial_vars", ":program", ":proto_message_activation_factory", ":runtime_equality", @@ -955,7 +957,6 @@ java_library( "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", - "@maven//:org_jspecify_jspecify", ], ) @@ -1014,6 +1015,7 @@ java_library( ":evaluation_exception", ":function_resolver", ":interpretable", + ":partial_vars", ":program", ":variable_resolver", "//:auto_value", @@ -1029,6 +1031,7 @@ cel_android_library( ":evaluation_exception", ":function_resolver_android", ":interpretable_android", + ":partial_vars_android", ":program_android", ":variable_resolver", "//:auto_value", @@ -1199,6 +1202,7 @@ java_library( ":unknown_attributes", "//common/annotations", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", "@maven//:org_jspecify_jspecify", ], ) @@ -1214,6 +1218,7 @@ cel_android_library( "//common/annotations", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", ], ) @@ -1273,10 +1278,13 @@ java_library( java_library( name = "accumulated_unknowns", srcs = ["AccumulatedUnknowns.java"], - visibility = ["//visibility:private"], + tags = [ + ], deps = [ ":unknown_attributes", + "//common/annotations", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", ], ) @@ -1286,7 +1294,9 @@ cel_android_library( visibility = ["//visibility:private"], deps = [ ":unknown_attributes_android", + "//common/annotations", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", ], ) @@ -1318,6 +1328,34 @@ cel_android_library( ], ) +java_library( + name = "partial_vars", + srcs = ["PartialVars.java"], + tags = [ + ], + deps = [ + ":variable_resolver", + "//:auto_value", + "//runtime:unknown_attributes", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "partial_vars_android", + srcs = ["PartialVars.java"], + tags = [ + ], + deps = [ + ":variable_resolver", + "//:auto_value", + "//runtime:unknown_attributes_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "program", srcs = ["Program.java"], @@ -1326,6 +1364,7 @@ java_library( deps = [ ":evaluation_exception", ":function_resolver", + ":partial_vars", ":variable_resolver", "@maven//:com_google_errorprone_error_prone_annotations", ], @@ -1339,8 +1378,8 @@ cel_android_library( deps = [ ":evaluation_exception", ":function_resolver_android", + ":partial_vars_android", ":variable_resolver", - "//:auto_value", "@maven//:com_google_errorprone_error_prone_annotations", ], ) diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index 346b25ae9..cab2c666e 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -134,6 +134,11 @@ public Object eval( return program.eval(resolver, lateBoundFunctionResolver); } + @Override + public Object eval(PartialVars partialVars) throws CelEvaluationException { + return program.eval(partialVars); + } + @Override public Object trace(CelEvaluationListener listener) throws CelEvaluationException { throw new UnsupportedOperationException("Trace is not yet supported."); diff --git a/runtime/src/main/java/dev/cel/runtime/CelUnknownSet.java b/runtime/src/main/java/dev/cel/runtime/CelUnknownSet.java index c7f1d0c91..62d975f93 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelUnknownSet.java +++ b/runtime/src/main/java/dev/cel/runtime/CelUnknownSet.java @@ -59,7 +59,7 @@ static CelUnknownSet create(Iterable unknownExprIds) { return create(ImmutableSet.of(), ImmutableSet.copyOf(unknownExprIds)); } - static CelUnknownSet create( + public static CelUnknownSet create( ImmutableSet attributes, ImmutableSet unknownExprIds) { return new AutoValue_CelUnknownSet(attributes, unknownExprIds); } diff --git a/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java b/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java index f84897ac2..73607cefd 100644 --- a/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java +++ b/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java @@ -14,6 +14,7 @@ package dev.cel.runtime; +import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CheckReturnValue; import dev.cel.common.annotations.Internal; import org.jspecify.annotations.Nullable; @@ -55,12 +56,12 @@ public static boolean isUnknown(Object obj) { return obj instanceof CelUnknownSet; } - static boolean isAccumulatedUnknowns(Object obj) { + public static boolean isAccumulatedUnknowns(Object obj) { return obj instanceof AccumulatedUnknowns; } /** If the argument is {@link CelUnknownSet}, adapts it into {@link AccumulatedUnknowns} */ - static Object maybeAdaptToAccumulatedUnknowns(Object val) { + public static Object maybeAdaptToAccumulatedUnknowns(Object val) { if (!(val instanceof CelUnknownSet)) { return val; } @@ -68,10 +69,20 @@ static Object maybeAdaptToAccumulatedUnknowns(Object val) { return adaptToAccumulatedUnknowns((CelUnknownSet) val); } - static AccumulatedUnknowns adaptToAccumulatedUnknowns(CelUnknownSet unknowns) { + public static AccumulatedUnknowns adaptToAccumulatedUnknowns(CelUnknownSet unknowns) { return AccumulatedUnknowns.create(unknowns.unknownExprIds(), unknowns.attributes()); } + public static Object maybeAdaptToCelUnknownSet(Object val) { + if (!(val instanceof AccumulatedUnknowns)) { + return val; + } + + AccumulatedUnknowns unknowns = (AccumulatedUnknowns) val; + return CelUnknownSet.create( + ImmutableSet.copyOf(unknowns.attributes()), ImmutableSet.copyOf(unknowns.exprIds())); + } + /** * Enforces strictness on both lhs/rhs arguments from logical operators (i.e: intentionally throws * an appropriate exception when {@link Throwable} is encountered as part of evaluated result. diff --git a/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java b/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java index 5e57f497b..af8c1a6d0 100644 --- a/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java @@ -52,6 +52,12 @@ public Object eval(CelVariableResolver resolver) throws CelEvaluationException { throw new UnsupportedOperationException("To be implemented"); } + @Override + public Object eval(PartialVars partialVars) throws CelEvaluationException { + // TODO: Wire in program planner + throw new UnsupportedOperationException("To be implemented"); + } + static Program plan(Interpretable interpretable) { return new AutoValue_LiteProgramImpl(interpretable); } diff --git a/runtime/src/main/java/dev/cel/runtime/PartialVars.java b/runtime/src/main/java/dev/cel/runtime/PartialVars.java new file mode 100644 index 000000000..1cd081040 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/PartialVars.java @@ -0,0 +1,70 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import java.util.Map; +import java.util.Optional; + +/** + * A holder for a {@link CelVariableResolver} and a set of {@link CelAttributePattern}s that + * indicate variables or parts of variables whose value are not yet known. + */ +@AutoValue +public abstract class PartialVars { + + /** The resolver to use for resolving evaluation variables. */ + public abstract CelVariableResolver resolver(); + + /** + * A list of attribute patterns specifying which missing attribute paths should be tracked as + * unknown values. + */ + public abstract ImmutableList unknowns(); + + /** Constructs a new {@code PartialVars} from one or more {@link CelAttributePattern}s. */ + public static PartialVars of(CelAttributePattern... unknownAttributes) { + return of((unused) -> Optional.empty(), ImmutableList.copyOf(unknownAttributes)); + } + + /** + * Constructs a new {@code PartialVars} from a {@link CelVariableResolver} and a list of {@link + * CelAttributePattern}s. + */ + public static PartialVars of( + CelVariableResolver resolver, Iterable unknownAttributes) { + return new AutoValue_PartialVars(resolver, ImmutableList.copyOf(unknownAttributes)); + } + + /** + * Constructs a new {@code PartialVars} from a map of variables and an array of {@link + * CelAttributePattern}s. + */ + public static PartialVars of(Map variables, CelAttributePattern... unknownAttributes) { + return of( + (name) -> variables.containsKey(name) ? Optional.of(variables.get(name)) : Optional.empty(), + unknownAttributes); + } + + /** + * Constructs a new {@code PartialVars} from a {@link CelVariableResolver} and an array of {@link + * CelAttributePattern}s. + */ + public static PartialVars of( + CelVariableResolver resolver, CelAttributePattern... unknownAttributes) { + return of(resolver, ImmutableList.copyOf(unknownAttributes)); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/Program.java b/runtime/src/main/java/dev/cel/runtime/Program.java index c0982f1f8..e808a373c 100644 --- a/runtime/src/main/java/dev/cel/runtime/Program.java +++ b/runtime/src/main/java/dev/cel/runtime/Program.java @@ -43,4 +43,7 @@ Object eval(Map mapValue, CelFunctionResolver lateBoundFunctionResolv */ Object eval(CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver) throws CelEvaluationException; + + /** Evaluate a compiled program with unknown attribute patterns {@code partialVars}. */ + Object eval(PartialVars partialVars) throws CelEvaluationException; } diff --git a/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java b/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java index d0e64429b..c9f4d083b 100644 --- a/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java @@ -60,6 +60,14 @@ public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctio return evalInternal(Activation.copyOf(mapValue), lateBoundFunctionResolver); } + @Override + public Object eval(PartialVars partialVars) throws CelEvaluationException { + return evalInternal( + UnknownContext.create(partialVars.resolver(), partialVars.unknowns()), + /* lateBoundFunctionResolver= */ Optional.empty(), + /* listener= */ Optional.empty()); + } + @Override public Object trace(CelEvaluationListener listener) throws CelEvaluationException { return evalInternal(Activation.EMPTY, listener); diff --git a/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java b/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java index cc011ed34..90165c1ac 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/Attribute.java @@ -20,7 +20,7 @@ /** Represents a resolvable symbol or path (such as a variable or a field selection). */ @Immutable interface Attribute { - Object resolve(GlobalResolver ctx, ExecutionFrame frame); + Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame); Attribute addQualifier(Qualifier qualifier); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index 6561e4e5c..3c18b192f 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -78,6 +78,8 @@ java_library( "//runtime:evaluation_exception_builder", "//runtime:function_resolver", "//runtime:interpretable", + "//runtime:interpreter_util", + "//runtime:partial_vars", "//runtime:program", "//runtime:resolved_overload", "//runtime:variable_resolver", @@ -128,7 +130,11 @@ java_library( "//common/types", "//common/types:type_providers", "//common/values", + "//runtime:accumulated_unknowns", "//runtime:interpretable", + "//runtime:interpreter_util", + "//runtime:partial_vars", + "//runtime:unknown_attributes", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:org_jspecify_jspecify", @@ -181,7 +187,6 @@ java_library( ":qualifier", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", ], ) @@ -235,6 +240,7 @@ java_library( ":execution_frame", ":planned_interpretable", "//common/values", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", "//runtime:resolved_overload", @@ -250,6 +256,7 @@ java_library( ":planned_interpretable", "//common/exceptions:overload_not_found", "//common/values", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", "//runtime:resolved_overload", @@ -265,6 +272,7 @@ java_library( ":execution_frame", ":planned_interpretable", "//common/values", + "//runtime:accumulated_unknowns", "//runtime:interpretable", "@maven//:com_google_guava_guava", ], @@ -278,6 +286,7 @@ java_library( ":execution_frame", ":planned_interpretable", "//common/values", + "//runtime:accumulated_unknowns", "//runtime:interpretable", "@maven//:com_google_guava_guava", ], @@ -289,6 +298,7 @@ java_library( deps = [ ":execution_frame", ":planned_interpretable", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", "@maven//:com_google_guava_guava", @@ -299,11 +309,13 @@ java_library( name = "eval_create_struct", srcs = ["EvalCreateStruct.java"], deps = [ + ":eval_helpers", ":execution_frame", ":planned_interpretable", "//common/types:type_providers", "//common/values", "//common/values:cel_value_provider", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", @@ -318,6 +330,7 @@ java_library( ":eval_helpers", ":execution_frame", ":planned_interpretable", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", @@ -329,11 +342,13 @@ java_library( name = "eval_create_map", srcs = ["EvalCreateMap.java"], deps = [ + ":eval_helpers", ":execution_frame", ":localized_evaluation_exception", ":planned_interpretable", "//common/exceptions:duplicate_key", "//common/exceptions:invalid_argument", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", @@ -348,6 +363,7 @@ java_library( ":activation_wrapper", ":execution_frame", ":planned_interpretable", + "//runtime:accumulated_unknowns", "//runtime:concatenated_list_view", "//runtime:evaluation_exception", "//runtime:interpretable", @@ -365,6 +381,7 @@ java_library( "//common/exceptions:iteration_budget_exceeded", "//runtime:evaluation_exception", "//runtime:function_resolver", + "//runtime:partial_vars", "//runtime:resolved_overload", ], ) @@ -424,6 +441,7 @@ java_library( ":execution_frame", ":planned_interpretable", "//common/exceptions:overload_not_found", + "//runtime:accumulated_unknowns", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -438,6 +456,7 @@ java_library( ":execution_frame", ":planned_interpretable", "//common/exceptions:overload_not_found", + "//runtime:accumulated_unknowns", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -452,6 +471,7 @@ java_library( ":execution_frame", ":planned_interpretable", "//common/values", + "//runtime:accumulated_unknowns", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java index 763f8faba..eb7406071 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java @@ -1,66 +1,73 @@ -// Copyright 2025 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dev.cel.runtime.planner; - -import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; - -import com.google.common.base.Preconditions; -import dev.cel.common.values.ErrorValue; -import dev.cel.runtime.GlobalResolver; - -final class EvalAnd extends PlannedInterpretable { - - @SuppressWarnings("Immutable") - private final PlannedInterpretable[] args; - - @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) { - ErrorValue errorValue = null; - for (PlannedInterpretable arg : args) { - Object argVal = evalNonstrictly(arg, resolver, frame); - if (argVal instanceof Boolean) { - // Short-circuit on false - if (!((boolean) argVal)) { - return false; - } - } else if (argVal instanceof ErrorValue) { - errorValue = (ErrorValue) argVal; - } else { - // TODO: Handle unknowns - errorValue = - ErrorValue.create( - arg.exprId(), - new IllegalArgumentException( - String.format("Expected boolean value, found: %s", argVal))); - } - } - - if (errorValue != null) { - return errorValue; - } - - return true; - } - - static EvalAnd create(long exprId, PlannedInterpretable[] args) { - return new EvalAnd(exprId, args); - } - - private EvalAnd(long exprId, PlannedInterpretable[] args) { - super(exprId); - Preconditions.checkArgument(args.length == 2); - this.args = args; - } -} +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; + +import com.google.common.base.Preconditions; +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.GlobalResolver; + +final class EvalAnd extends PlannedInterpretable { + + @SuppressWarnings("Immutable") + private final PlannedInterpretable[] args; + + @Override + public Object eval(GlobalResolver resolver, ExecutionFrame frame) { + ErrorValue errorValue = null; + AccumulatedUnknowns unknowns = null; + for (PlannedInterpretable arg : args) { + Object argVal = evalNonstrictly(arg, resolver, frame); + if (argVal instanceof Boolean) { + // Short-circuit on false + if (!((boolean) argVal)) { + return false; + } + } else if (argVal instanceof ErrorValue) { + errorValue = (ErrorValue) argVal; + } else if (argVal instanceof AccumulatedUnknowns) { + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVal); + } else { + errorValue = + ErrorValue.create( + arg.exprId(), + new IllegalArgumentException( + String.format("Expected boolean value, found: %s", argVal))); + } + } + + if (unknowns != null) { + return unknowns; + } + + if (errorValue != null) { + return errorValue; + } + + return true; + } + + static EvalAnd create(long exprId, PlannedInterpretable[] args) { + return new EvalAnd(exprId, args); + } + + private EvalAnd(long exprId, PlannedInterpretable[] args) { + super(exprId); + Preconditions.checkArgument(args.length == 2); + this.args = args; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java index fdd7ad2a3..a0a95c47a 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java @@ -24,9 +24,9 @@ final class EvalAttribute extends InterpretableAttribute { @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) { - Object resolved = attr.resolve(resolver, frame); + Object resolved = attr.resolve(exprId(), resolver, frame); if (resolved instanceof MissingAttribute) { - ((MissingAttribute) resolved).resolve(resolver, frame); + ((MissingAttribute) resolved).resolve(exprId(), resolver, frame); } return resolved; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java index 74482d629..3be1f016a 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java @@ -15,6 +15,7 @@ package dev.cel.runtime.planner; import com.google.common.base.Preconditions; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.GlobalResolver; @@ -28,8 +29,10 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval PlannedInterpretable condition = args[0]; PlannedInterpretable truthy = args[1]; PlannedInterpretable falsy = args[2]; - // TODO: Handle unknowns Object condResult = condition.eval(resolver, frame); + if (condResult instanceof AccumulatedUnknowns) { + return condResult; + } if (!(condResult instanceof Boolean)) { throw new IllegalArgumentException( String.format("Expected boolean value, found :%s", condResult)); diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java index 773272ea3..bae1e9302 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java @@ -16,6 +16,7 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.GlobalResolver; import java.util.Optional; @@ -32,9 +33,15 @@ final class EvalCreateList extends PlannedInterpretable { @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(values.length); + AccumulatedUnknowns unknowns = null; for (int i = 0; i < values.length; i++) { Object element = EvalHelpers.evalStrictly(values[i], resolver, frame); + if (element instanceof AccumulatedUnknowns) { + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, element); + continue; + } + if (isOptional[i]) { if (!(element instanceof Optional)) { throw new IllegalArgumentException( @@ -51,6 +58,11 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval builder.add(element); } + + if (unknowns != null) { + return unknowns; + } + return builder.build(); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java index f6f73e842..1e1b831bb 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java @@ -21,6 +21,7 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.common.exceptions.CelDuplicateKeyException; import dev.cel.common.exceptions.CelInvalidArgumentException; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.GlobalResolver; import java.util.HashSet; @@ -46,38 +47,49 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(keys.length); HashSet keysSeen = Sets.newHashSetWithExpectedSize(keys.length); + AccumulatedUnknowns unknowns = null; for (int i = 0; i < keys.length; i++) { PlannedInterpretable keyInterpretable = keys[i]; Object key = keyInterpretable.eval(resolver, frame); - if (!(key instanceof String - || key instanceof Long - || key instanceof UnsignedLong - || key instanceof Boolean)) { - throw new LocalizedEvaluationException( - new CelInvalidArgumentException("Unsupported key type: " + key), - keyInterpretable.exprId()); - } - boolean isDuplicate = !keysSeen.add(key); - if (!isDuplicate) { - if (key instanceof Long) { - long longVal = (Long) key; - if (longVal >= 0) { - isDuplicate = keysSeen.contains(UnsignedLong.valueOf(longVal)); + if (key instanceof AccumulatedUnknowns) { + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, key); + } else { + if (!(key instanceof String + || key instanceof Long + || key instanceof UnsignedLong + || key instanceof Boolean)) { + throw new LocalizedEvaluationException( + new CelInvalidArgumentException("Unsupported key type: " + key), + keyInterpretable.exprId()); + } + + boolean isDuplicate = !keysSeen.add(key); + if (!isDuplicate) { + if (key instanceof Long) { + long longVal = (Long) key; + if (longVal >= 0) { + isDuplicate = keysSeen.contains(UnsignedLong.valueOf(longVal)); + } + } else if (key instanceof UnsignedLong) { + UnsignedLong ulongVal = (UnsignedLong) key; + isDuplicate = keysSeen.contains(ulongVal.longValue()); } - } else if (key instanceof UnsignedLong) { - UnsignedLong ulongVal = (UnsignedLong) key; - isDuplicate = keysSeen.contains(ulongVal.longValue()); } - } - if (isDuplicate) { - throw new LocalizedEvaluationException( - CelDuplicateKeyException.of(key), keyInterpretable.exprId()); + if (isDuplicate) { + throw new LocalizedEvaluationException( + CelDuplicateKeyException.of(key), keyInterpretable.exprId()); + } } - Object val = values[i].eval(resolver, frame); + Object val = EvalHelpers.evalStrictly(values[i], resolver, frame); + + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, val); + if (unknowns != null) { + continue; + } if (isOptional[i]) { if (!(val instanceof Optional)) { @@ -94,13 +106,15 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval continue; } val = opt.get(); - } else { - System.out.println(); } builder.put(key, val); } + if (unknowns != null) { + return unknowns; + } + return builder.buildOrThrow(); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java index 4edc87b79..cdeb0c574 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java @@ -14,14 +14,15 @@ package dev.cel.runtime.planner; +import com.google.common.collect.Maps; import com.google.errorprone.annotations.Immutable; import dev.cel.common.types.CelType; import dev.cel.common.values.CelValueProvider; import dev.cel.common.values.StructValue; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.GlobalResolver; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -45,17 +46,22 @@ final class EvalCreateStruct extends PlannedInterpretable { @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { - Map fieldValues = new HashMap<>(); + Map fieldValues = Maps.newHashMapWithExpectedSize(keys.length); + AccumulatedUnknowns unknowns = null; for (int i = 0; i < keys.length; i++) { - Object value = values[i].eval(resolver, frame); + Object value = EvalHelpers.evalStrictly(values[i], resolver, frame); + + if (value instanceof AccumulatedUnknowns) { + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, value); + continue; + } if (isOptional[i]) { if (!(value instanceof Optional)) { throw new IllegalArgumentException( String.format( - "Cannot initialize optional entry 'single_double_wrapper' from non-optional value" - + " %s", - value)); + "Cannot initialize optional entry '%s' from non-optional value" + " %s", + keys[i], value)); } Optional opt = (Optional) value; @@ -71,6 +77,10 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval fieldValues.put(keys[i], value); } + if (unknowns != null) { + return unknowns; + } + // Either a primitive (wrappers) or a struct is produced Object value = valueProvider diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java index 3545ee4f7..197db42ad 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java @@ -16,6 +16,7 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.ConcatenatedListView; import dev.cel.runtime.GlobalResolver; @@ -73,6 +74,9 @@ private EvalFold( @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { Object iterRangeRaw = iterRange.eval(resolver, frame); + if (iterRangeRaw instanceof AccumulatedUnknowns) { + return iterRangeRaw; + } Folder folder = new Folder(resolver, accuVar, iterVar, iterVar2); folder.accuVal = maybeWrapAccumulator(accuInit.eval(folder, frame)); diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java index 92d234acc..38b060b92 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java @@ -1,78 +1,78 @@ -// Copyright 2025 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dev.cel.runtime.planner; - -import com.google.common.base.Joiner; -import dev.cel.common.CelErrorCode; -import dev.cel.common.exceptions.CelRuntimeException; -import dev.cel.common.values.CelValueConverter; -import dev.cel.common.values.ErrorValue; -import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelResolvedOverload; -import dev.cel.runtime.GlobalResolver; - -final class EvalHelpers { - - static Object evalNonstrictly( - PlannedInterpretable interpretable, GlobalResolver resolver, ExecutionFrame frame) { - try { - return interpretable.eval(resolver, frame); - } catch (LocalizedEvaluationException e) { - // Intercept the localized exception to get a more specific expr ID for error reporting - // Example: foo [1] && strict_err [2] -> ID 2 is propagated. - return ErrorValue.create(e.exprId(), e); - } catch (Exception e) { - return ErrorValue.create(interpretable.exprId(), e); - } - } - - static Object evalStrictly( - PlannedInterpretable interpretable, GlobalResolver resolver, ExecutionFrame frame) { - try { - return interpretable.eval(resolver, frame); - } catch (LocalizedEvaluationException e) { - // Already localized - propagate as-is to preserve inner expression ID - throw e; - } catch (CelRuntimeException e) { - // Wrap with current interpretable's location - throw new LocalizedEvaluationException(e, interpretable.exprId()); - } catch (Exception e) { - // Wrap generic exceptions with location - throw new LocalizedEvaluationException( - e, CelErrorCode.INTERNAL_ERROR, interpretable.exprId()); - } - } - - static Object dispatch( - CelResolvedOverload overload, CelValueConverter valueConverter, Object[] args) - throws CelEvaluationException { - try { - Object result = overload.getDefinition().apply(args); - return valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result)); - } catch (CelRuntimeException e) { - // Function dispatch failure that's already been handled -- just propagate. - throw e; - } catch (RuntimeException e) { - // Unexpected function dispatch failure. - throw new IllegalArgumentException( - String.format( - "Function '%s' failed with arg(s) '%s'", - overload.getOverloadId(), Joiner.on(", ").join(args)), - e); - } - } - - private EvalHelpers() {} -} +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.common.base.Joiner; +import dev.cel.common.CelErrorCode; +import dev.cel.common.exceptions.CelRuntimeException; +import dev.cel.common.values.CelValueConverter; +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelResolvedOverload; +import dev.cel.runtime.GlobalResolver; + +final class EvalHelpers { + + static Object evalNonstrictly( + PlannedInterpretable interpretable, GlobalResolver resolver, ExecutionFrame frame) { + try { + return interpretable.eval(resolver, frame); + } catch (LocalizedEvaluationException e) { + // Intercept the localized exception to get a more specific expr ID for error reporting + // Example: foo [1] && strict_err [2] -> ID 2 is propagated. + return ErrorValue.create(e.exprId(), e); + } catch (Exception e) { + return ErrorValue.create(interpretable.exprId(), e); + } + } + + static Object evalStrictly( + PlannedInterpretable interpretable, GlobalResolver resolver, ExecutionFrame frame) { + try { + return interpretable.eval(resolver, frame); + } catch (LocalizedEvaluationException e) { + // Already localized - propagate as-is to preserve inner expression ID + throw e; + } catch (CelRuntimeException e) { + // Wrap with current interpretable's location + throw new LocalizedEvaluationException(e, interpretable.exprId()); + } catch (Exception e) { + // Wrap generic exceptions with location + throw new LocalizedEvaluationException( + e, CelErrorCode.INTERNAL_ERROR, interpretable.exprId()); + } + } + + static Object dispatch( + CelResolvedOverload overload, CelValueConverter valueConverter, Object[] args) + throws CelEvaluationException { + try { + Object result = overload.getDefinition().apply(args); + return valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result)); + } catch (CelRuntimeException e) { + // Function dispatch failure that's already been handled -- just propagate. + throw e; + } catch (RuntimeException e) { + // Unexpected function dispatch failure. + throw new IllegalArgumentException( + String.format( + "Function '%s' failed with arg(s) '%s'", + overload.getOverloadId(), Joiner.on(", ").join(args)), + e); + } + } + + private EvalHelpers() {} +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java index a22ba8e94..cdee878ee 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java @@ -19,6 +19,7 @@ import com.google.common.collect.ImmutableList; import dev.cel.common.exceptions.CelOverloadNotFoundException; import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.GlobalResolver; @@ -36,10 +37,17 @@ final class EvalLateBoundCall extends PlannedInterpretable { @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { Object[] argVals = new Object[args.length]; + AccumulatedUnknowns unknowns = null; for (int i = 0; i < args.length; i++) { PlannedInterpretable arg = args[i]; // Late bound functions are assumed to be strict. argVals[i] = evalStrictly(arg, resolver, frame); + + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVals[i]); + } + + if (unknowns != null) { + return unknowns; } CelResolvedOverload resolvedOverload = diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.java index 70009d567..5ad1933d7 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.java @@ -17,6 +17,7 @@ import com.google.common.base.Preconditions; import com.google.errorprone.annotations.Immutable; import dev.cel.common.exceptions.CelOverloadNotFoundException; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.GlobalResolver; import java.util.Optional; @@ -29,6 +30,10 @@ final class EvalOptionalOr extends PlannedInterpretable { public Object eval(GlobalResolver resolver, ExecutionFrame frame) { Object lhsValue = EvalHelpers.evalStrictly(lhs, resolver, frame); + if (lhsValue instanceof AccumulatedUnknowns) { + return lhsValue; + } + if (!(lhsValue instanceof Optional)) { throw new CelOverloadNotFoundException("or"); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.java index 7a4940c7c..6634d60f6 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.java @@ -17,6 +17,7 @@ import com.google.common.base.Preconditions; import com.google.errorprone.annotations.Immutable; import dev.cel.common.exceptions.CelOverloadNotFoundException; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.GlobalResolver; import java.util.Optional; @@ -28,6 +29,10 @@ final class EvalOptionalOrValue extends PlannedInterpretable { @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) { Object lhsValue = EvalHelpers.evalStrictly(lhs, resolver, frame); + if (lhsValue instanceof AccumulatedUnknowns) { + return lhsValue; + } + if (!(lhsValue instanceof Optional)) { throw new CelOverloadNotFoundException("orValue"); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java index bc14149f3..8887aa697 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java @@ -18,6 +18,7 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.common.values.CelValueConverter; import dev.cel.common.values.SelectableValue; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.GlobalResolver; import java.util.Map; import java.util.Optional; @@ -42,6 +43,10 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { } Object runtimeOperandValue = celValueConverter.toRuntimeValue(operandValue); + if (runtimeOperandValue instanceof AccumulatedUnknowns) { + return runtimeOperandValue; + } + boolean hasField = false; if (runtimeOperandValue instanceof SelectableValue) { @@ -62,6 +67,10 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { return resultValue; } + if (resultValue instanceof AccumulatedUnknowns) { + return resultValue; + } + return Optional.of(resultValue); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java index 22fc56a7f..bc19ed81a 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java @@ -1,66 +1,73 @@ -// Copyright 2025 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dev.cel.runtime.planner; - -import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; - -import com.google.common.base.Preconditions; -import dev.cel.common.values.ErrorValue; -import dev.cel.runtime.GlobalResolver; - -final class EvalOr extends PlannedInterpretable { - - @SuppressWarnings("Immutable") - private final PlannedInterpretable[] args; - - @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) { - ErrorValue errorValue = null; - for (PlannedInterpretable arg : args) { - Object argVal = evalNonstrictly(arg, resolver, frame); - if (argVal instanceof Boolean) { - // Short-circuit on true - if (((boolean) argVal)) { - return true; - } - } else if (argVal instanceof ErrorValue) { - errorValue = (ErrorValue) argVal; - } else { - // TODO: Handle unknowns - errorValue = - ErrorValue.create( - arg.exprId(), - new IllegalArgumentException( - String.format("Expected boolean value, found: %s", argVal))); - } - } - - if (errorValue != null) { - return errorValue; - } - - return false; - } - - static EvalOr create(long exprId, PlannedInterpretable[] args) { - return new EvalOr(exprId, args); - } - - private EvalOr(long exprId, PlannedInterpretable[] args) { - super(exprId); - Preconditions.checkArgument(args.length == 2); - this.args = args; - } -} +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; + +import com.google.common.base.Preconditions; +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.GlobalResolver; + +final class EvalOr extends PlannedInterpretable { + + @SuppressWarnings("Immutable") + private final PlannedInterpretable[] args; + + @Override + public Object eval(GlobalResolver resolver, ExecutionFrame frame) { + ErrorValue errorValue = null; + AccumulatedUnknowns unknowns = null; + for (PlannedInterpretable arg : args) { + Object argVal = evalNonstrictly(arg, resolver, frame); + if (argVal instanceof Boolean) { + // Short-circuit on true + if (((boolean) argVal)) { + return true; + } + } else if (argVal instanceof ErrorValue) { + errorValue = (ErrorValue) argVal; + } else if (argVal instanceof AccumulatedUnknowns) { + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVal); + } else { + errorValue = + ErrorValue.create( + arg.exprId(), + new IllegalArgumentException( + String.format("Expected boolean value, found: %s", argVal))); + } + } + + if (unknowns != null) { + return unknowns; + } + + if (errorValue != null) { + return errorValue; + } + + return false; + } + + static EvalOr create(long exprId, PlannedInterpretable[] args) { + return new EvalOr(exprId, args); + } + + private EvalOr(long exprId, PlannedInterpretable[] args) { + super(exprId); + Preconditions.checkArgument(args.length == 2); + this.args = args; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java index 9f14f8bf9..eb8745632 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java @@ -18,6 +18,7 @@ import static dev.cel.runtime.planner.EvalHelpers.evalStrictly; import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.GlobalResolver; @@ -34,12 +35,19 @@ final class EvalVarArgsCall extends PlannedInterpretable { @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { Object[] argVals = new Object[args.length]; + AccumulatedUnknowns unknowns = null; for (int i = 0; i < args.length; i++) { PlannedInterpretable arg = args[i]; argVals[i] = resolvedOverload.isStrict() ? evalStrictly(arg, resolver, frame) : evalNonstrictly(arg, resolver, frame); + + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVals[i]); + } + + if (unknowns != null) { + return unknowns; } return EvalHelpers.dispatch(resolvedOverload, celValueConverter, argVals); diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java b/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java index 80ee4b318..e29c68dd8 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java @@ -19,6 +19,7 @@ import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.CelResolvedOverload; +import dev.cel.runtime.PartialVars; import java.util.Collection; import java.util.Optional; @@ -27,6 +28,7 @@ final class ExecutionFrame { private final int comprehensionIterationLimit; private final CelFunctionResolver functionResolver; + private final PartialVars partialVars; private int iterationCount; Optional findOverload( @@ -47,12 +49,19 @@ void incrementIterations() { } } - static ExecutionFrame create(CelFunctionResolver functionResolver, CelOptions celOptions) { - return new ExecutionFrame(functionResolver, celOptions.comprehensionMaxIterations()); + static ExecutionFrame create( + CelFunctionResolver functionResolver, PartialVars partialVars, CelOptions celOptions) { + return new ExecutionFrame( + functionResolver, partialVars, celOptions.comprehensionMaxIterations()); } - private ExecutionFrame(CelFunctionResolver functionResolver, int limit) { + Optional partialVars() { + return Optional.ofNullable(partialVars); + } + + private ExecutionFrame(CelFunctionResolver functionResolver, PartialVars partialVars, int limit) { this.comprehensionIterationLimit = limit; this.functionResolver = functionResolver; + this.partialVars = partialVars; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java index 40a9f6203..1506eb180 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java @@ -28,10 +28,10 @@ final class MaybeAttribute implements Attribute { private final ImmutableList attributes; @Override - public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { + public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { MissingAttribute maybeError = null; for (NamespacedAttribute attr : attributes) { - Object value = attr.resolve(ctx, frame); + Object value = attr.resolve(exprId, ctx, frame); if (value == null) { continue; } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java index 02b04781c..b7fb8ad72 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java @@ -25,7 +25,7 @@ final class MissingAttribute implements Attribute { private final Kind kind; @Override - public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { + public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { switch (kind) { case ATTRIBUTE_NOT_FOUND: throw CelAttributeNotFoundException.forMissingAttributes(missingAttributes); diff --git a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java index cc8ca1d97..ed37eada1 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java @@ -15,6 +15,7 @@ package dev.cel.runtime.planner; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.Immutable; import dev.cel.common.types.CelType; @@ -23,14 +24,21 @@ import dev.cel.common.types.SimpleType; import dev.cel.common.types.TypeType; import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.CelAttribute; +import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.InterpreterUtil; +import dev.cel.runtime.PartialVars; +import java.util.Map; import java.util.NoSuchElementException; +import java.util.Optional; import org.jspecify.annotations.Nullable; @Immutable final class NamespacedAttribute implements Attribute { private final boolean disambiguateNames; - private final ImmutableSet namespacedNames; + private final ImmutableMap candidateAttributes; private final ImmutableList qualifiers; private final CelValueConverter celValueConverter; private final CelTypeProvider typeProvider; @@ -40,11 +48,11 @@ ImmutableList qualifiers() { } ImmutableSet candidateVariableNames() { - return namespacedNames; + return candidateAttributes.keySet(); } @Override - public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { + public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { GlobalResolver inputVars = ctx; // Unwrap any local activations to ensure that we reach the variables provided as input // to the expression in the event that we need to disambiguate between global and local @@ -53,13 +61,33 @@ public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { inputVars = unwrapToNonLocal(ctx); } - for (String name : namespacedNames) { + for (Map.Entry entry : candidateAttributes.entrySet()) { + String name = entry.getKey(); + CelAttribute attr = entry.getValue(); + GlobalResolver resolver = ctx; if (disambiguateNames) { resolver = inputVars; } Object value = resolver.resolve(name); + value = InterpreterUtil.maybeAdaptToAccumulatedUnknowns(value); + + PartialVars partialVars = frame.partialVars().orElse(null); + + if (partialVars != null) { + ImmutableList patterns = partialVars.unknowns(); + for (Qualifier qualifier : qualifiers) { + attr = attr.qualify(CelAttribute.Qualifier.fromGeneric(qualifier.value())); + } + + CelAttributePattern partialMatch = findPartialMatchingPattern(attr, patterns).orElse(null); + if (partialMatch != null) { + return AccumulatedUnknowns.create( + ImmutableList.of(exprId), ImmutableList.of(partialMatch.simplify(attr))); + } + } + if (value != null) { return applyQualifiers(value, celValueConverter, qualifiers); } @@ -71,7 +99,7 @@ public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { } } - return MissingAttribute.newMissingAttribute(namespacedNames); + return MissingAttribute.newMissingAttribute(candidateAttributes.keySet()); } private @Nullable Object findIdent(String name) { @@ -131,10 +159,17 @@ private GlobalResolver unwrapToNonLocal(GlobalResolver resolver) { @Override public NamespacedAttribute addQualifier(Qualifier qualifier) { + ImmutableMap.Builder attributesBuilder = ImmutableMap.builder(); + CelAttribute.Qualifier celQualifier = CelAttribute.Qualifier.fromGeneric(qualifier.value()); + + for (Map.Entry entry : candidateAttributes.entrySet()) { + attributesBuilder.put(entry.getKey(), entry.getValue().qualify(celQualifier)); + } + return new NamespacedAttribute( typeProvider, celValueConverter, - namespacedNames, + attributesBuilder.buildOrThrow(), disambiguateNames, ImmutableList.builder().addAll(qualifiers).add(qualifier).build()); } @@ -150,37 +185,49 @@ private static Object applyQualifiers( return celValueConverter.maybeUnwrap(obj); } + private static Optional findPartialMatchingPattern( + CelAttribute attr, ImmutableList patterns) { + for (CelAttributePattern pattern : patterns) { + if (pattern.isPartialMatch(attr)) { + return Optional.of(pattern); + } + } + return Optional.empty(); + } + static NamespacedAttribute create( CelTypeProvider typeProvider, CelValueConverter celValueConverter, ImmutableSet namespacedNames) { - ImmutableSet.Builder namesBuilder = ImmutableSet.builder(); + ImmutableMap.Builder attributesBuilder = ImmutableMap.builder(); boolean disambiguateNames = false; + for (String name : namespacedNames) { + String baseName = name; if (name.startsWith(".")) { disambiguateNames = true; - namesBuilder.add(name.substring(1)); - } else { - namesBuilder.add(name); + baseName = name.substring(1); } + attributesBuilder.put(baseName, CelAttribute.fromQualifiedIdentifier(baseName)); } + return new NamespacedAttribute( typeProvider, celValueConverter, - namesBuilder.build(), + attributesBuilder.buildOrThrow(), disambiguateNames, ImmutableList.of()); } - NamespacedAttribute( + private NamespacedAttribute( CelTypeProvider typeProvider, CelValueConverter celValueConverter, - ImmutableSet namespacedNames, + ImmutableMap candidateAttributes, boolean disambiguateNames, ImmutableList qualifiers) { this.typeProvider = typeProvider; this.celValueConverter = celValueConverter; - this.namespacedNames = namespacedNames; + this.candidateAttributes = candidateAttributes; this.disambiguateNames = disambiguateNames; this.qualifiers = qualifiers; } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java index 8b419cab2..34fc34b50 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java @@ -26,6 +26,8 @@ import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.CelVariableResolver; import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.InterpreterUtil; +import dev.cel.runtime.PartialVars; import dev.cel.runtime.Program; import java.util.Collection; import java.util.Map; @@ -58,47 +60,61 @@ public Optional findOverloadMatchingArgs( @Override public Object eval() throws CelEvaluationException { - return evalOrThrow(interpretable(), GlobalResolver.EMPTY, EMPTY_FUNCTION_RESOLVER); + return evalOrThrow(interpretable(), GlobalResolver.EMPTY, EMPTY_FUNCTION_RESOLVER, null); } @Override public Object eval(Map mapValue) throws CelEvaluationException { - return evalOrThrow(interpretable(), Activation.copyOf(mapValue), EMPTY_FUNCTION_RESOLVER); + return evalOrThrow(interpretable(), Activation.copyOf(mapValue), EMPTY_FUNCTION_RESOLVER, null); } @Override public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctionResolver) throws CelEvaluationException { - return evalOrThrow(interpretable(), Activation.copyOf(mapValue), lateBoundFunctionResolver); + return evalOrThrow( + interpretable(), Activation.copyOf(mapValue), lateBoundFunctionResolver, null); } @Override public Object eval(CelVariableResolver resolver) throws CelEvaluationException { return evalOrThrow( - interpretable(), (name) -> resolver.find(name).orElse(null), EMPTY_FUNCTION_RESOLVER); + interpretable(), (name) -> resolver.find(name).orElse(null), EMPTY_FUNCTION_RESOLVER, null); } @Override public Object eval(CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver) throws CelEvaluationException { return evalOrThrow( - interpretable(), (name) -> resolver.find(name).orElse(null), lateBoundFunctionResolver); + interpretable(), + (name) -> resolver.find(name).orElse(null), + lateBoundFunctionResolver, + null); + } + + @Override + public Object eval(PartialVars partialVars) throws CelEvaluationException { + return evalOrThrow( + interpretable(), + (name) -> partialVars.resolver().find(name).orElse(null), + EMPTY_FUNCTION_RESOLVER, + partialVars); } private Object evalOrThrow( PlannedInterpretable interpretable, GlobalResolver resolver, - CelFunctionResolver functionResolver) + CelFunctionResolver functionResolver, + PartialVars partialVars) throws CelEvaluationException { try { - ExecutionFrame frame = ExecutionFrame.create(functionResolver, options()); + ExecutionFrame frame = ExecutionFrame.create(functionResolver, partialVars, options()); Object evalResult = interpretable.eval(resolver, frame); if (evalResult instanceof ErrorValue) { ErrorValue errorValue = (ErrorValue) evalResult; throw newCelEvaluationException(errorValue.exprId(), errorValue.value()); } - return evalResult; + return InterpreterUtil.maybeAdaptToCelUnknownSet(evalResult); } catch (RuntimeException e) { throw newCelEvaluationException(interpretable.exprId(), e); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java index b3d83c390..1ab2fa3e7 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java @@ -17,6 +17,7 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.GlobalResolver; /** @@ -31,15 +32,18 @@ final class RelativeAttribute implements Attribute { private final ImmutableList qualifiers; @Override - public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { + public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { Object obj = EvalHelpers.evalStrictly(operand, ctx, frame); + if (obj instanceof AccumulatedUnknowns) { + return obj; + } + obj = celValueConverter.toRuntimeValue(obj); for (Qualifier qualifier : qualifiers) { obj = qualifier.qualify(obj); } - // TODO: Handle unknowns return celValueConverter.maybeUnwrap(obj); } diff --git a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel index 8a0b1f9de..577010971 100644 --- a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel @@ -130,16 +130,25 @@ java_library( srcs = [ "PlannerInterpreterTest.java", ], + resources = [ + "//runtime/testdata", + ], deps = [ "//common:cel_ast", "//common:compiler_common", "//common:container", "//common:options", + "//common/types", "//common/types:type_providers", "//extensions", "//runtime", + "//runtime:function_binding", "//runtime:runtime_experimental_factory", + "//runtime:unknown_attributes", "//testing:base_interpreter_test", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", ], diff --git a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java index 3254855c7..2c0bec739 100644 --- a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java +++ b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java @@ -14,6 +14,8 @@ package dev.cel.runtime; +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Timestamp; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.common.CelAbstractSyntaxTree; @@ -21,8 +23,15 @@ import dev.cel.common.CelOptions; import dev.cel.common.CelValidationException; import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.ListType; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.extensions.CelExtensions; import dev.cel.testing.BaseInterpreterTest; +import java.util.Arrays; +import java.util.Objects; +import org.junit.Test; import org.junit.runner.RunWith; /** Interpreter tests using ProgramPlanner */ @@ -37,7 +46,8 @@ protected CelRuntimeBuilder newBaseRuntimeBuilder(CelOptions celOptions) { .addLateBoundFunctions("record") .setOptions(celOptions) .addLibraries(CelExtensions.optional()) - .addFileTypes(TEST_FILE_DESCRIPTORS); + .addFileTypes(TEST_FILE_DESCRIPTORS) + .addMessageTypes(TestAllTypes.getDescriptor()); } @Override @@ -70,26 +80,247 @@ protected CelAbstractSyntaxTree prepareTest(CelTypeProvider typeProvider) { } } + @Override + public void optional_errors() { + if (isParseOnly) { + // Parsed-only evaluation contains function name in the + // error message instead of the function overload. + skipBaselineVerification(); + } else { + super.optional_errors(); + } + } + @Override public void unknownField() { - // TODO: Unknown support not implemented yet + // Exercised in planner_unknownFieldAccess instead skipBaselineVerification(); } @Override public void unknownResultSet() { - // TODO: Unknown support not implemented yet + // Exercised in planner_unknownResultSet_success instead skipBaselineVerification(); } - @Override - public void optional_errors() { - if (isParseOnly) { - // Parsed-only evaluation contains function name in the - // error message instead of the function overload. - skipBaselineVerification(); - } else { - super.optional_errors(); - } + @Test + public void planner_unknownFieldSelection() { + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + + CelAttributePattern patternX = CelAttributePattern.fromQualifiedIdentifier("x"); + + source = "x"; + // We have the full message, but we're claiming that the attribute is unknown. + runTest(ImmutableMap.of("x", TestAllTypes.getDefaultInstance()), patternX); + // A "partially known message". The result is still an unknown. + runTest( + ImmutableMap.of("x", TestAllTypes.getDefaultInstance()), + CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); + + source = "x.single_int32"; + runTest(ImmutableMap.of(), patternX); + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); + + source = "x.map_int32_int64[22]"; + runTest(ImmutableMap.of(), patternX); + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.map_int32_int64")); + + source = "x.repeated_nested_message[1]"; + runTest(ImmutableMap.of(), patternX); + runTest( + ImmutableMap.of(), + CelAttributePattern.fromQualifiedIdentifier("x.repeated_nested_message")); + + source = "x.single_nested_message.bb"; + runTest(ImmutableMap.of(), patternX); + runTest( + ImmutableMap.of(), + CelAttributePattern.fromQualifiedIdentifier("x.single_nested_message.bb")); + + source = "{1: x.single_int32}"; + runTest(ImmutableMap.of(), patternX); + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); + + source = "[1, x.single_int32]"; + runTest(ImmutableMap.of(), patternX); + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); + } + + @Test + public void planner_unknownResultSet_success() { + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + TestAllTypes message = + TestAllTypes.newBuilder() + .setSingleString("test") + .setSingleTimestamp(Timestamp.newBuilder().setSeconds(15)) + .build(); + ImmutableMap variables = ImmutableMap.of("x", message); + CelAttributePattern unknownInt32 = + CelAttributePattern.fromQualifiedIdentifier("x.single_int32"); + CelAttributePattern unknownInt64 = + CelAttributePattern.fromQualifiedIdentifier("x.single_int64"); + + source = "x.single_int32 == 1 && true"; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 && false"; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 && x.single_int64 == 1"; + runTest(variables, unknownInt32, unknownInt64); + + source = "true && x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = "false && x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_string == \"test\""; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_string != \"test\""; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_int64 == 1"; + runTest(variables, unknownInt32, unknownInt64); + + source = "true || x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = "false || x.single_int32 == 1"; + runTest(variables, unknownInt32); + + // dispatch test + declareFunction( + "f", memberOverload("f", Arrays.asList(SimpleType.INT, SimpleType.INT), SimpleType.BOOL)); + celRuntime = + newBaseRuntimeBuilder( + CelOptions.current() + .enableTimestampEpoch(true) + .enableHeterogeneousNumericComparisons(true) + .enableOptionalSyntax(true) + .comprehensionMaxIterations(1_000) + .build()) + .addFunctionBindings( + CelFunctionBinding.from("f", Integer.class, Integer.class, Objects::equals)) + .setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())) + .build(); + + source = "x.single_int32.f(1)"; + runTest(variables, unknownInt32); + + source = "1.f(x.single_int32)"; + runTest(variables, unknownInt32); + + source = "x.single_int64.f(x.single_int32)"; + runTest(variables, unknownInt32, unknownInt64); + + source = "[0, 2, 4].exists(z, z == 2 || z == x.single_int32)"; + runTest(variables, unknownInt32); + + source = "[0, 2, 4].exists(z, z == x.single_int32)"; + runTest(variables, unknownInt32); + + source = + "[0, 2, 4].exists_one(z, z == 0 || (z == 2 && z == x.single_int32) " + + "|| (z == 4 && z == x.single_int64))"; + runTest(variables, unknownInt32, unknownInt64); + + source = "[0, 2].all(z, z == 2 || z == x.single_int32)"; + runTest(variables, unknownInt32); + + source = + "[0, 2, 4].filter(z, z == 0 || (z == 2 && z == x.single_int32) " + + "|| (z == 4 && z == x.single_int64))"; + runTest(variables, unknownInt32, unknownInt64); + + source = + "[0, 2, 4].map(z, z == 0 || (z == 2 && z == x.single_int32) " + + "|| (z == 4 && z == x.single_int64))"; + runTest(variables, unknownInt32, unknownInt64); + + source = "x.single_int32 == 1 ? 1 : 2"; + runTest(variables, unknownInt32); + + source = "true ? x.single_int32 : 2"; + runTest(variables, unknownInt32); + + source = "true ? 1 : x.single_int32"; + runTest(variables, unknownInt32); + + source = "false ? x.single_int32 : 2"; + runTest(variables, unknownInt32); + + source = "false ? 1 : x.single_int32"; + runTest(variables, unknownInt32); + + source = "x.single_int64 == 1 ? x.single_int32 : x.single_int32"; + runTest(variables, unknownInt32, unknownInt64); + + source = "{x.single_int32: 2, 3: 4}"; + runTest(variables, unknownInt32); + + source = "{1: x.single_int32, 3: 4}"; + runTest(variables, unknownInt32); + + source = "{1: x.single_int32, x.single_int64: 4}"; + runTest(variables, unknownInt32, unknownInt64); + + source = "[1, x.single_int32, 3, 4]"; + runTest(variables, unknownInt32); + + source = "[1, x.single_int32, x.single_int64, 4]"; + runTest(variables, unknownInt32, unknownInt64); + + source = "TestAllTypes{single_int32: x.single_int32}.single_int32 == 2"; + runTest(variables, unknownInt32); + + source = "TestAllTypes{single_int32: x.single_int32, single_int64: x.single_int64}"; + runTest(variables, unknownInt32, unknownInt64); + + clearAllDeclarations(); + declareVariable("unknown_list", ListType.create(SimpleType.INT)); + source = "unknown_list.map(x, x)"; + runTest(variables, CelAttributePattern.fromQualifiedIdentifier("unknown_list")); + } + + @Test + public void planner_unknownResultSet_errors() { + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + TestAllTypes message = + TestAllTypes.newBuilder() + .setSingleString("test") + .setSingleTimestamp(Timestamp.newBuilder().setSeconds(15)) + .build(); + ImmutableMap variables = ImmutableMap.of("x", message); + CelAttributePattern unknownInt32 = + CelAttributePattern.fromQualifiedIdentifier("x.single_int32"); + + source = "x.single_int32 == 1 && x.single_timestamp <= timestamp(\"bad timestamp string\")"; + runTest(variables, unknownInt32); + + source = "x.single_timestamp <= timestamp(\"bad timestamp string\") && x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = + "x.single_timestamp <= timestamp(\"bad timestamp string\") " + + "&& x.single_timestamp > timestamp(\"another bad timestamp string\")"; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_timestamp <= timestamp(\"bad timestamp string\")"; + runTest(variables, unknownInt32); + + source = "x.single_timestamp <= timestamp(\"bad timestamp string\") || x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = + "x.single_timestamp <= timestamp(\"bad timestamp string\") " + + "|| x.single_timestamp > timestamp(\"another bad timestamp string\")"; + runTest(variables, unknownInt32); + + source = "x"; + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x")); } } diff --git a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel index fb05b0b31..9116818dc 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel @@ -43,10 +43,12 @@ java_library( "//runtime:descriptor_type_resolver", "//runtime:dispatcher", "//runtime:function_binding", + "//runtime:partial_vars", "//runtime:program", "//runtime:runtime_equality", "//runtime:runtime_helpers", "//runtime:standard_functions", + "//runtime:unknown_attributes", "//runtime/planner:program_planner", "//runtime/standard:type", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index 20b4e641a..de30902d3 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -65,13 +65,17 @@ import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import dev.cel.extensions.CelExtensions; import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.CelAttribute; +import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelLateFunctionBindings; import dev.cel.runtime.CelStandardFunctions; import dev.cel.runtime.CelStandardFunctions.StandardFunction; +import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.DefaultDispatcher; import dev.cel.runtime.DescriptorTypeResolver; +import dev.cel.runtime.PartialVars; import dev.cel.runtime.Program; import dev.cel.runtime.RuntimeEquality; import dev.cel.runtime.RuntimeHelpers; @@ -946,6 +950,35 @@ public void plan_comprehension_iterationLimit_success() throws Exception { ImmutableList.of(2L, 3L), ImmutableList.of(3L, 4L), ImmutableList.of(4L, 5L))); } + @Test + public void plan_partialEval_withWildcardQualification() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("unk", MapType.create(SimpleType.STRING, SimpleType.BOOL)) + .addVar("unk.a", SimpleType.BOOL) + .addVar("unk.b", SimpleType.BOOL) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "unk.a && unk.b && unk['c']"); + + Program program = PLANNER.plan(ast); + + CelUnknownSet result = + (CelUnknownSet) + program.eval( + PartialVars.of( + CelAttributePattern.create("unk") + .qualify(CelAttribute.Qualifier.ofWildCard()))); + + assertThat(result) + .isEqualTo( + CelUnknownSet.create( + ImmutableSet.of( + CelAttribute.create("unk"), + CelAttribute.create("unk").qualify(CelAttribute.Qualifier.ofString("a")), + CelAttribute.create("unk").qualify(CelAttribute.Qualifier.ofString("b"))), + ImmutableSet.of(2L, 5L, 7L))); + } + @Test public void localShadowIdentifier_inSelect() throws Exception { CelCompiler celCompiler = diff --git a/runtime/src/test/resources/planner_unknownFieldSelection.baseline b/runtime/src/test/resources/planner_unknownFieldSelection.baseline new file mode 100644 index 000000000..0cbc75299 --- /dev/null +++ b/runtime/src/test/resources/planner_unknownFieldSelection.baseline @@ -0,0 +1,111 @@ +Source: x +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=, unknown_attributes=[x]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[1]} + +Source: x +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[1]} + +Source: x.single_int32 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[2]} + +Source: x.single_int32 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[2]} + +Source: x.map_int32_int64[22] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[2]} + +Source: x.map_int32_int64[22] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x.map_int32_int64]} +result: CelUnknownSet{attributes=[x.map_int32_int64], unknownExprIds=[2]} + +Source: x.repeated_nested_message[1] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[2]} + +Source: x.repeated_nested_message[1] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x.repeated_nested_message]} +result: CelUnknownSet{attributes=[x.repeated_nested_message], unknownExprIds=[2]} + +Source: x.single_nested_message.bb +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[3]} + +Source: x.single_nested_message.bb +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x.single_nested_message.bb]} +result: CelUnknownSet{attributes=[x.single_nested_message.bb], unknownExprIds=[3]} + +Source: {1: x.single_int32} +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[5]} + +Source: {1: x.single_int32} +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[5]} + +Source: [1, x.single_int32] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[4]} + +Source: [1, x.single_int32] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[4]} diff --git a/runtime/src/test/resources/planner_unknownResultSet_errors.baseline b/runtime/src/test/resources/planner_unknownResultSet_errors.baseline new file mode 100644 index 000000000..812067ddf --- /dev/null +++ b/runtime/src/test/resources/planner_unknownResultSet_errors.baseline @@ -0,0 +1,81 @@ +Source: x.single_int32 == 1 && x.single_timestamp <= timestamp("bad timestamp string") +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[2]} + +Source: x.single_timestamp <= timestamp("bad timestamp string") && x.single_int32 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[8]} + +Source: x.single_timestamp <= timestamp("bad timestamp string") && x.single_timestamp > timestamp("another bad timestamp string") +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +error: evaluation error at test_location:89: Text 'another bad timestamp string' could not be parsed at index 0 +error_code: BAD_FORMAT + +Source: x.single_int32 == 1 || x.single_timestamp <= timestamp("bad timestamp string") +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[2]} + +Source: x.single_timestamp <= timestamp("bad timestamp string") || x.single_int32 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[8]} + +Source: x.single_timestamp <= timestamp("bad timestamp string") || x.single_timestamp > timestamp("another bad timestamp string") +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +error: evaluation error at test_location:89: Text 'another bad timestamp string' could not be parsed at index 0 +error_code: BAD_FORMAT + +Source: x +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {unknown_attributes=[x]} +result: CelUnknownSet{attributes=[x], unknownExprIds=[1]} \ No newline at end of file diff --git a/runtime/src/test/resources/planner_unknownResultSet_success.baseline b/runtime/src/test/resources/planner_unknownResultSet_success.baseline new file mode 100644 index 000000000..2f2c218d0 --- /dev/null +++ b/runtime/src/test/resources/planner_unknownResultSet_success.baseline @@ -0,0 +1,461 @@ +Source: x.single_int32 == 1 && true +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[2]} + +Source: x.single_int32 == 1 && false +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: false + +Source: x.single_int32 == 1 && x.single_int64 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[2, 7]} + +Source: true && x.single_int32 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[4]} + +Source: false && x.single_int32 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: false + +Source: x.single_int32 == 1 || x.single_string == "test" +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: true + +Source: x.single_int32 == 1 || x.single_string != "test" +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[2]} + +Source: x.single_int32 == 1 || x.single_int64 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[2, 7]} + +Source: true || x.single_int32 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: true + +Source: false || x.single_int32 == 1 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[4]} + +Source: x.single_int32.f(1) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[2]} + +Source: 1.f(x.single_int32) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[4]} + +Source: x.single_int64.f(x.single_int32) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[2, 5]} + +Source: [0, 2, 4].exists(z, z == 2 || z == x.single_int32) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: true + +Source: [0, 2, 4].exists(z, z == x.single_int32) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[10]} + +Source: [0, 2, 4].exists_one(z, z == 0 || (z == 2 && z == x.single_int32) || (z == 4 && z == x.single_int64)) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int64], unknownExprIds=[27]} + +Source: [0, 2].all(z, z == 2 || z == x.single_int32) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[13]} + +Source: [0, 2, 4].filter(z, z == 0 || (z == 2 && z == x.single_int32) || (z == 4 && z == x.single_int64)) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int64], unknownExprIds=[27]} + +Source: [0, 2, 4].map(z, z == 0 || (z == 2 && z == x.single_int32) || (z == 4 && z == x.single_int64)) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[18, 27]} + +Source: x.single_int32 == 1 ? 1 : 2 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[2]} + +Source: true ? x.single_int32 : 2 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[4]} + +Source: true ? 1 : x.single_int32 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: 1 + +Source: false ? x.single_int32 : 2 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: 2 + +Source: false ? 1 : x.single_int32 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[5]} + +Source: x.single_int64 == 1 ? x.single_int32 : x.single_int32 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int64], unknownExprIds=[2]} + +Source: {x.single_int32: 2, 3: 4} +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[4]} + +Source: {1: x.single_int32, 3: 4} +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[5]} + +Source: {1: x.single_int32, x.single_int64: 4} +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[5, 8]} + +Source: [1, x.single_int32, 3, 4] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[4]} + +Source: [1, x.single_int32, x.single_int64, 4] +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[4, 6]} + +Source: TestAllTypes{single_int32: x.single_int32}.single_int32 == 2 +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32]} +result: CelUnknownSet{attributes=[x.single_int32], unknownExprIds=[4]} + +Source: TestAllTypes{single_int32: x.single_int32, single_int64: x.single_int64} +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +declare f { + function f int.(int) -> bool +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x.single_int32, x.single_int64]} +result: CelUnknownSet{attributes=[x.single_int32, x.single_int64], unknownExprIds=[4, 7]} + +Source: unknown_list.map(x, x) +declare unknown_list { + value list(int) +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[unknown_list]} +result: CelUnknownSet{attributes=[unknown_list], unknownExprIds=[1]} \ No newline at end of file diff --git a/runtime/src/test/resources/unknownField.baseline b/runtime/src/test/resources/unknownField.baseline index c5f3c755a..8e4598bef 100644 --- a/runtime/src/test/resources/unknownField.baseline +++ b/runtime/src/test/resources/unknownField.baseline @@ -52,4 +52,4 @@ declare x { } =====> bindings: {} -result: CelUnknownSet{attributes=[], unknownExprIds=[3]} +result: CelUnknownSet{attributes=[], unknownExprIds=[3]} \ No newline at end of file diff --git a/testing/src/main/java/dev/cel/testing/BUILD.bazel b/testing/src/main/java/dev/cel/testing/BUILD.bazel index f2480a034..2ecabdf05 100644 --- a/testing/src/main/java/dev/cel/testing/BUILD.bazel +++ b/testing/src/main/java/dev/cel/testing/BUILD.bazel @@ -90,7 +90,7 @@ java_library( "//extensions:optional_library", "//runtime", "//runtime:function_binding", - "//runtime:late_function_binding", + "//runtime:partial_vars", "//runtime:unknown_attributes", "@cel_spec//proto/cel/expr:checked_java_proto", "@cel_spec//proto/cel/expr:syntax_java_proto", diff --git a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java index f3c1cf398..69db9c9db 100644 --- a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java +++ b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java @@ -75,6 +75,7 @@ import dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum; import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelLateFunctionBindings; @@ -83,6 +84,7 @@ import dev.cel.runtime.CelRuntimeFactory; import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.CelVariableResolver; +import dev.cel.runtime.PartialVars; import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; import java.io.IOException; import java.time.Duration; @@ -193,24 +195,43 @@ private Object runTest(Map input, CelLateFunctionBindings lateFunctio return runTestInternal(input, Optional.of(lateFunctionBindings)); } + @CanIgnoreReturnValue + protected Object runTest(Map input, CelAttributePattern... patterns) { + return runTestInternal(input, Optional.empty(), patterns); + } + /** * Helper to run a test for configured instance variables. Input must be of type map or {@link * CelVariableResolver}. */ - @SuppressWarnings("unchecked") private Object runTestInternal( Object input, Optional lateFunctionBindings) { + return runTestInternal(input, lateFunctionBindings, new CelAttributePattern[0]); + } + + // Test only + @SuppressWarnings("unchecked") + private Object runTestInternal( + Object input, + Optional lateFunctionBindings, + CelAttributePattern... patterns) { CelAbstractSyntaxTree ast = compileTestCase(); if (ast == null) { // Usually indicates test was not setup correctly println("Source compilation failed"); return null; } - printBinding(input); + printBinding(input, patterns); Object result = null; try { CelRuntime.Program program = celRuntime.createProgram(ast); - if (lateFunctionBindings.isPresent()) { + if (patterns.length > 0) { + PartialVars partialVars = + input instanceof Map + ? PartialVars.of((Map) input, patterns) + : PartialVars.of((CelVariableResolver) input, patterns); + result = program.eval(partialVars); + } else if (lateFunctionBindings.isPresent()) { if (input instanceof Map) { Map map = ((Map) input); CelVariableResolver variableResolver = (name) -> Optional.ofNullable(map.get(name)); @@ -2532,17 +2553,17 @@ private static String readResourceContent(String path) throws IOException { } @SuppressWarnings("unchecked") - private void printBinding(Object input) { + private void printBinding(Object input, CelAttributePattern... patterns) { if (input instanceof Map) { Map inputMap = (Map) input; - if (inputMap.isEmpty()) { + if (inputMap.isEmpty() && patterns.length == 0) { println("bindings: {}"); return; } boolean first = true; StringBuilder sb = new StringBuilder().append("{"); - for (Map.Entry entry : ((Map) input).entrySet()) { + for (Map.Entry entry : inputMap.entrySet()) { if (!first) { sb.append(", "); } @@ -2556,10 +2577,21 @@ private void printBinding(Object input) { sb.append(UnredactedDebugFormatForTest.unredactedToString(entry.getValue())); } } + if (patterns.length > 0) { + if (!inputMap.isEmpty()) { + sb.append(", "); + } + sb.append("unknown_attributes="); + sb.append(Arrays.toString(patterns)); + } sb.append("}"); println("bindings: " + sb); } else { - println("bindings: " + input); + if (patterns.length > 0) { + println("bindings: " + input + ", unknown_attributes=" + Arrays.toString(patterns)); + } else { + println("bindings: " + input); + } } } From 486a380903332f9882ad755ffe806ad58cab6e05 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 17 Mar 2026 14:56:01 -0700 Subject: [PATCH 018/204] Allow specifying a set of optimizers to run to the policy compiler PiperOrigin-RevId: 885225062 --- .../src/main/java/dev/cel/policy/BUILD.bazel | 2 ++ .../cel/policy/CelPolicyCompilerBuilder.java | 6 ++++ .../dev/cel/policy/CelPolicyCompilerImpl.java | 36 ++++++++++--------- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/policy/src/main/java/dev/cel/policy/BUILD.bazel b/policy/src/main/java/dev/cel/policy/BUILD.bazel index 52b0b1ba7..916f16f9b 100644 --- a/policy/src/main/java/dev/cel/policy/BUILD.bazel +++ b/policy/src/main/java/dev/cel/policy/BUILD.bazel @@ -138,6 +138,7 @@ java_library( ], deps = [ ":compiler", + "//optimizer:ast_optimizer", "@maven//:com_google_errorprone_error_prone_annotations", ], ) @@ -214,6 +215,7 @@ java_library( "//common/types", "//common/types:type_providers", "//optimizer", + "//optimizer:ast_optimizer", "//optimizer:optimization_exception", "//optimizer:optimizer_builder", "//optimizer/optimizers:common_subexpression_elimination", diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyCompilerBuilder.java b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerBuilder.java index 592a0120d..4089477a1 100644 --- a/policy/src/main/java/dev/cel/policy/CelPolicyCompilerBuilder.java +++ b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerBuilder.java @@ -16,6 +16,8 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CheckReturnValue; +import dev.cel.optimizer.CelAstOptimizer; +import java.util.List; /** Interface for building an instance of {@link CelPolicyCompiler} */ public interface CelPolicyCompilerBuilder { @@ -38,6 +40,10 @@ public interface CelPolicyCompilerBuilder { @CanIgnoreReturnValue CelPolicyCompilerBuilder setAstDepthLimit(int iterationLimit); + /** Configures the policy compiler to run the provided optimizers on compiled policies. */ + @CanIgnoreReturnValue + CelPolicyCompilerBuilder setOptimizers(List optimizers); + @CheckReturnValue CelPolicyCompiler build(); } diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java index f6f893c1c..7841b9827 100644 --- a/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java +++ b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java @@ -33,6 +33,7 @@ import dev.cel.common.formats.ValueString; import dev.cel.common.types.CelType; import dev.cel.common.types.SimpleType; +import dev.cel.optimizer.CelAstOptimizer; import dev.cel.optimizer.CelOptimizationException; import dev.cel.optimizer.CelOptimizer; import dev.cel.optimizer.CelOptimizerFactory; @@ -63,6 +64,7 @@ final class CelPolicyCompilerImpl implements CelPolicyCompiler { private final Cel cel; private final String variablesPrefix; private final int iterationLimit; + private final ImmutableList optimizers; private final Optional astDepthValidator; @Override @@ -140,19 +142,7 @@ public CelAbstractSyntaxTree compose(CelPolicy policy, CelCompiledRule compiledR } CelOptimizer astOptimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(cel) - .addAstOptimizers( - ConstantFoldingOptimizer.getInstance(), - SubexpressionOptimizer.newInstance( - SubexpressionOptimizerOptions.newBuilder() - // "record" is used for recording subexpression results via - // BlueprintLateFunctionBinding. Safely eliminable, since repeated - // invocation does not change the intermediate results. - .addEliminableFunctions("record") - .populateMacroCalls(true) - .enableCelBlock(true) - .build())) - .build(); + CelOptimizerFactory.standardCelOptimizerBuilder(cel).addAstOptimizers(optimizers).build(); try { // Optimize the composed graph using const fold and CSE ast = astOptimizer.optimize(ast); @@ -339,6 +329,7 @@ static final class Builder implements CelPolicyCompilerBuilder { private final Cel cel; private String variablesPrefix; private int iterationLimit; + private ImmutableList optimizers; private Optional astDepthLimitValidator; private Builder(Cel cel) { @@ -362,7 +353,7 @@ public Builder setIterationLimit(int iterationLimit) { @Override @CanIgnoreReturnValue - public CelPolicyCompilerBuilder setAstDepthLimit(int astDepthLimit) { + public Builder setAstDepthLimit(int astDepthLimit) { if (astDepthLimit < 0) { astDepthLimitValidator = Optional.empty(); } else { @@ -371,27 +362,40 @@ public CelPolicyCompilerBuilder setAstDepthLimit(int astDepthLimit) { return this; } + @Override + public Builder setOptimizers(List optimizers) { + this.optimizers = ImmutableList.copyOf(optimizers); + return this; + } + @Override public CelPolicyCompiler build() { return new CelPolicyCompilerImpl( - cel, this.variablesPrefix, this.iterationLimit, astDepthLimitValidator); + cel, this.variablesPrefix, this.iterationLimit, this.optimizers, astDepthLimitValidator); } } static Builder newBuilder(Cel cel) { return new Builder(cel) .setVariablesPrefix(DEFAULT_VARIABLE_PREFIX) - .setIterationLimit(DEFAULT_ITERATION_LIMIT); + .setIterationLimit(DEFAULT_ITERATION_LIMIT) + .setOptimizers( + ImmutableList.of( + ConstantFoldingOptimizer.getInstance(), + SubexpressionOptimizer.newInstance( + SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()))); } private CelPolicyCompilerImpl( Cel cel, String variablesPrefix, int iterationLimit, + ImmutableList optimizers, Optional astDepthValidator) { this.cel = checkNotNull(cel); this.variablesPrefix = checkNotNull(variablesPrefix); this.iterationLimit = iterationLimit; + this.optimizers = optimizers; this.astDepthValidator = astDepthValidator; } } From b0a3f588f5a78988a61bda0ee9a449e21848d56e Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 19 Mar 2026 10:41:18 -0700 Subject: [PATCH 019/204] Set enableTimestampEpoch by default PiperOrigin-RevId: 886258398 --- common/src/main/java/dev/cel/common/CelOptions.java | 1 + 1 file changed, 1 insertion(+) diff --git a/common/src/main/java/dev/cel/common/CelOptions.java b/common/src/main/java/dev/cel/common/CelOptions.java index 9cf9a9caa..e3bb8776e 100644 --- a/common/src/main/java/dev/cel/common/CelOptions.java +++ b/common/src/main/java/dev/cel/common/CelOptions.java @@ -177,6 +177,7 @@ public static Builder current() { .enableUnsignedComparisonAndArithmeticIsUnsigned(true) .enableUnsignedLongs(true) .enableRegexPartialMatch(true) + .enableTimestampEpoch(true) .errorOnDuplicateMapKeys(true) .evaluateCanonicalTypesToNativeValues(true) .errorOnIntWrap(true) From 3572797c1bd1f19ad1ab4ebc7fd34df8ef0e5aea Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 19 Mar 2026 15:35:29 -0700 Subject: [PATCH 020/204] Various fixes to CelEnvironment, YAML parsing and serialization Includes: - Container setting is made optional to prevent the CEL environment from overriding an already set container with an empty one in case the env is extended multiple times - Brings CEL environment YAML serialization in parity (examples, type, description) - Fixes "comprehensions" to be "two-var-comprehensions" - Partitioned newVaueString into newYamlString and newSourceString, where the former respects YAML multiline syntax PiperOrigin-RevId: 886408709 --- .../java/dev/cel/bundle/CelEnvironment.java | 57 ++++++++++++++----- .../cel/bundle/CelEnvironmentYamlParser.java | 28 +++++++++ .../bundle/CelEnvironmentYamlSerializer.java | 6 +- .../bundle/CelEnvironmentExporterTest.java | 3 +- .../dev/cel/bundle/CelEnvironmentTest.java | 45 ++++++++++++--- .../bundle/CelEnvironmentYamlParserTest.java | 56 ++++++++++-------- .../dev/cel/common/formats/ParserContext.java | 30 +++++++++- .../dev/cel/common/formats/YamlHelper.java | 2 +- .../common/formats/YamlParserContextImpl.java | 13 ++++- .../dev/cel/policy/CelPolicyYamlParser.java | 39 +++++++------ .../cel/policy/CelPolicyYamlParserTest.java | 16 +++++- .../java/dev/cel/policy/PolicyTestHelper.java | 8 +-- .../resources/environment/extended_env.yaml | 57 +++++++++++-------- 13 files changed, 259 insertions(+), 101 deletions(-) diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java index 8614b87b5..b85f16cb1 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java @@ -43,6 +43,7 @@ import dev.cel.common.types.OptionalType; import dev.cel.common.types.SimpleType; import dev.cel.common.types.TypeParamType; +import dev.cel.common.types.TypeType; import dev.cel.compiler.CelCompiler; import dev.cel.compiler.CelCompilerBuilder; import dev.cel.compiler.CelCompilerLibrary; @@ -71,27 +72,28 @@ public abstract class CelEnvironment { "math", CanonicalCelExtension.MATH, "optional", CanonicalCelExtension.OPTIONAL, "protos", CanonicalCelExtension.PROTOS, + "regex", CanonicalCelExtension.REGEX, "sets", CanonicalCelExtension.SETS, "strings", CanonicalCelExtension.STRINGS, - "comprehensions", CanonicalCelExtension.COMPREHENSIONS); + "two-var-comprehensions", CanonicalCelExtension.COMPREHENSIONS); private static final ImmutableMap> LIMIT_HANDLERS = ImmutableMap.of( "cel.limit.expression_code_points", - (options, value) -> options.maxExpressionCodePointSize(value), + CelOptions.Builder::maxExpressionCodePointSize, "cel.limit.parse_error_recovery", - (options, value) -> options.maxParseErrorRecoveryLimit(value), + CelOptions.Builder::maxParseErrorRecoveryLimit, "cel.limit.parse_recursion_depth", - (options, value) -> options.maxParseRecursionDepth(value)); + CelOptions.Builder::maxParseRecursionDepth); private static final ImmutableMap FEATURE_HANDLERS = ImmutableMap.of( "cel.feature.macro_call_tracking", - (options, enabled) -> options.populateMacroCalls(enabled), + CelOptions.Builder::populateMacroCalls, "cel.feature.backtick_escape_syntax", - (options, enabled) -> options.enableQuotedIdentifierSyntax(enabled), + CelOptions.Builder::enableQuotedIdentifierSyntax, "cel.feature.cross_type_numeric_comparisons", - (options, enabled) -> options.enableHeterogeneousNumericComparisons(enabled)); + CelOptions.Builder::enableHeterogeneousNumericComparisons); /** Environment source in textual format (ex: textproto, YAML). */ public abstract Optional source(); @@ -99,10 +101,8 @@ public abstract class CelEnvironment { /** Name of the environment. */ public abstract String name(); - /** - * Container, which captures default namespace and aliases for value resolution. - */ - public abstract CelContainer container(); + /** Container, which captures default namespace and aliases for value resolution. */ + public abstract Optional container(); /** * An optional description of the environment (example: location of the file containing the config @@ -226,7 +226,6 @@ public static Builder newBuilder() { return new AutoValue_CelEnvironment.Builder() .setName("") .setDescription("") - .setContainer(CelContainer.ofName("")) .setVariables(ImmutableSet.of()) .setFunctions(ImmutableSet.of()) .setFeatures(ImmutableSet.of()) @@ -242,7 +241,6 @@ public CelCompiler extend(CelCompiler celCompiler, CelOptions celOptions) CelCompilerBuilder compilerBuilder = celCompiler .toCompilerBuilder() - .setContainer(container()) .setOptions(celOptions) .setTypeProvider(celTypeProvider) .addVarDeclarations( @@ -254,6 +252,8 @@ public CelCompiler extend(CelCompiler celCompiler, CelOptions celOptions) .map(f -> f.toCelFunctionDecl(celTypeProvider)) .collect(toImmutableList())); + container().ifPresent(compilerBuilder::setContainer); + addAllCompilerExtensions(compilerBuilder, celOptions); applyStandardLibrarySubset(compilerBuilder); @@ -416,6 +416,8 @@ public abstract static class VariableDecl { /** The type of the variable. */ public abstract TypeDecl type(); + public abstract Optional description(); + /** Builder for {@link VariableDecl}. */ @AutoValue.Builder public abstract static class Builder implements RequiredFieldsChecker { @@ -428,6 +430,8 @@ public abstract static class Builder implements RequiredFieldsChecker { public abstract VariableDecl.Builder setType(TypeDecl typeDecl); + public abstract VariableDecl.Builder setDescription(String name); + @Override public ImmutableList requiredFields() { return ImmutableList.of( @@ -459,6 +463,8 @@ public abstract static class FunctionDecl { public abstract String name(); + public abstract Optional description(); + public abstract ImmutableSet overloads(); /** Builder for {@link FunctionDecl}. */ @@ -471,6 +477,8 @@ public abstract static class Builder implements RequiredFieldsChecker { public abstract FunctionDecl.Builder setName(String name); + public abstract FunctionDecl.Builder setDescription(String description); + public abstract FunctionDecl.Builder setOverloads(ImmutableSet overloads); @Override @@ -519,6 +527,9 @@ public abstract static class OverloadDecl { /** List of function overload type values. */ public abstract ImmutableList arguments(); + /** Examples for the overload. */ + public abstract ImmutableList examples(); + /** Return type of the overload. Required. */ public abstract TypeDecl returnType(); @@ -537,8 +548,21 @@ public abstract static class Builder implements RequiredFieldsChecker { // This should stay package-private to encourage add/set methods to be used instead. abstract ImmutableList.Builder argumentsBuilder(); + abstract ImmutableList.Builder examplesBuilder(); + public abstract OverloadDecl.Builder setArguments(ImmutableList args); + @CanIgnoreReturnValue + public OverloadDecl.Builder addExamples(Iterable examples) { + this.examplesBuilder().addAll(checkNotNull(examples)); + return this; + } + + @CanIgnoreReturnValue + public OverloadDecl.Builder addExamples(String... examples) { + return addExamples(Arrays.asList(examples)); + } + @CanIgnoreReturnValue public OverloadDecl.Builder addArguments(Iterable args) { this.argumentsBuilder().addAll(checkNotNull(args)); @@ -667,6 +691,10 @@ public CelType toCelType(CelTypeProvider celTypeProvider) { CelType keyType = params().get(0).toCelType(celTypeProvider); CelType valueType = params().get(1).toCelType(celTypeProvider); return MapType.create(keyType, valueType); + case "type": + checkState( + params().size() == 1, "Expected 1 parameter for type, got %s", params().size()); + return TypeType.create(params().get(0).toCelType(celTypeProvider)); default: if (isTypeParam()) { return TypeParamType.create(name()); @@ -838,6 +866,7 @@ enum CanonicalCelExtension { SETS( (options, version) -> CelExtensions.sets(options), (options, version) -> CelExtensions.sets(options)), + REGEX((options, version) -> CelExtensions.regex(), (options, version) -> CelExtensions.regex()), LISTS((options, version) -> CelExtensions.lists(), (options, version) -> CelExtensions.lists()), COMPREHENSIONS( (options, version) -> CelExtensions.comprehensions(), @@ -1054,7 +1083,7 @@ public static OverloadSelector.Builder newBuilder() { } @FunctionalInterface - private static interface BooleanOptionConsumer { + private interface BooleanOptionConsumer { void accept(CelOptions.Builder options, boolean value); } } diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java index ce8857654..f129d9f5d 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java @@ -353,6 +353,9 @@ private VariableDecl parseVariable(ParserContext ctx, Node node) { case "name": builder.setName(newString(ctx, valueNode)); break; + case "description": + builder.setDescription(newString(ctx, valueNode)); + break; case "type": if (typeDeclBuilder != null) { ctx.reportError( @@ -428,6 +431,9 @@ private FunctionDecl parseFunction(ParserContext ctx, Node node) { case "overloads": builder.setOverloads(parseOverloads(ctx, valueNode)); break; + case "description": + builder.setDescription(newString(ctx, valueNode).trim()); + break; default: ctx.reportError(keyId, String.format("Unsupported function tag: %s", keyName)); break; @@ -479,6 +485,9 @@ private static ImmutableSet parseOverloads(ParserContext ctx case "target": overloadDeclBuilder.setTarget(parseTypeDecl(ctx, valueNode)); break; + case "examples": + overloadDeclBuilder.addExamples(parseOverloadExamples(ctx, valueNode)); + break; default: ctx.reportError(keyId, String.format("Unsupported overload tag: %s", fieldName)); break; @@ -494,6 +503,25 @@ private static ImmutableSet parseOverloads(ParserContext ctx return overloadSetBuilder.build(); } + private static ImmutableList parseOverloadExamples(ParserContext ctx, Node node) { + long listValueId = ctx.collectMetadata(node); + if (!assertYamlType(ctx, listValueId, node, YamlNodeType.LIST)) { + return ImmutableList.of(); + } + SequenceNode paramsListNode = (SequenceNode) node; + ImmutableList.Builder builder = ImmutableList.builder(); + for (Node elementNode : paramsListNode.getValue()) { + long elementNodeId = ctx.collectMetadata(elementNode); + if (!assertYamlType(ctx, elementNodeId, elementNode, YamlNodeType.STRING)) { + continue; + } + + builder.add(((ScalarNode) elementNode).getValue()); + } + + return builder.build(); + } + private static ImmutableList parseOverloadArguments( ParserContext ctx, Node node) { long listValueId = ctx.collectMetadata(node); diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java index 179faf2ac..9d5b4b69e 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java @@ -79,10 +79,8 @@ public Node representData(Object data) { if (!environment.description().isEmpty()) { configMap.put("description", environment.description()); } - if (!environment.container().name().isEmpty() - || !environment.container().abbreviations().isEmpty() - || !environment.container().aliases().isEmpty()) { - configMap.put("container", environment.container()); + if (environment.container().isPresent()) { + configMap.put("container", environment.container().get()); } if (!environment.extensions().isEmpty()) { configMap.put("extensions", environment.extensions().asList()); diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java index 10b9dee8e..ae0de2c18 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java @@ -333,7 +333,7 @@ public void container() { CelEnvironmentExporter exporter = CelEnvironmentExporter.newBuilder().build(); CelEnvironment celEnvironment = exporter.export(cel); - CelContainer container = celEnvironment.container(); + CelContainer container = celEnvironment.container().get(); assertThat(container.name()).isEqualTo("cntnr"); assertThat(container.abbreviations()).containsExactly("foo.Bar", "baz.Qux").inOrder(); assertThat(container.aliases()).containsAtLeast("nm", "user.name", "id", "user.id").inOrder(); @@ -368,4 +368,3 @@ public void options() { CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10)); } } - diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java index f7eb254d7..a5a2f3e6d 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java @@ -28,6 +28,10 @@ import dev.cel.common.CelOptions; import dev.cel.common.CelValidationException; import dev.cel.common.CelValidationResult; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.TypeType; import dev.cel.compiler.CelCompiler; import dev.cel.compiler.CelCompilerFactory; import dev.cel.parser.CelStandardMacro; @@ -44,9 +48,7 @@ public void newBuilder_defaults() { assertThat(environment.source()).isEmpty(); assertThat(environment.name()).isEmpty(); assertThat(environment.description()).isEmpty(); - assertThat(environment.container().name()).isEmpty(); - assertThat(environment.container().abbreviations()).isEmpty(); - assertThat(environment.container().aliases()).isEmpty(); + assertThat(environment.container()).isEmpty(); assertThat(environment.extensions()).isEmpty(); assertThat(environment.variables()).isEmpty(); assertThat(environment.functions()).isEmpty(); @@ -65,10 +67,10 @@ public void container() { .build()) .build(); - assertThat(environment.container().name()).isEqualTo("cntr"); - assertThat(environment.container().abbreviations()).containsExactly("foo.Bar", "baz.Qux"); - assertThat(environment.container().aliases()) - .containsExactly("nm", "user.name", "id", "user.id"); + CelContainer container = environment.container().get(); + assertThat(container.name()).isEqualTo("cntr"); + assertThat(container.abbreviations()).containsExactly("foo.Bar", "baz.Qux"); + assertThat(container.aliases()).containsExactly("nm", "user.name", "id", "user.id"); } @Test @@ -81,9 +83,10 @@ public void extend_allExtensions() throws Exception { ExtensionConfig.latest("math"), ExtensionConfig.latest("optional"), ExtensionConfig.latest("protos"), + ExtensionConfig.latest("regex"), ExtensionConfig.latest("sets"), ExtensionConfig.latest("strings"), - ExtensionConfig.latest("comprehensions")); + ExtensionConfig.latest("two-var-comprehensions")); CelEnvironment environment = CelEnvironment.newBuilder().addExtensions(extensionConfigs).build(); @@ -435,4 +438,30 @@ public void stdlibSubset_functionOverloadExcluded() throws Exception { result = extendedCompiler.compile("1 == 1 && 1 != 1 + 1"); assertThat(result.getErrorString()).contains("found no matching overload for '_+_'"); } + + @Test + public void typeDecl_toCelType_type() { + CelTypeProvider typeProvider = + CelCompilerFactory.standardCelCompilerBuilder().build().getTypeProvider(); + CelEnvironment.TypeDecl typeDecl = + CelEnvironment.TypeDecl.newBuilder() + .setName("type") + .addParams(CelEnvironment.TypeDecl.create("int")) + .build(); + + CelType celType = typeDecl.toCelType(typeProvider); + + assertThat(celType).isEqualTo(TypeType.create(SimpleType.INT)); + } + + @Test + public void typeDecl_toCelType_type_wrongParamCount_throws() { + CelTypeProvider typeProvider = + CelCompilerFactory.standardCelCompilerBuilder().build().getTypeProvider(); + CelEnvironment.TypeDecl typeDecl = CelEnvironment.TypeDecl.newBuilder().setName("type").build(); + + IllegalStateException e = + assertThrows(IllegalStateException.class, () -> typeDecl.toCelType(typeProvider)); + assertThat(e).hasMessageThat().contains("Expected 1 parameter for type, got 0"); + } } diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java index e98f6110e..043664e8e 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java @@ -675,9 +675,7 @@ private enum EnvironmentParseErrorTestcase { + " | - version: 0\n" + " | ..^"), ILLEGAL_LIBRARY_SUBSET_TAG( - "name: 'test_suite_name'\n" - + "stdlib:\n" - + " unknown_tag: 'test_value'\n", + "name: 'test_suite_name'\n" + "stdlib:\n" + " unknown_tag: 'test_value'\n", "ERROR: :3:3: Unsupported library subset tag: unknown_tag\n" + " | unknown_tag: 'test_value'\n" + " | ..^"), @@ -859,30 +857,40 @@ private enum EnvironmentYamlResourceTestCase { .setVariables( VariableDecl.newBuilder() .setName("msg") + .setDescription( + "msg represents all possible type permutation which CEL understands from a" + + " proto perspective") .setType(TypeDecl.create("cel.expr.conformance.proto3.TestAllTypes")) .build()) .setFunctions( - FunctionDecl.create( - "isEmpty", - ImmutableSet.of( - OverloadDecl.newBuilder() - .setId("wrapper_string_isEmpty") - .setTarget(TypeDecl.create("google.protobuf.StringValue")) - .setReturnType(TypeDecl.create("bool")) - .build(), - OverloadDecl.newBuilder() - .setId("list_isEmpty") - .setTarget( - TypeDecl.newBuilder() - .setName("list") - .addParams( - TypeDecl.newBuilder() - .setName("T") - .setIsTypeParam(true) - .build()) - .build()) - .setReturnType(TypeDecl.create("bool")) - .build()))) + FunctionDecl.newBuilder() + .setName("isEmpty") + .setDescription( + "determines whether a list is empty,\nor a string has no characters") + .setOverloads( + ImmutableSet.of( + OverloadDecl.newBuilder() + .setId("wrapper_string_isEmpty") + .setTarget(TypeDecl.create("google.protobuf.StringValue")) + .addExamples("''.isEmpty() // true") + .setReturnType(TypeDecl.create("bool")) + .build(), + OverloadDecl.newBuilder() + .setId("list_isEmpty") + .addExamples("[].isEmpty() // true") + .addExamples("[1].isEmpty() // false") + .setTarget( + TypeDecl.newBuilder() + .setName("list") + .addParams( + TypeDecl.newBuilder() + .setName("T") + .setIsTypeParam(true) + .build()) + .build()) + .setReturnType(TypeDecl.create("bool")) + .build())) + .build()) .setFeatures(CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true)) .setLimits( ImmutableSet.of( diff --git a/common/src/main/java/dev/cel/common/formats/ParserContext.java b/common/src/main/java/dev/cel/common/formats/ParserContext.java index 0bdfdb299..17eff473f 100644 --- a/common/src/main/java/dev/cel/common/formats/ParserContext.java +++ b/common/src/main/java/dev/cel/common/formats/ParserContext.java @@ -42,6 +42,32 @@ public interface ParserContext { Map getIdToOffsetMap(); - /** NewString creates a new ValueString from the YAML node. */ - ValueString newValueString(T node); + /** + * @deprecated Use {@link #newSourceString} instead. + */ + @Deprecated + default ValueString newValueString(T node) { + return newSourceString(node); + } + + /** + * NewYamlString creates a new ValueString from the YAML node, evaluated according to standard + * YAML parsing rules. + * + *

This respects the whitespace folding semantics defined by the node's scalar style (e.g., + * folded string {@code >} versus literal string {@code |}). Use this method for general string + * fields such as {@code description}, {@code name}, or {@code id}. + */ + ValueString newYamlString(T node); + + /** + * NewRawString creates a new ValueString from the YAML node, preserving formatting for accurate + * source mapping. + * + *

This extracts the verbatim text directly from the source file, preserving raw block + * indentation and unmodified newlines. Use this method when the string represents code or a CEL + * expression where precise character-level offsets must be maintained for accurate diagnostic + * error reporting. + */ + ValueString newSourceString(T node); } diff --git a/common/src/main/java/dev/cel/common/formats/YamlHelper.java b/common/src/main/java/dev/cel/common/formats/YamlHelper.java index e0780b01f..c16126f95 100644 --- a/common/src/main/java/dev/cel/common/formats/YamlHelper.java +++ b/common/src/main/java/dev/cel/common/formats/YamlHelper.java @@ -136,7 +136,7 @@ public static boolean newBoolean(ParserContext ctx, Node node) { } public static String newString(ParserContext ctx, Node node) { - return ctx.newValueString(node).value(); + return ctx.newYamlString(node).value(); } private YamlHelper() {} diff --git a/common/src/main/java/dev/cel/common/formats/YamlParserContextImpl.java b/common/src/main/java/dev/cel/common/formats/YamlParserContextImpl.java index 456872803..9f6077562 100644 --- a/common/src/main/java/dev/cel/common/formats/YamlParserContextImpl.java +++ b/common/src/main/java/dev/cel/common/formats/YamlParserContextImpl.java @@ -62,7 +62,18 @@ public Map getIdToOffsetMap() { } @Override - public ValueString newValueString(Node node) { + public ValueString newYamlString(Node node) { + long id = collectMetadata(node); + if (!assertYamlType(this, id, node, YamlNodeType.STRING, YamlNodeType.TEXT)) { + return ValueString.of(id, ERROR); + } + + ScalarNode scalarNode = (ScalarNode) node; + return ValueString.of(id, scalarNode.getValue()); + } + + @Override + public ValueString newSourceString(Node node) { long id = collectMetadata(node); if (!assertYamlType(this, id, node, YamlNodeType.STRING, YamlNodeType.TEXT)) { return ValueString.of(id, ERROR); diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java b/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java index 43595c4ab..18b406af0 100644 --- a/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java +++ b/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java @@ -126,13 +126,13 @@ public CelPolicy parsePolicy(PolicyParserContext ctx, Node node) { parseImports(policyBuilder, ctx, valueNode); break; case "name": - policyBuilder.setName(ctx.newValueString(valueNode)); + policyBuilder.setName(ctx.newYamlString(valueNode)); break; case "description": - policyBuilder.setDescription(ctx.newValueString(valueNode)); + policyBuilder.setDescription(ctx.newYamlString(valueNode)); break; case "display_name": - policyBuilder.setDisplayName(ctx.newValueString(valueNode)); + policyBuilder.setDisplayName(ctx.newYamlString(valueNode)); break; case "rule": policyBuilder.setRule(parseRule(ctx, policyBuilder, valueNode)); @@ -189,7 +189,7 @@ private void parseImport( continue; } - policyBuilder.addImport(Import.create(valueId, ctx.newValueString(value))); + policyBuilder.addImport(Import.create(valueId, ctx.newYamlString(value))); } } @@ -212,10 +212,10 @@ public CelPolicy.Rule parseRule( Node value = nodeTuple.getValueNode(); switch (fieldName) { case "id": - ruleBuilder.setRuleId(ctx.newValueString(value)); + ruleBuilder.setRuleId(ctx.newYamlString(value)); break; case "description": - ruleBuilder.setDescription(ctx.newValueString(value)); + ruleBuilder.setDescription(ctx.newYamlString(value)); break; case "variables": ruleBuilder.addVariables(parseVariables(ctx, policyBuilder, value)); @@ -267,7 +267,7 @@ public CelPolicy.Match parseMatch( Node value = nodeTuple.getValueNode(); switch (fieldName) { case "condition": - matchBuilder.setCondition(ctx.newValueString(value)); + matchBuilder.setCondition(ctx.newSourceString(value)); break; case "output": matchBuilder @@ -275,7 +275,7 @@ public CelPolicy.Match parseMatch( .filter(result -> result.kind().equals(Match.Result.Kind.RULE)) .ifPresent( result -> ctx.reportError(tagId, "Only the rule or the output may be set")); - matchBuilder.setResult(Match.Result.ofOutput(ctx.newValueString(value))); + matchBuilder.setResult(Match.Result.ofOutput(ctx.newSourceString(value))); break; case "explanation": matchBuilder @@ -286,7 +286,7 @@ public CelPolicy.Match parseMatch( ctx.reportError( tagId, "Explanation can only be set on output match cases, not nested rules")); - matchBuilder.setExplanation(ctx.newValueString(value)); + matchBuilder.setExplanation(ctx.newYamlString(value)); break; case "rule": matchBuilder @@ -356,8 +356,8 @@ private Variable parseVariableInline( Node keyNode = nodeTuple.getKeyNode(); long keyId = ctx.collectMetadata(keyNode); builder - .setName(ctx.newValueString(keyNode)) - .setExpression(ctx.newValueString(nodeTuple.getValueNode())); + .setName(ctx.newYamlString(keyNode)) + .setExpression(ctx.newSourceString(nodeTuple.getValueNode())); iterations++; if (iterations > 1) { @@ -385,16 +385,16 @@ private Variable parseVariableObject( String keyName = ((ScalarNode) keyNode).getValue(); switch (keyName) { case "name": - builder.setName(ctx.newValueString(valueNode)); + builder.setName(ctx.newYamlString(valueNode)); break; case "expression": - builder.setExpression(ctx.newValueString(valueNode)); + builder.setExpression(ctx.newSourceString(valueNode)); break; case "description": - builder.setDescription(ctx.newValueString(valueNode)); + builder.setDescription(ctx.newYamlString(valueNode)); break; case "display_name": - builder.setDisplayName(ctx.newValueString(valueNode)); + builder.setDisplayName(ctx.newYamlString(valueNode)); break; default: tagVisitor.visitVariableTag(ctx, keyId, keyName, valueNode, policyBuilder, builder); @@ -449,8 +449,13 @@ public Map getIdToOffsetMap() { } @Override - public ValueString newValueString(Node node) { - return ctx.newValueString(node); + public ValueString newYamlString(Node node) { + return ctx.newYamlString(node); + } + + @Override + public ValueString newSourceString(Node node) { + return ctx.newSourceString(node); } } diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java index f8327c255..22aec6746 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java @@ -99,6 +99,20 @@ public void parseYamlPolicy_withDescription() throws Exception { .hasValue(ValueString.of(10, "this is a description of the variable")); } + @Test + public void parseYamlPolicy_withDescription_foldedStyle() throws Exception { + String policySource = + "name: 'policy_name'\n" + + "description: >-\n" + + " this is a multiline string\n" + + " that gets folded into a single line"; + + CelPolicy policy = POLICY_PARSER.parse(policySource); + + assertThat(policy.description().map(ValueString::value)) + .hasValue("this is a multiline string that gets folded into a single line"); + } + @Test public void parseYamlPolicy_withDisplayName() throws Exception { String policySource = @@ -144,7 +158,7 @@ public void parseYamlPolicy_withImports() throws Exception { assertThat(policy.imports()) .containsExactly( Import.create(8L, ValueString.of(9L, "foo")), - Import.create(12L, ValueString.of(13L, " bar"))) + Import.create(12L, ValueString.of(13L, "bar"))) .inOrder(); } diff --git a/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java b/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java index 8d9e0084b..18d5ffc69 100644 --- a/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java +++ b/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java @@ -268,7 +268,7 @@ public void visitPolicyTag( CelPolicy.Builder policyBuilder) { switch (tagName) { case "kind": - policyBuilder.putMetadata("kind", ctx.newValueString(node)); + policyBuilder.putMetadata("kind", ctx.newYamlString(node)); break; case "metadata": long metadataId = ctx.collectMetadata(node); @@ -299,7 +299,7 @@ public void visitRuleTag( Rule.Builder ruleBuilder) { switch (tagName) { case "failurePolicy": - policyBuilder.putMetadata(tagName, ctx.newValueString(node)); + policyBuilder.putMetadata(tagName, ctx.newYamlString(node)); break; case "matchConstraints": long matchConstraintsId = ctx.collectMetadata(node); @@ -343,13 +343,13 @@ public void visitMatchTag( case "expression": // The K8s expression to validate must return false in order to generate a violation // message. - ValueString conditionValue = ctx.newValueString(node); + ValueString conditionValue = ctx.newYamlString(node); conditionValue = conditionValue.toBuilder().setValue("!(" + conditionValue.value() + ")").build(); matchBuilder.setCondition(conditionValue); break; case "messageExpression": - matchBuilder.setResult(Result.ofOutput(ctx.newValueString(node))); + matchBuilder.setResult(Result.ofOutput(ctx.newYamlString(node))); break; default: TagVisitor.super.visitMatchTag(ctx, id, tagName, node, policyBuilder, matchBuilder); diff --git a/testing/src/test/resources/environment/extended_env.yaml b/testing/src/test/resources/environment/extended_env.yaml index 4763c868f..9fc2d511d 100644 --- a/testing/src/test/resources/environment/extended_env.yaml +++ b/testing/src/test/resources/environment/extended_env.yaml @@ -15,32 +15,43 @@ name: "extended-env" container: "cel.expr" extensions: - - name: "optional" - version: "2" - - name: "math" - version: "latest" +- name: "optional" + version: "2" +- name: "math" + version: "latest" variables: - - name: "msg" - type_name: "cel.expr.conformance.proto3.TestAllTypes" +- name: "msg" + type_name: "cel.expr.conformance.proto3.TestAllTypes" + description: >- + msg represents all possible type permutation which + CEL understands from a proto perspective functions: - - name: "isEmpty" - overloads: - - id: "wrapper_string_isEmpty" - target: - type_name: "google.protobuf.StringValue" - return: - type_name: "bool" - - id: "list_isEmpty" - target: - type_name: "list" - params: - - type_name: "T" - is_type_param: true - return: - type_name: "bool" +- name: "isEmpty" + description: |- + determines whether a list is empty, + or a string has no characters + overloads: + - id: "wrapper_string_isEmpty" + examples: + - "''.isEmpty() // true" + target: + type_name: "google.protobuf.StringValue" + return: + type_name: "bool" + - id: "list_isEmpty" + examples: + - "[].isEmpty() // true" + - "[1].isEmpty() // false" + target: + type_name: "list" + params: + - type_name: "T" + is_type_param: true + return: + type_name: "bool" features: - - name: cel.feature.macro_call_tracking - enabled: true +- name: cel.feature.macro_call_tracking + enabled: true limits: - name: cel.limit.expression_code_points value: 1000 From 690d0829812ad66d4dfeff25e07204683d628524 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 20 Mar 2026 10:38:12 -0700 Subject: [PATCH 021/204] Deprecate enableTimestampEpoch option PiperOrigin-RevId: 886867078 --- .../java/dev/cel/bundle/CelExperimentalFactory.java | 6 +----- bundle/src/test/java/dev/cel/bundle/CelImplTest.java | 1 - .../dev/cel/checker/CelCheckerLegacyImplTest.java | 2 +- common/src/main/java/dev/cel/common/CelOptions.java | 12 +++++++++--- .../java/dev/cel/conformance/ConformanceTest.java | 1 - .../dev/cel/extensions/CelOptionalLibraryTest.java | 6 +----- .../optimizers/ConstantFoldingOptimizerTest.java | 2 +- .../optimizer/optimizers/InliningOptimizerTest.java | 3 +-- .../SubexpressionOptimizerBaselineTest.java | 3 +-- .../optimizers/SubexpressionOptimizerTest.java | 3 +-- .../cel/runtime/CelRuntimeExperimentalFactory.java | 6 +----- .../test/java/dev/cel/runtime/ActivationTest.java | 3 +-- .../dev/cel/runtime/CelStandardFunctionsTest.java | 2 +- .../dev/cel/runtime/DescriptorTypeResolverTest.java | 2 -- .../java/dev/cel/runtime/PlannerInterpreterTest.java | 1 - .../java/dev/cel/testing/BaseInterpreterTest.java | 1 - .../java/dev/cel/testing/CelBaselineTestCase.java | 1 - .../validators/RegexLiteralValidatorTest.java | 3 +-- .../validators/TimestampLiteralValidatorTest.java | 5 ++--- 19 files changed, 22 insertions(+), 41 deletions(-) diff --git a/bundle/src/main/java/dev/cel/bundle/CelExperimentalFactory.java b/bundle/src/main/java/dev/cel/bundle/CelExperimentalFactory.java index 2275d1c56..9a3e95dd8 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelExperimentalFactory.java +++ b/bundle/src/main/java/dev/cel/bundle/CelExperimentalFactory.java @@ -50,11 +50,7 @@ public static CelBuilder plannerCelBuilder() { CelCheckerLegacyImpl.newBuilder().setStandardEnvironmentEnabled(true)), CelRuntimeImpl.newBuilder()) // CEL-Internal-2 - .setOptions( - CelOptions.current() - .enableHeterogeneousNumericComparisons(true) - .enableTimestampEpoch(true) - .build()); + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()); } private CelExperimentalFactory() {} diff --git a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java index 9f7083c92..ae37fca50 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java @@ -2109,7 +2109,6 @@ public void program_fdsContainsWktDependency_descriptorInstancesMatch() throws E standardCelBuilderWithMacros() .addMessageTypes(descriptors) // CEL-Internal-2 - .setOptions(CelOptions.current().enableTimestampEpoch(true).build()) .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .build(); CelAbstractSyntaxTree ast = diff --git a/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java b/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java index c0c54381d..92a70c2d6 100644 --- a/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java +++ b/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java @@ -63,7 +63,7 @@ public void toCheckerBuilder_isImmutable() { public void toCheckerBuilder_singularFields_copied() { CelStandardDeclarations subsetDecls = CelStandardDeclarations.newBuilder().includeFunctions(StandardFunction.BOOL).build(); - CelOptions celOptions = CelOptions.current().enableTimestampEpoch(true).build(); + CelOptions celOptions = CelOptions.current().build(); CelContainer celContainer = CelContainer.ofName("foo"); CelType expectedResultType = SimpleType.BOOL; CelTypeProvider customTypeProvider = diff --git a/common/src/main/java/dev/cel/common/CelOptions.java b/common/src/main/java/dev/cel/common/CelOptions.java index e3bb8776e..0c348c8a8 100644 --- a/common/src/main/java/dev/cel/common/CelOptions.java +++ b/common/src/main/java/dev/cel/common/CelOptions.java @@ -293,14 +293,20 @@ public abstract static class Builder { public abstract Builder enableHomogeneousLiterals(boolean value); /** - * Enable the {@code int64_to_timestamp} overload which creates a timestamp from Uxix epoch + * Enable the {@code int64_to_timestamp} overload which creates a timestamp from Unix epoch * seconds. * - *

This option will be automatically enabled after a sufficient period of time has elapsed to - * ensure that all runtimes support the implementation. + *

Historically used to opt-in to this feature, this option is now enabled by default across + * all runtimes. * *

TODO: Remove this feature once it has been auto-enabled. + * + * @deprecated This option is now enabled by default. If you are passing {@code true}, simply + * remove this method call. If you are passing {@code false} to disable this feature, subset + * the environment instead using {@code dev.cel.checker.CelStandardDeclarations} and {@code + * dev.cel.runtime.CelStandardFunctions}. */ + @Deprecated public abstract Builder enableTimestampEpoch(boolean value); /** diff --git a/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java index 5a25fb9d9..86f9b8f29 100644 --- a/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java +++ b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java @@ -58,7 +58,6 @@ public final class ConformanceTest extends Statement { private static final CelOptions OPTIONS = CelOptions.current() - .enableTimestampEpoch(true) .enableHeterogeneousNumericComparisons(true) .enableProtoDifferencerEquality(true) .enableOptionalSyntax(true) diff --git a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java index ab412fb39..4f348c12a 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java @@ -123,11 +123,7 @@ private CelBuilder newCelBuilder(int version) { } return celBuilder - .setOptions( - CelOptions.current() - .enableTimestampEpoch(true) - .enableHeterogeneousNumericComparisons(true) - .build()) + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .addMessageTypes(TestAllTypes.getDescriptor()) diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java index e259a7a35..a8cadf83a 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java @@ -47,7 +47,7 @@ @RunWith(TestParameterInjector.class) public class ConstantFoldingOptimizerTest { private static final CelOptions CEL_OPTIONS = - CelOptions.current().populateMacroCalls(true).enableTimestampEpoch(true).build(); + CelOptions.current().populateMacroCalls(true).build(); private static final Cel CEL = CelFactory.standardCelBuilder() .addVar("x", SimpleType.DYN) diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/InliningOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/InliningOptimizerTest.java index 7930a03d8..da2e9b745 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/InliningOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/InliningOptimizerTest.java @@ -58,8 +58,7 @@ public class InliningOptimizerTest { "child", StructTypeReference.create(TestAllTypes.NestedMessage.getDescriptor().getFullName())) .addVar("shadowed_ident", SimpleType.INT) - .setOptions( - CelOptions.current().populateMacroCalls(true).enableTimestampEpoch(true).build()) + .setOptions(CelOptions.current().populateMacroCalls(true).build()) .build(); @Test diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java index 07573f428..802ef3037 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java @@ -265,8 +265,7 @@ private static CelBuilder newCelBuilder() { .addMessageTypes(TestAllTypes.getDescriptor()) .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .setOptions( - CelOptions.current().enableTimestampEpoch(true).populateMacroCalls(true).build()) + .setOptions(CelOptions.current().populateMacroCalls(true).build()) .addCompilerLibraries( CelExtensions.optional(), CelExtensions.bindings(), CelExtensions.comprehensions()) .addRuntimeLibraries(CelExtensions.optional(), CelExtensions.comprehensions()) diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java index 735cd24f0..2289a7d4a 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java @@ -105,8 +105,7 @@ private static CelBuilder newCelBuilder() { return CelFactory.standardCelBuilder() .addMessageTypes(TestAllTypes.getDescriptor()) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .setOptions( - CelOptions.current().enableTimestampEpoch(true).populateMacroCalls(true).build()) + .setOptions(CelOptions.current().populateMacroCalls(true).build()) .addCompilerLibraries(CelExtensions.bindings()) .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeExperimentalFactory.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeExperimentalFactory.java index d0089e48d..743f90669 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeExperimentalFactory.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeExperimentalFactory.java @@ -41,11 +41,7 @@ public final class CelRuntimeExperimentalFactory { public static CelRuntimeBuilder plannerRuntimeBuilder() { return CelRuntimeImpl.newBuilder() // CEL-Internal-2 - .setOptions( - CelOptions.current() - .enableTimestampEpoch(true) - .enableHeterogeneousNumericComparisons(true) - .build()); + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()); } private CelRuntimeExperimentalFactory() {} diff --git a/runtime/src/test/java/dev/cel/runtime/ActivationTest.java b/runtime/src/test/java/dev/cel/runtime/ActivationTest.java index 5e3f3f1fe..fc435c848 100644 --- a/runtime/src/test/java/dev/cel/runtime/ActivationTest.java +++ b/runtime/src/test/java/dev/cel/runtime/ActivationTest.java @@ -33,11 +33,10 @@ public final class ActivationTest { private static final CelOptions TEST_OPTIONS = - CelOptions.current().enableTimestampEpoch(true).enableUnsignedLongs(true).build(); + CelOptions.current().enableUnsignedLongs(true).build(); private static final CelOptions TEST_OPTIONS_SKIP_UNSET_FIELDS = CelOptions.current() - .enableTimestampEpoch(true) .enableUnsignedLongs(true) .fromProtoUnsetFieldOption(CelOptions.ProtoUnsetFieldOptions.SKIP) .build(); diff --git a/runtime/src/test/java/dev/cel/runtime/CelStandardFunctionsTest.java b/runtime/src/test/java/dev/cel/runtime/CelStandardFunctionsTest.java index d85ef7424..c5f5572a7 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelStandardFunctionsTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelStandardFunctionsTest.java @@ -224,7 +224,7 @@ public void unsignedLongsDisabled_int64Identity_throws() { public void timestampEpochDisabled_int64Identity_throws() { CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() - .setOptions(CelOptions.current().enableTimestampEpoch(true).build()) + .setOptions(CelOptions.current().build()) .build(); CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder() diff --git a/runtime/src/test/java/dev/cel/runtime/DescriptorTypeResolverTest.java b/runtime/src/test/java/dev/cel/runtime/DescriptorTypeResolverTest.java index 878576f94..bdd865601 100644 --- a/runtime/src/test/java/dev/cel/runtime/DescriptorTypeResolverTest.java +++ b/runtime/src/test/java/dev/cel/runtime/DescriptorTypeResolverTest.java @@ -24,7 +24,6 @@ import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; -import dev.cel.common.CelOptions; import dev.cel.common.types.OpaqueType; import dev.cel.common.types.OptionalType; import dev.cel.common.types.ProtoMessageTypeProvider; @@ -44,7 +43,6 @@ public class DescriptorTypeResolverTest { private static final Cel CEL = CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableTimestampEpoch(true).build()) .setTypeProvider(PROTO_MESSAGE_TYPE_PROVIDER) .addCompilerLibraries(CelOptionalLibrary.INSTANCE) .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) diff --git a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java index 2c0bec739..181842ab4 100644 --- a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java +++ b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java @@ -198,7 +198,6 @@ public void planner_unknownResultSet_success() { celRuntime = newBaseRuntimeBuilder( CelOptions.current() - .enableTimestampEpoch(true) .enableHeterogeneousNumericComparisons(true) .enableOptionalSyntax(true) .comprehensionMaxIterations(1_000) diff --git a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java index 69db9c9db..42cb5e41b 100644 --- a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java +++ b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java @@ -114,7 +114,6 @@ public abstract class BaseInterpreterTest extends CelBaselineTestCase { private static final CelOptions BASE_CEL_OPTIONS = CelOptions.current() - .enableTimestampEpoch(true) .enableHeterogeneousNumericComparisons(true) .enableOptionalSyntax(true) .comprehensionMaxIterations(1_000) diff --git a/testing/src/main/java/dev/cel/testing/CelBaselineTestCase.java b/testing/src/main/java/dev/cel/testing/CelBaselineTestCase.java index 79ae88f47..8c79e5931 100644 --- a/testing/src/main/java/dev/cel/testing/CelBaselineTestCase.java +++ b/testing/src/main/java/dev/cel/testing/CelBaselineTestCase.java @@ -56,7 +56,6 @@ public abstract class CelBaselineTestCase extends BaselineTestCase { protected static final int COMPREHENSION_MAX_ITERATIONS = 1_000; protected static final CelOptions TEST_OPTIONS = CelOptions.current() - .enableTimestampEpoch(true) .enableHeterogeneousNumericComparisons(true) .enableHiddenAccumulatorVar(true) .enableOptionalSyntax(true) diff --git a/validator/src/test/java/dev/cel/validator/validators/RegexLiteralValidatorTest.java b/validator/src/test/java/dev/cel/validator/validators/RegexLiteralValidatorTest.java index 35a9ffd4f..a41317371 100644 --- a/validator/src/test/java/dev/cel/validator/validators/RegexLiteralValidatorTest.java +++ b/validator/src/test/java/dev/cel/validator/validators/RegexLiteralValidatorTest.java @@ -39,8 +39,7 @@ @RunWith(TestParameterInjector.class) public class RegexLiteralValidatorTest { - private static final CelOptions CEL_OPTIONS = - CelOptions.current().enableTimestampEpoch(true).build(); + private static final CelOptions CEL_OPTIONS = CelOptions.current().build(); private static final Cel CEL = CelFactory.standardCelBuilder().setOptions(CEL_OPTIONS).build(); diff --git a/validator/src/test/java/dev/cel/validator/validators/TimestampLiteralValidatorTest.java b/validator/src/test/java/dev/cel/validator/validators/TimestampLiteralValidatorTest.java index 7770df54c..404ed7f7e 100644 --- a/validator/src/test/java/dev/cel/validator/validators/TimestampLiteralValidatorTest.java +++ b/validator/src/test/java/dev/cel/validator/validators/TimestampLiteralValidatorTest.java @@ -41,8 +41,7 @@ @RunWith(TestParameterInjector.class) public class TimestampLiteralValidatorTest { - private static final CelOptions CEL_OPTIONS = - CelOptions.current().enableTimestampEpoch(true).build(); + private static final CelOptions CEL_OPTIONS = CelOptions.current().build(); private static final Cel CEL = CelFactory.standardCelBuilder().setOptions(CEL_OPTIONS).build(); @@ -205,7 +204,7 @@ public void parentIsNotCallExpr_doesNotThrow(String source) throws Exception { public void env_withSetResultType_success() throws Exception { Cel cel = CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableTimestampEpoch(true).build()) + .setOptions(CelOptions.current().build()) .setResultType(SimpleType.BOOL) .build(); CelValidator validator = From b92f1f6a4ddaefe3f9aeca6a284b1cf097f63ef2 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 26 Mar 2026 16:28:49 -0700 Subject: [PATCH 022/204] JSON field name resolution fix for shadowed cases PiperOrigin-RevId: 890106179 --- .../src/test/java/dev/cel/bundle/BUILD.bazel | 2 + .../test/java/dev/cel/bundle/CelImplTest.java | 135 +++++++++++++++--- .../cel/common/values/ProtoMessageValue.java | 10 +- .../values/ProtoMessageValueProvider.java | 10 +- .../cel/common/internal/DynamicProtoTest.java | 2 +- .../types/ProtoMessageTypeProviderTest.java | 6 +- .../cel/policy/CelPolicyCompilerImplTest.java | 2 +- .../runtime/DescriptorMessageProvider.java | 32 ++--- .../dev/cel/runtime/CelLiteRuntimeTest.java | 2 +- testing/protos/BUILD.bazel | 5 + testing/src/test/resources/protos/BUILD.bazel | 13 ++ .../test/resources/protos/single_file.proto | 14 +- .../protos/single_file_extensions.proto | 27 ++++ 13 files changed, 208 insertions(+), 52 deletions(-) create mode 100644 testing/src/test/resources/protos/single_file_extensions.proto diff --git a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel index ffa3322fe..2901e1ff9 100644 --- a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel +++ b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel @@ -17,6 +17,7 @@ java_library( deps = [ "//:java_truth", "//bundle:cel", + "//bundle:cel_experimental_factory", "//bundle:cel_impl", "//bundle:environment", "//bundle:environment_exception", @@ -55,6 +56,7 @@ java_library( "//runtime:evaluation_listener", "//runtime:function_binding", "//runtime:unknown_attributes", + "//testing/protos:single_file_extension_java_proto", "//testing/protos:single_file_java_proto", "@cel_spec//proto/cel/expr:checked_java_proto", "@cel_spec//proto/cel/expr:syntax_java_proto", diff --git a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java index ae37fca50..22ef7e2f4 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java @@ -98,6 +98,7 @@ import dev.cel.expr.conformance.proto2.Proto2ExtensionScopedMessage; import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.extensions.CelExtensions; import dev.cel.parser.CelParserImpl; import dev.cel.parser.CelStandardMacro; import dev.cel.runtime.CelAttribute; @@ -113,7 +114,8 @@ import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.CelVariableResolver; import dev.cel.runtime.UnknownContext; -import dev.cel.testing.testdata.SingleFileProto.SingleFile; +import dev.cel.testing.testdata.SingleFile; +import dev.cel.testing.testdata.SingleFileExtensionsProto; import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; import java.time.Instant; import java.util.ArrayList; @@ -2142,20 +2144,90 @@ public void toBuilder_isImmutable() { } @Test - public void eval_withJsonFieldName() throws Exception { - Cel cel = - standardCelBuilderWithMacros() - .addVar("file", StructTypeReference.create(SingleFile.getDescriptor().getFullName())) - .addMessageTypes(SingleFile.getDescriptor()) - .setOptions(CelOptions.current().enableJsonFieldNames(true).build()) - .build(); - CelAbstractSyntaxTree ast = cel.compile("file.camelCased").getAst(); + public void eval_withJsonFieldName(@TestParameter RuntimeEnv runtimeEnv) throws Exception { + Cel cel = runtimeEnv.cel; + CelAbstractSyntaxTree ast = + cel.compile( + "file.int32_snake_case_json_name == 1 && " + + "file.int64CamelCaseJsonName == 2 && " + + "file.uint32DefaultJsonName == 3u && " + + "file.`uint64-custom-json-name` == 4u && " + + "file.single_string == 'shadows' && " + + "file.singleString == 'shadowed'") + .getAst(); + + boolean result = + (boolean) + cel.createProgram(ast) + .eval( + ImmutableMap.of( + "file", + SingleFile.newBuilder() + .setInt32SnakeCaseJsonName(1) + .setInt64CamelCaseJsonName(2L) + .setUint32DefaultJsonName(3) + .setUint64CustomJsonName(4) + .setStringJsonNameShadows("shadows") + .setSingleString("shadowed") + .setExtension(SingleFileExtensionsProto.int64CamelCaseJsonName, 5L) + .build())); - Object result = - cel.createProgram(ast) - .eval(ImmutableMap.of("file", SingleFile.newBuilder().setSnakeCased("foo").build())); + assertThat(result).isTrue(); + } + + @Test + public void eval_withJsonFieldName_fieldsFallBack(@TestParameter RuntimeEnv runtimeEnv) throws Exception { + Cel cel = runtimeEnv.cel; + CelAbstractSyntaxTree ast = + cel.compile( + "dyn(file).int32_snake_case_json_name == 1 && " + + "dyn(file).`uint64-custom-json-name` == 4u && " + + "dyn(file).single_string == 'shadows' && " + + "dyn(file).string_json_name_shadows == 'shadows' && " + + "dyn(file).singleString == 'shadowed'") + .getAst(); + + boolean result = + (boolean) + cel.createProgram(ast) + .eval( + ImmutableMap.of( + "file", + SingleFile.newBuilder() + .setInt32SnakeCaseJsonName(1) + .setInt64CamelCaseJsonName(2L) + .setUint32DefaultJsonName(3) + .setUint64CustomJsonName(4) + .setStringJsonNameShadows("shadows") + .setSingleString("shadowed") + .build())); - assertThat(result).isEqualTo("foo"); + assertThat(result).isTrue(); + } + + @Test + public void eval_withJsonFieldName_extensionFields(@TestParameter RuntimeEnv runtimeEnv) throws Exception { + Cel cel = runtimeEnv.cel; + CelAbstractSyntaxTree ast = + cel.compile( + "proto.getExt(file, dev.cel.testing.testdata.int64CamelCaseJsonName) == 5 &&" + + " proto.getExt(file, dev.cel.testing.testdata.single_string) == 'foo'") + .getAst(); + + boolean result = + (boolean) + cel.createProgram(ast) + .eval( + ImmutableMap.of( + "file", + SingleFile.newBuilder() + .setInt64CamelCaseJsonName(2L) + .setExtension(SingleFileExtensionsProto.int64CamelCaseJsonName, 5L) + .setSingleString("This should not be used") + .setExtension(SingleFileExtensionsProto.singleString, "foo") + .build())); + + assertThat(result).isTrue(); } @Test @@ -2171,7 +2243,7 @@ public void eval_withJsonFieldName_runtimeOptionDisabled_throws() throws Excepti .addMessageTypes(SingleFile.getDescriptor()) .setOptions(CelOptions.current().enableJsonFieldNames(false).build()) .build(); - CelAbstractSyntaxTree ast = celCompiler.compile("file.camelCased").getAst(); + CelAbstractSyntaxTree ast = celCompiler.compile("file.int64CamelCaseJsonName").getAst(); CelEvaluationException e = assertThrows( @@ -2183,7 +2255,8 @@ public void eval_withJsonFieldName_runtimeOptionDisabled_throws() throws Excepti assertThat(e) .hasMessageThat() .contains( - "field 'camelCased' is not declared in message 'dev.cel.testing.testdata.SingleFile"); + "field 'int64CamelCaseJsonName' is not declared in message" + + " 'dev.cel.testing.testdata.SingleFile"); } @Test @@ -2194,7 +2267,7 @@ public void compile_withJsonFieldName_astTagged() throws Exception { .addMessageTypes(SingleFile.getDescriptor()) .setOptions(CelOptions.current().enableJsonFieldNames(true).build()) .build(); - CelAbstractSyntaxTree ast = cel.compile("file.camelCased").getAst(); + CelAbstractSyntaxTree ast = cel.compile("file.int64CamelCaseJsonName").getAst(); assertThat(ast.getSource().getExtensions()) .contains( @@ -2243,4 +2316,34 @@ private static TypeProvider aliasingProvider(ImmutableMap typeAlia } }; } + + private enum RuntimeEnv { + LEGACY(setupEnv(CelFactory.standardCelBuilder())), + PLANNER(setupEnv(CelExperimentalFactory.plannerCelBuilder())) + ; + + private final Cel cel; + + private static Cel setupEnv(CelBuilder celBuilder) { + ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); + SingleFileExtensionsProto.registerAllExtensions(extensionRegistry); + return celBuilder + .addVar("file", StructTypeReference.create(SingleFile.getDescriptor().getFullName())) + .addMessageTypes(SingleFile.getDescriptor()) + .addFileTypes(SingleFileExtensionsProto.getDescriptor()) + .addCompilerLibraries(CelExtensions.protos()) + .setExtensionRegistry(extensionRegistry) + .setOptions( + CelOptions.current() + .enableJsonFieldNames(true) + .enableHeterogeneousNumericComparisons(true) + .enableQuotedIdentifierSyntax(true) + .build()) + .build(); + } + + RuntimeEnv(Cel cel) { + this.cel = cel; + } + } } diff --git a/common/src/main/java/dev/cel/common/values/ProtoMessageValue.java b/common/src/main/java/dev/cel/common/values/ProtoMessageValue.java index e402bb429..12d47c253 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoMessageValue.java +++ b/common/src/main/java/dev/cel/common/values/ProtoMessageValue.java @@ -92,11 +92,6 @@ public static ProtoMessageValue create( private FieldDescriptor findField( CelDescriptorPool celDescriptorPool, Descriptor descriptor, String fieldName) { - FieldDescriptor fieldDescriptor = descriptor.findFieldByName(fieldName); - if (fieldDescriptor != null) { - return fieldDescriptor; - } - if (enableJsonFieldNames()) { for (FieldDescriptor fd : descriptor.getFields()) { if (fd.getJsonName().equals(fieldName)) { @@ -105,6 +100,11 @@ private FieldDescriptor findField( } } + FieldDescriptor fieldDescriptor = descriptor.findFieldByName(fieldName); + if (fieldDescriptor != null) { + return fieldDescriptor; + } + return celDescriptorPool .findExtensionDescriptor(descriptor, fieldName) .orElseThrow( diff --git a/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java b/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java index b7895d845..7beb40c61 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java +++ b/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java @@ -68,11 +68,6 @@ public Optional newValue(String structType, Map fields) } private FieldDescriptor findField(Descriptor descriptor, String fieldName) { - FieldDescriptor fieldDescriptor = descriptor.findFieldByName(fieldName); - if (fieldDescriptor != null) { - return fieldDescriptor; - } - if (celOptions.enableJsonFieldNames()) { for (FieldDescriptor fd : descriptor.getFields()) { if (fd.getJsonName().equals(fieldName)) { @@ -81,6 +76,11 @@ private FieldDescriptor findField(Descriptor descriptor, String fieldName) { } } + FieldDescriptor fieldDescriptor = descriptor.findFieldByName(fieldName); + if (fieldDescriptor != null) { + return fieldDescriptor; + } + return protoMessageFactory .getDescriptorPool() .findExtensionDescriptor(descriptor, fieldName) diff --git a/common/src/test/java/dev/cel/common/internal/DynamicProtoTest.java b/common/src/test/java/dev/cel/common/internal/DynamicProtoTest.java index cc5ba5632..7be994391 100644 --- a/common/src/test/java/dev/cel/common/internal/DynamicProtoTest.java +++ b/common/src/test/java/dev/cel/common/internal/DynamicProtoTest.java @@ -37,7 +37,7 @@ import dev.cel.common.CelDescriptorUtil; import dev.cel.common.CelDescriptors; import dev.cel.testing.testdata.MultiFile; -import dev.cel.testing.testdata.SingleFileProto.SingleFile; +import dev.cel.testing.testdata.SingleFile; import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/common/src/test/java/dev/cel/common/types/ProtoMessageTypeProviderTest.java b/common/src/test/java/dev/cel/common/types/ProtoMessageTypeProviderTest.java index 16797b714..c9f9d9e21 100644 --- a/common/src/test/java/dev/cel/common/types/ProtoMessageTypeProviderTest.java +++ b/common/src/test/java/dev/cel/common/types/ProtoMessageTypeProviderTest.java @@ -23,7 +23,7 @@ import dev.cel.common.types.StructType.Field; import dev.cel.expr.conformance.proto2.TestAllTypes; import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; -import dev.cel.testing.testdata.SingleFileProto.SingleFile; +import dev.cel.testing.testdata.SingleFile; import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -269,8 +269,8 @@ public void findField_withJsonNameOption() { (ProtoMessageType) typeProvider.findType(SingleFile.getDescriptor().getFullName()).get(); // Note that these are the same fields, with json_name option set - Optional snakeCasedField = msgType.findField("snake_cased"); - Optional jsonNameField = msgType.findField("camelCased"); + Optional snakeCasedField = msgType.findField("int64_camel_case_json_name"); + Optional jsonNameField = msgType.findField("int64CamelCaseJsonName"); assertThat(snakeCasedField).isEmpty(); assertThat(jsonNameField).isPresent(); diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java index 336a392ff..fec5f9b94 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java @@ -45,7 +45,7 @@ import dev.cel.policy.PolicyTestHelper.TestYamlPolicy; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelLateFunctionBindings; -import dev.cel.testing.testdata.SingleFileProto.SingleFile; +import dev.cel.testing.testdata.SingleFile; import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; import java.io.IOException; import java.util.Map; diff --git a/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java b/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java index ba0e442ec..ecbba5e7e 100644 --- a/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java +++ b/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java @@ -173,30 +173,28 @@ public Object hasField(Object message, String fieldName) { } private FieldDescriptor findField(Descriptor descriptor, String fieldName) { - FieldDescriptor fieldDescriptor = descriptor.findFieldByName(fieldName); - if (fieldDescriptor == null) { - Optional maybeFieldDescriptor = - protoMessageFactory.getDescriptorPool().findExtensionDescriptor(descriptor, fieldName); - if (maybeFieldDescriptor.isPresent()) { - fieldDescriptor = maybeFieldDescriptor.get(); - } - } - - if (fieldDescriptor == null && celOptions.enableJsonFieldNames()) { + if (celOptions.enableJsonFieldNames()) { for (FieldDescriptor fd : descriptor.getFields()) { if (fd.getJsonName().equals(fieldName)) { - fieldDescriptor = fd; - break; + return fd; } } } - if (fieldDescriptor == null) { - throw new IllegalArgumentException( - String.format( - "field '%s' is not declared in message '%s'", fieldName, descriptor.getFullName())); + FieldDescriptor fieldDescriptor = descriptor.findFieldByName(fieldName); + if (fieldDescriptor != null) { + return fieldDescriptor; + } + fieldDescriptor = + protoMessageFactory.getDescriptorPool().findExtensionDescriptor(descriptor, fieldName).orElse(null); + if (fieldDescriptor != null) { + return fieldDescriptor; } - return fieldDescriptor; + + + throw new IllegalArgumentException( + String.format( + "field '%s' is not declared in message '%s'", fieldName, descriptor.getFullName())); } private static MessageOrBuilder assertFullProtoMessage(Object candidate, String fieldName) { diff --git a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeTest.java b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeTest.java index 4ffe0941c..0ce7bd184 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeTest.java @@ -59,8 +59,8 @@ import dev.cel.testing.testdata.MultiFile; import dev.cel.testing.testdata.MultiFileCelDescriptor; import dev.cel.testing.testdata.SimpleEnum; +import dev.cel.testing.testdata.SingleFile; import dev.cel.testing.testdata.SingleFileCelDescriptor; -import dev.cel.testing.testdata.SingleFileProto.SingleFile; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; diff --git a/testing/protos/BUILD.bazel b/testing/protos/BUILD.bazel index c51fbba85..17706ca45 100644 --- a/testing/protos/BUILD.bazel +++ b/testing/protos/BUILD.bazel @@ -9,6 +9,11 @@ alias( actual = "//testing/src/test/resources/protos:single_file_java_proto", ) +alias( + name = "single_file_extension_java_proto", + actual = "//testing/src/test/resources/protos:single_file_extension_java_proto", +) + alias( name = "multi_file_java_proto", actual = "//testing/src/test/resources/protos:multi_file_java_proto", diff --git a/testing/src/test/resources/protos/BUILD.bazel b/testing/src/test/resources/protos/BUILD.bazel index af361b174..1fac2e1f0 100644 --- a/testing/src/test/resources/protos/BUILD.bazel +++ b/testing/src/test/resources/protos/BUILD.bazel @@ -25,6 +25,19 @@ java_proto_library( deps = [":single_file_proto"], ) +proto_library( + name = "single_file_extension_proto", + srcs = ["single_file_extensions.proto"], + deps = [":single_file_proto"], +) + +java_proto_library( + name = "single_file_extension_java_proto", + tags = [ + ], + deps = [":single_file_extension_proto"], +) + proto_library( name = "multi_file_proto", srcs = [ diff --git a/testing/src/test/resources/protos/single_file.proto b/testing/src/test/resources/protos/single_file.proto index b5ce518e0..8306cc16c 100644 --- a/testing/src/test/resources/protos/single_file.proto +++ b/testing/src/test/resources/protos/single_file.proto @@ -12,12 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -syntax = "proto3"; +edition = "2024"; package dev.cel.testing.testdata; option java_package = "dev.cel.testing.testdata"; -option java_outer_classname = "SingleFileProto"; message SingleFile { message Path { @@ -26,5 +25,14 @@ message SingleFile { string name = 1; Path path = 2; - string snake_cased = 3 [json_name = "camelCased"]; + int32 int32_snake_case_json_name = 4 [json_name = "int32_snake_case_json_name"]; + int64 int64_camel_case_json_name = 5 [json_name = "int64CamelCaseJsonName"]; + uint32 uint32_default_json_name = 6; + uint64 uint64_custom_json_name = 7 [json_name = "uint64-custom-json-name"]; + + // Collides with normal field name. + string string_json_name_shadows = 8 [json_name = "single_string"]; + string single_string = 9; + + extensions 1000 to max; } diff --git a/testing/src/test/resources/protos/single_file_extensions.proto b/testing/src/test/resources/protos/single_file_extensions.proto new file mode 100644 index 000000000..9d18d38df --- /dev/null +++ b/testing/src/test/resources/protos/single_file_extensions.proto @@ -0,0 +1,27 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +edition = "2024"; + +package dev.cel.testing.testdata; + +import "testing/src/test/resources/protos/single_file.proto"; + +option java_package = "dev.cel.testing.testdata"; +option features.enforce_naming_style = STYLE_LEGACY; + +extend SingleFile { + int64 int64CamelCaseJsonName = 1000; + string single_string = 1001; +} From 28d2b3cd2203bac039fd6402ebd328a02d6367c5 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 27 Mar 2026 12:55:08 -0700 Subject: [PATCH 023/204] Refactor test runner to accept required descriptors at the callsite, introduce BindingTransformer PiperOrigin-RevId: 890595249 --- .../dev/cel/conformance/ConformanceTest.java | 30 +++++-- .../conformance/ConformanceTestRunner.java | 9 +- .../dev/cel/testing/testrunner/BUILD.bazel | 7 ++ .../testing/testrunner/CelTestContext.java | 82 +++++++++++++++++++ .../CelTestSuiteTextProtoParser.java | 32 ++++++-- .../testrunner/CelTestSuiteYamlParser.java | 11 ++- .../testing/testrunner/TestRunnerLibrary.java | 42 ++++++++-- .../dev/cel/testing/utils/ExprValueUtils.java | 75 ++++++++--------- .../testrunner/TestRunnerLibraryTest.java | 71 ++++++++++++++++ 9 files changed, 290 insertions(+), 69 deletions(-) diff --git a/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java index 86f9b8f29..db57ccb79 100644 --- a/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java +++ b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java @@ -16,8 +16,6 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; -import static dev.cel.testing.utils.ExprValueUtils.DEFAULT_EXTENSION_REGISTRY; -import static dev.cel.testing.utils.ExprValueUtils.DEFAULT_TYPE_REGISTRY; import static dev.cel.testing.utils.ExprValueUtils.fromValue; import static dev.cel.testing.utils.ExprValueUtils.toExprValue; @@ -29,6 +27,8 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.TypeRegistry; import dev.cel.checker.CelChecker; import dev.cel.common.CelContainer; import dev.cel.common.CelOptions; @@ -84,6 +84,21 @@ public final class ConformanceTest extends Statement { CelExtensions.strings(), CelOptionalLibrary.INSTANCE); + static final TypeRegistry CONFORMANCE_TYPE_REGISTRY = + TypeRegistry.newBuilder() + .add(dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor()) + .add(dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor()) + .build(); + + static final ExtensionRegistry CONFORMANCE_EXTENSION_REGISTRY = + createConformanceExtensionRegistry(); + + private static ExtensionRegistry createConformanceExtensionRegistry() { + ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); + dev.cel.expr.conformance.proto2.TestAllTypesExtensions.registerAllExtensions(extensionRegistry); + return extensionRegistry; + } + private static final CelParser PARSER_WITH_MACROS = CelParserFactory.standardCelParserBuilder() .setOptions(OPTIONS) @@ -106,7 +121,7 @@ private static CelChecker getChecker(SimpleTest test) throws Exception { ImmutableList.Builder decls = ImmutableList.builderWithExpectedSize(test.getTypeEnvCount()); for (dev.cel.expr.Decl decl : test.getTypeEnvList()) { - decls.add(Decl.parseFrom(decl.toByteArray(), DEFAULT_EXTENSION_REGISTRY)); + decls.add(Decl.parseFrom(decl.toByteArray(), CONFORMANCE_EXTENSION_REGISTRY)); } return CelCompilerFactory.standardCelCheckerBuilder() .setOptions(OPTIONS) @@ -127,7 +142,7 @@ private static CelRuntime getRuntime(SimpleTest test, boolean usePlanner) { // CEL-Internal-2 .setOptions(OPTIONS) .addLibraries(CANONICAL_RUNTIME_EXTENSIONS) - .setExtensionRegistry(DEFAULT_EXTENSION_REGISTRY) + .setExtensionRegistry(CONFORMANCE_EXTENSION_REGISTRY) .addMessageTypes(dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor()) .addMessageTypes(dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor()) .addFileTypes(dev.cel.expr.conformance.proto2.TestAllTypesExtensions.getDescriptor()); @@ -151,7 +166,8 @@ private static ImmutableMap getBindings(SimpleTest test) throws private static Object fromExprValue(ExprValue value) throws Exception { switch (value.getKindCase()) { case VALUE: - return fromValue(value.getValue()); + return fromValue( + value.getValue(), CONFORMANCE_TYPE_REGISTRY, CONFORMANCE_EXTENSION_REGISTRY); default: throw new IllegalArgumentException( String.format("Unexpected binding value kind: %s", value.getKindCase())); @@ -224,7 +240,7 @@ public void evaluate() throws Throwable { assertThat(result) .ignoringRepeatedFieldOrderOfFieldDescriptors( MapValue.getDescriptor().findFieldByName("entries")) - .unpackingAnyUsing(DEFAULT_TYPE_REGISTRY, DEFAULT_EXTENSION_REGISTRY) + .unpackingAnyUsing(CONFORMANCE_TYPE_REGISTRY, CONFORMANCE_EXTENSION_REGISTRY) .isEqualTo(ExprValue.newBuilder().setValue(test.getValue()).build()); break; case EVAL_ERROR: @@ -237,7 +253,7 @@ public void evaluate() throws Throwable { assertThat(result) .ignoringRepeatedFieldOrderOfFieldDescriptors( MapValue.getDescriptor().findFieldByName("entries")) - .unpackingAnyUsing(DEFAULT_TYPE_REGISTRY, DEFAULT_EXTENSION_REGISTRY) + .unpackingAnyUsing(CONFORMANCE_TYPE_REGISTRY, CONFORMANCE_EXTENSION_REGISTRY) .isEqualTo(ExprValue.newBuilder().setValue(test.getTypedResult().getResult()).build()); assertThat(resultType).isEqualTo(test.getTypedResult().getDeducedType()); break; diff --git a/conformance/src/test/java/dev/cel/conformance/ConformanceTestRunner.java b/conformance/src/test/java/dev/cel/conformance/ConformanceTestRunner.java index dc3d5021e..4c3631d31 100644 --- a/conformance/src/test/java/dev/cel/conformance/ConformanceTestRunner.java +++ b/conformance/src/test/java/dev/cel/conformance/ConformanceTestRunner.java @@ -14,8 +14,7 @@ package dev.cel.conformance; -import static dev.cel.testing.utils.ExprValueUtils.DEFAULT_EXTENSION_REGISTRY; -import static dev.cel.testing.utils.ExprValueUtils.DEFAULT_TYPE_REGISTRY; +import static dev.cel.conformance.ConformanceTest.CONFORMANCE_EXTENSION_REGISTRY; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; @@ -50,14 +49,16 @@ private static ImmutableSortedMap loadTestFiles() { SPLITTER.splitToList(System.getProperty("dev.cel.conformance.ConformanceTests.tests")); try { TextFormat.Parser parser = - TextFormat.Parser.newBuilder().setTypeRegistry(DEFAULT_TYPE_REGISTRY).build(); + TextFormat.Parser.newBuilder() + .setTypeRegistry(ConformanceTest.CONFORMANCE_TYPE_REGISTRY) + .build(); ImmutableSortedMap.Builder testFiles = ImmutableSortedMap.naturalOrder(); for (String testPath : testPaths) { SimpleTestFile.Builder fileBuilder = SimpleTestFile.newBuilder(); try (BufferedReader input = Files.newBufferedReader(Paths.get(testPath), StandardCharsets.UTF_8)) { - parser.merge(input, DEFAULT_EXTENSION_REGISTRY, fileBuilder); + parser.merge(input, CONFORMANCE_EXTENSION_REGISTRY, fileBuilder); } SimpleTestFile testFile = fileBuilder.build(); testFiles.put(testFile.getName(), testFile); diff --git a/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel b/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel index 6924f753f..5af0665f9 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel +++ b/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel @@ -92,6 +92,7 @@ java_library( "//bundle:environment", "//bundle:environment_yaml_parser", "//common:cel_ast", + "//common:cel_descriptor_util", "//common:compiler_common", "//common:options", "//common:proto_ast", @@ -134,6 +135,7 @@ java_library( ":cel_test_suite", ":cel_test_suite_exception", "//common:compiler_common", + "//common/annotations", "//common/formats:file_source", "//common/formats:parser_context", "//common/formats:yaml_helper", @@ -163,10 +165,14 @@ java_library( ":result_matcher", "//:auto_value", "//bundle:cel", + "//common:cel_descriptor_util", "//common:options", "//policy:parser", "//runtime", + "//testing/testrunner:proto_descriptor_utils", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", ], ) @@ -223,6 +229,7 @@ java_library( ":cel_test_suite", ":cel_test_suite_exception", ":registry_utils", + "//common/annotations", "@cel_spec//proto/cel/expr:expr_java_proto", "@cel_spec//proto/cel/expr/conformance/test:suite_java_proto", "@maven//:com_google_guava_guava", diff --git a/testing/src/main/java/dev/cel/testing/testrunner/CelTestContext.java b/testing/src/main/java/dev/cel/testing/testrunner/CelTestContext.java index aa0d4b34f..5635b6152 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/CelTestContext.java +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelTestContext.java @@ -14,12 +14,23 @@ package dev.cel.testing.testrunner; import com.google.auto.value.AutoValue; +import com.google.auto.value.extension.memoized.Memoized; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.TypeRegistry; import dev.cel.bundle.Cel; import dev.cel.bundle.CelFactory; +import dev.cel.common.CelDescriptorUtil; import dev.cel.common.CelOptions; import dev.cel.policy.CelPolicyParser; import dev.cel.runtime.CelLateFunctionBindings; +import dev.cel.testing.utils.ProtoDescriptorUtils; +import java.io.IOException; +import java.util.Arrays; import java.util.Map; import java.util.Optional; @@ -63,6 +74,19 @@ public abstract class CelTestContext { */ public abstract Optional celLateFunctionBindings(); + /** Interface for transforming bindings before evaluation. */ + @FunctionalInterface + public interface BindingTransformer { + ImmutableMap transform(ImmutableMap bindings) throws Exception; + } + + /** + * The binding transformer for the CEL test. + * + *

This transformer is used to transform the bindings before evaluation. + */ + public abstract Optional bindingTransformer(); + /** * The variable bindings for the CEL test. * @@ -99,6 +123,34 @@ public abstract class CelTestContext { */ public abstract Optional fileDescriptorSetPath(); + abstract ImmutableSet fileTypes(); + + @Memoized + public Optional typeRegistry() { + if (fileTypes().isEmpty() && !fileDescriptorSetPath().isPresent()) { + return Optional.empty(); + } + TypeRegistry.Builder builder = TypeRegistry.newBuilder(); + if (!fileTypes().isEmpty()) { + builder.add( + CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(fileTypes()) + .messageTypeDescriptors()); + } + if (fileDescriptorSetPath().isPresent()) { + try { + builder.add( + ProtoDescriptorUtils.getAllDescriptorsFromJvm(fileDescriptorSetPath().get()) + .messageTypeDescriptors()); + } catch (IOException e) { + throw new IllegalStateException( + "Failed to load descriptors from path: " + fileDescriptorSetPath().get(), e); + } + } + return Optional.of(builder.build()); + } + + public abstract Optional extensionRegistry(); + /** Returns a builder for {@link CelTestContext} with the current instance's values. */ public abstract Builder toBuilder(); @@ -123,6 +175,8 @@ public abstract static class Builder { public abstract Builder setCelLateFunctionBindings( CelLateFunctionBindings celLateFunctionBindings); + public abstract Builder setBindingTransformer(BindingTransformer bindingTransformer); + public abstract Builder setVariableBindings(Map variableBindings); public abstract Builder setResultMatcher(ResultMatcher resultMatcher); @@ -133,6 +187,34 @@ public abstract Builder setCelLateFunctionBindings( public abstract Builder setFileDescriptorSetPath(String fileDescriptorSetPath); + abstract ImmutableSet.Builder fileTypesBuilder(); + + @CanIgnoreReturnValue + public Builder addMessageTypes(Descriptor... descriptors) { + return addMessageTypes(Arrays.asList(descriptors)); + } + + @CanIgnoreReturnValue + public Builder addMessageTypes(Iterable descriptors) { + for (Descriptor descriptor : descriptors) { + addFileTypes(descriptor.getFile()); + } + return this; + } + + @CanIgnoreReturnValue + public Builder addFileTypes(FileDescriptor... fileDescriptors) { + return addFileTypes(Arrays.asList(fileDescriptors)); + } + + @CanIgnoreReturnValue + public Builder addFileTypes(Iterable fileDescriptors) { + fileTypesBuilder().addAll(fileDescriptors); + return this; + } + + public abstract Builder setExtensionRegistry(ExtensionRegistry extensionRegistry); + public abstract CelTestContext build(); } } diff --git a/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteTextProtoParser.java b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteTextProtoParser.java index 3819e38d2..5e7e62498 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteTextProtoParser.java +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteTextProtoParser.java @@ -22,6 +22,7 @@ import com.google.protobuf.TextFormat; import com.google.protobuf.TextFormat.ParseException; import com.google.protobuf.TypeRegistry; +import dev.cel.common.annotations.Internal; import dev.cel.expr.conformance.test.InputValue; import dev.cel.expr.conformance.test.TestCase; import dev.cel.expr.conformance.test.TestSection; @@ -35,23 +36,40 @@ /** * CelTestSuiteTextProtoParser intakes a textproto document that describes the structure of a CEL * test suite, parses it then creates a {@link CelTestSuite}. + * + *

CEL Library Internals. Do Not Use. */ -final class CelTestSuiteTextProtoParser { +@Internal +public final class CelTestSuiteTextProtoParser { /** Creates a new instance of {@link CelTestSuiteTextProtoParser}. */ - static CelTestSuiteTextProtoParser newInstance() { + public static CelTestSuiteTextProtoParser newInstance() { return new CelTestSuiteTextProtoParser(); } - CelTestSuite parse(String textProto) throws IOException, CelTestSuiteException { - TestSuite testSuite = parseTestSuite(textProto); + public CelTestSuite parse(String textProto) throws IOException, CelTestSuiteException { + return parse( + textProto, TypeRegistry.getEmptyTypeRegistry(), ExtensionRegistry.getEmptyRegistry()); + } + + public CelTestSuite parse(String textProto, TypeRegistry customTypeRegistry) + throws IOException, CelTestSuiteException { + return parse(textProto, customTypeRegistry, ExtensionRegistry.getEmptyRegistry()); + } + + public CelTestSuite parse( + String textProto, TypeRegistry customTypeRegistry, ExtensionRegistry customExtensionRegistry) + throws IOException, CelTestSuiteException { + TestSuite testSuite = parseTestSuite(textProto, customTypeRegistry, customExtensionRegistry); return parseCelTestSuite(testSuite); } - private TestSuite parseTestSuite(String textProto) throws IOException { + private TestSuite parseTestSuite( + String textProto, TypeRegistry customTypeRegistry, ExtensionRegistry customExtensionRegistry) + throws IOException { String fileDescriptorSetPath = System.getProperty("file_descriptor_set_path"); - TypeRegistry typeRegistry = TypeRegistry.getEmptyTypeRegistry(); - ExtensionRegistry extensionRegistry = ExtensionRegistry.getEmptyRegistry(); + TypeRegistry typeRegistry = customTypeRegistry; + ExtensionRegistry extensionRegistry = customExtensionRegistry; if (fileDescriptorSetPath != null) { extensionRegistry = RegistryUtils.getExtensionRegistry(fileDescriptorSetPath); typeRegistry = RegistryUtils.getTypeRegistry(fileDescriptorSetPath); diff --git a/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteYamlParser.java b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteYamlParser.java index d1a3d6615..71c4b9231 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteYamlParser.java +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteYamlParser.java @@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import dev.cel.common.CelIssue; +import dev.cel.common.annotations.Internal; import dev.cel.common.formats.CelFileSource; import dev.cel.common.formats.ParserContext; import dev.cel.common.formats.YamlHelper.YamlNodeType; @@ -43,15 +44,18 @@ /** * CelTestSuiteYamlParser intakes a YAML document that describes the structure of a CEL test suite, * parses it then creates a {@link CelTestSuite}. + * + *

CEL Library Internals. Do Not Use. */ -final class CelTestSuiteYamlParser { +@Internal +public final class CelTestSuiteYamlParser { /** Creates a new instance of {@link CelTestSuiteYamlParser}. */ - static CelTestSuiteYamlParser newInstance() { + public static CelTestSuiteYamlParser newInstance() { return new CelTestSuiteYamlParser(); } - CelTestSuite parse(String celTestSuiteYamlContent) throws CelTestSuiteException { + public CelTestSuite parse(String celTestSuiteYamlContent) throws CelTestSuiteException { return parseYaml(celTestSuiteYamlContent, ""); } @@ -110,6 +114,7 @@ private CelTestSuite.Builder parseTestSuite(ParserContext ctx, Node node) case "description": builder.setDescription(newString(ctx, valueNode)); break; + case "section": case "sections": builder.setSections(parseSections(ctx, valueNode)); break; diff --git a/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java b/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java index a5e912ccb..742b0cfb5 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java +++ b/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java @@ -31,11 +31,13 @@ import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.Message; import com.google.protobuf.TextFormat; +import com.google.protobuf.TypeRegistry; import dev.cel.bundle.Cel; import dev.cel.bundle.CelEnvironment; import dev.cel.bundle.CelEnvironment.ExtensionConfig; import dev.cel.bundle.CelEnvironmentYamlParser; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelDescriptorUtil; import dev.cel.common.CelOptions; import dev.cel.common.CelProtoAbstractSyntaxTree; import dev.cel.common.CelValidationException; @@ -205,6 +207,16 @@ private static Cel extendCel(CelTestContext celTestContext, CelOptions celOption .build(); } + if (!celTestContext.fileTypes().isEmpty()) { + extendedCel = + extendedCel + .toCelBuilder() + .addMessageTypes( + CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(celTestContext.fileTypes()) + .messageTypeDescriptors()) + .build(); + } + CelEnvironment environment = CelEnvironment.newBuilder().build(); // Extend the cel object with the config file if provided. @@ -302,8 +314,15 @@ private static Object getEvaluationResult( return getEvaluationResultWithMessage( getEvaluatedContextExpr(testCase, celTestContext), program, celCoverageIndex); case BINDINGS: - return getEvaluationResultWithBindings( - getBindings(testCase, celTestContext), program, celCoverageIndex); + ImmutableMap bindings = getBindings(testCase, celTestContext); + if (celTestContext.bindingTransformer().isPresent()) { + try { + bindings = celTestContext.bindingTransformer().get().transform(bindings); + } catch (Exception e) { + throw new CelEvaluationException("Binding transformation failed: " + e.getMessage(), e); + } + } + return getEvaluationResultWithBindings(bindings, program, celCoverageIndex); case NO_INPUT: ImmutableMap.Builder newBindings = ImmutableMap.builder(); for (Map.Entry entry : celTestContext.variableBindings().entrySet()) { @@ -396,10 +415,23 @@ private static Object evaluateInput(Cel cel, String expr) private static Object getValueFromBinding(Object value, CelTestContext celTestContext) throws IOException { if (value instanceof Value) { - if (celTestContext.fileDescriptorSetPath().isPresent()) { - return fromValue((Value) value, celTestContext.fileDescriptorSetPath().get()); + if (celTestContext.typeRegistry().isPresent() + || celTestContext.extensionRegistry().isPresent()) { + if (celTestContext.typeRegistry().isPresent()) { + ExtensionRegistry extensionRegistry = + celTestContext.extensionRegistry().orElse(ExtensionRegistry.getEmptyRegistry()); + return fromValue((Value) value, celTestContext.typeRegistry().get(), extensionRegistry); + } else if (celTestContext.extensionRegistry().isPresent()) { + return fromValue( + (Value) value, + TypeRegistry.newBuilder().build(), + celTestContext.extensionRegistry().get()); + } else if (celTestContext.fileDescriptorSetPath().isPresent()) { + return fromValue((Value) value, celTestContext.fileDescriptorSetPath().get()); + } } - return fromValue((Value) value); + return fromValue( + (Value) value, TypeRegistry.newBuilder().build(), ExtensionRegistry.getEmptyRegistry()); } return value; } diff --git a/testing/src/main/java/dev/cel/testing/utils/ExprValueUtils.java b/testing/src/main/java/dev/cel/testing/utils/ExprValueUtils.java index 041c0f52d..9bccecc95 100644 --- a/testing/src/main/java/dev/cel/testing/utils/ExprValueUtils.java +++ b/testing/src/main/java/dev/cel/testing/utils/ExprValueUtils.java @@ -28,8 +28,6 @@ import com.google.protobuf.Message; import com.google.protobuf.NullValue; import com.google.protobuf.TypeRegistry; -import dev.cel.common.CelDescriptorUtil; -import dev.cel.common.CelDescriptors; import dev.cel.common.internal.DefaultInstanceMessageFactory; import dev.cel.common.internal.ProtoTimeUtils; import dev.cel.common.types.CelType; @@ -55,8 +53,6 @@ public final class ExprValueUtils { private ExprValueUtils() {} - public static final TypeRegistry DEFAULT_TYPE_REGISTRY = newDefaultTypeRegistry(); - public static final ExtensionRegistry DEFAULT_EXTENSION_REGISTRY = newDefaultExtensionRegistry(); /** * Converts a {@link Value} to a Java native object using the given file descriptor set to parse @@ -68,10 +64,9 @@ private ExprValueUtils() {} * @throws IOException If there's an error during conversion. */ public static Object fromValue(Value value, String fileDescriptorSetPath) throws IOException { - if (value.getKindCase().equals(Value.KindCase.OBJECT_VALUE)) { - return parseAny(value.getObjectValue(), fileDescriptorSetPath); - } - return toNativeObject(value); + TypeRegistry typeRegistry = RegistryUtils.getTypeRegistry(fileDescriptorSetPath); + ExtensionRegistry extensionRegistry = RegistryUtils.getExtensionRegistry(fileDescriptorSetPath); + return fromValue(value, typeRegistry, extensionRegistry); } /** @@ -81,19 +76,38 @@ public static Object fromValue(Value value, String fileDescriptorSetPath) throws * @return The converted Java object. * @throws IOException If there's an error during conversion. */ - public static Object fromValue(Value value) throws IOException { + + /** + * Converts a {@link Value} to a Java native object using custom registries. + * + * @param value The {@link Value} to convert. + * @param typeRegistry The type registry to use for object resolution. + * @param extensionRegistry The extension registry to use for object resolution. + * @return The converted Java object. + * @throws IOException If there's an error during conversion. + */ + public static Object fromValue( + Value value, TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) + throws IOException { if (value.getKindCase().equals(Value.KindCase.OBJECT_VALUE)) { Descriptor descriptor = - DEFAULT_TYPE_REGISTRY.getDescriptorForTypeUrl(value.getObjectValue().getTypeUrl()); + typeRegistry.getDescriptorForTypeUrl(value.getObjectValue().getTypeUrl()); + if (descriptor == null) { + throw new IOException( + "Unknown type, descriptor was not found in registry: " + + value.getObjectValue().getTypeUrl()); + } Message prototype = getDefaultInstance(descriptor); return prototype .getParserForType() - .parseFrom(value.getObjectValue().getValue(), DEFAULT_EXTENSION_REGISTRY); + .parseFrom(value.getObjectValue().getValue(), extensionRegistry); } - return toNativeObject(value); + return toNativeObject(value, typeRegistry, extensionRegistry); } - private static Object toNativeObject(Value value) throws IOException { + private static Object toNativeObject( + Value value, TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) + throws IOException { switch (value.getKindCase()) { case NULL_VALUE: return dev.cel.common.values.NullValue.NULL_VALUE; @@ -118,7 +132,9 @@ private static Object toNativeObject(Value value) throws IOException { ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(map.getEntriesCount()); for (MapValue.Entry entry : map.getEntriesList()) { - builder.put(fromValue(entry.getKey()), fromValue(entry.getValue())); + builder.put( + fromValue(entry.getKey(), typeRegistry, extensionRegistry), + fromValue(entry.getValue(), typeRegistry, extensionRegistry)); } return builder.buildOrThrow(); } @@ -128,7 +144,7 @@ private static Object toNativeObject(Value value) throws IOException { ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(list.getValuesCount()); for (Value element : list.getValuesList()) { - builder.add(fromValue(element)); + builder.add(fromValue(element, typeRegistry, extensionRegistry)); } return builder.build(); } @@ -181,7 +197,7 @@ public static Value toValue(Object object, CelType type) throws Exception { if (object instanceof dev.cel.expr.Value) { object = Value.parseFrom( - ((dev.cel.expr.Value) object).toByteArray(), DEFAULT_EXTENSION_REGISTRY); + ((dev.cel.expr.Value) object).toByteArray(), ExtensionRegistry.getEmptyRegistry()); } if (object instanceof Value) { return (Value) object; @@ -287,19 +303,6 @@ public static Value toValue(Object object, CelType type) throws Exception { String.format("Unexpected result type: %s", object.getClass())); } - private static Message parseAny(Any value, String fileDescriptorSetPath) throws IOException { - TypeRegistry typeRegistry = RegistryUtils.getTypeRegistry(fileDescriptorSetPath); - ExtensionRegistry extensionRegistry = RegistryUtils.getExtensionRegistry(fileDescriptorSetPath); - Descriptor descriptor = typeRegistry.getDescriptorForTypeUrl(value.getTypeUrl()); - return unpackAny(value, descriptor, extensionRegistry); - } - - private static Message unpackAny( - Any value, Descriptor descriptor, ExtensionRegistry extensionRegistry) throws IOException { - Message defaultInstance = getDefaultInstance(descriptor); - return defaultInstance.getParserForType().parseFrom(value.getValue(), extensionRegistry); - } - private static Message getDefaultInstance(Descriptor descriptor) { return DefaultInstanceMessageFactory.getInstance() .getPrototype(descriptor) @@ -309,20 +312,6 @@ private static Message getDefaultInstance(Descriptor descriptor) { "Could not find a default message for: " + descriptor.getFullName())); } - private static ExtensionRegistry newDefaultExtensionRegistry() { - ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); - dev.cel.expr.conformance.proto2.TestAllTypesExtensions.registerAllExtensions(extensionRegistry); - return extensionRegistry; - } - private static TypeRegistry newDefaultTypeRegistry() { - CelDescriptors allDescriptors = - CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( - ImmutableList.of( - dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor().getFile(), - dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor().getFile())); - - return TypeRegistry.newBuilder().add(allDescriptors.messageTypeDescriptors()).build(); - } } diff --git a/testing/src/test/java/dev/cel/testing/testrunner/TestRunnerLibraryTest.java b/testing/src/test/java/dev/cel/testing/testrunner/TestRunnerLibraryTest.java index d5a5248a8..b83375b35 100644 --- a/testing/src/test/java/dev/cel/testing/testrunner/TestRunnerLibraryTest.java +++ b/testing/src/test/java/dev/cel/testing/testrunner/TestRunnerLibraryTest.java @@ -26,6 +26,7 @@ import dev.cel.common.types.SimpleType; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase; +import java.util.Map; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -281,4 +282,74 @@ public void triggerRunTest_evaluateRawExpr_withCoverage() throws Exception { .build(), celCoverageIndex); } + + @Test + public void runTest_withBindingTransformer() throws Exception { + CelTestCase testCase = + CelTestCase.newBuilder() + .setName("binding_transformer_test") + .setDescription("Test binding transformer") + .setInput( + CelTestCase.Input.ofBindings( + ImmutableMap.of("x", CelTestCase.Input.Binding.ofValue(1L)))) + .setOutput(CelTestCase.Output.ofResultValue(3L)) // 1 + 1 (transformed) + 1 (expr) = 3 + .build(); + + TestRunnerLibrary.evaluateTestCase( + testCase, + CelTestContext.newBuilder() + .setCelExpression(CelExpressionSource.fromRawExpr("x + 1")) + .setCel(CelFactory.standardCelBuilder().addVar("x", SimpleType.INT).build()) + .setBindingTransformer( + bindings -> { + ImmutableMap.Builder transformed = ImmutableMap.builder(); + for (Map.Entry entry : bindings.entrySet()) { + if (entry.getKey().equals("x")) { + transformed.put("x", (Long) entry.getValue() + 1L); + } else { + transformed.put(entry); + } + } + return transformed.buildOrThrow(); + }) + .build()); + } + + @Test + public void runTest_withMessageTypes() throws Exception { + CelTestCase testCase = + CelTestCase.newBuilder() + .setName("message_types_consolidation_test") + .setDescription("Test message types consolidation") + .setOutput(CelTestCase.Output.ofResultValue(true)) + .build(); + + TestRunnerLibrary.evaluateTestCase( + testCase, + CelTestContext.newBuilder() + .setCelExpression( + CelExpressionSource.fromRawExpr( + "cel.expr.conformance.proto3.TestAllTypes{single_int64: 1} ==" + + " cel.expr.conformance.proto3.TestAllTypes{single_int64: 1}")) + .addMessageTypes(TestAllTypes.getDescriptor()) + .build()); + } + + @Test + public void typeRegistry_withFileTypes() throws Exception { + CelTestContext celTestContext = + CelTestContext.newBuilder() + .setCelExpression(CelExpressionSource.fromRawExpr("true")) + .setCel(CelFactory.standardCelBuilder().build()) + .addMessageTypes(TestAllTypes.getDescriptor()) + .build(); + + assertThat( + celTestContext + .typeRegistry() + .get() + .find("cel.expr.conformance.proto3.TestAllTypes") + .getFullName()) + .isEqualTo("cel.expr.conformance.proto3.TestAllTypes"); + } } From 13568dfba2d0dc424b56183579bf0fe114024397 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 27 Mar 2026 14:10:01 -0700 Subject: [PATCH 024/204] Internal Changes PiperOrigin-RevId: 890628970 --- policy/src/main/java/dev/cel/policy/CelPolicy.java | 13 +++++++++---- .../dev/cel/testing/testrunner/CelTestSuite.java | 5 +++-- .../cel/testing/testrunner/TestRunnerLibrary.java | 7 +++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/policy/src/main/java/dev/cel/policy/CelPolicy.java b/policy/src/main/java/dev/cel/policy/CelPolicy.java index 9980d0cad..9e442a2e7 100644 --- a/policy/src/main/java/dev/cel/policy/CelPolicy.java +++ b/policy/src/main/java/dev/cel/policy/CelPolicy.java @@ -27,6 +27,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -77,8 +78,7 @@ public abstract static class Builder { public abstract Builder setPolicySource(CelPolicySource policySource); - // This should stay package-private to encourage add/set methods to be used instead. - abstract ImmutableMap.Builder metadataBuilder(); + private final HashMap metadata = new HashMap<>(); public abstract Builder setMetadata(ImmutableMap value); @@ -90,6 +90,10 @@ public List imports() { return Collections.unmodifiableList(importList); } + public Map metadata() { + return Collections.unmodifiableMap(metadata); + } + @CanIgnoreReturnValue public Builder addImport(Import value) { importList.add(value); @@ -104,13 +108,13 @@ public Builder addImports(Collection values) { @CanIgnoreReturnValue public Builder putMetadata(String key, Object value) { - metadataBuilder().put(key, value); + metadata.put(key, value); return this; } @CanIgnoreReturnValue public Builder putMetadata(Map map) { - metadataBuilder().putAll(map); + metadata.putAll(map); return this; } @@ -118,6 +122,7 @@ public Builder putMetadata(Map map) { public CelPolicy build() { setImports(ImmutableList.copyOf(importList)); + setMetadata(ImmutableMap.copyOf(metadata)); return autoBuild(); } } diff --git a/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuite.java b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuite.java index e6086f128..a8869a8fb 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuite.java +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuite.java @@ -93,7 +93,7 @@ public abstract static class Builder { public abstract Builder toBuilder(); public static Builder newBuilder() { - return new AutoValue_CelTestSuite_CelTestSection.Builder(); + return new AutoValue_CelTestSuite_CelTestSection.Builder().setDescription(""); } /** Class representing a CEL test case within a test section. */ @@ -237,7 +237,8 @@ public abstract static class Builder { public static Builder newBuilder() { return new AutoValue_CelTestSuite_CelTestSection_CelTestCase.Builder() - .setInput(Input.ofNoInput()); // Default input to no input. + .setInput(Input.ofNoInput()) // Default input to no input. + .setDescription(""); } } } diff --git a/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java b/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java index 742b0cfb5..2465d330e 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java +++ b/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java @@ -106,6 +106,13 @@ public static void runTest( } } + /** Runs the test with the provided AST. */ + public static void runTest( + CelAbstractSyntaxTree ast, CelTestCase testCase, CelTestContext celTestContext) + throws Exception { + evaluate(ast, testCase, celTestContext, /* celCoverageIndex= */ null); + } + @VisibleForTesting static void evaluateTestCase(CelTestCase testCase, CelTestContext celTestContext) throws Exception { From 75fafc91fee010ca1a7c719d07df408bad265b88 Mon Sep 17 00:00:00 2001 From: "Philip K. Warren" Date: Fri, 27 Mar 2026 16:20:13 -0500 Subject: [PATCH 025/204] Add string extensions quote and reverse Add `strings.quote` and `reverse` extensions to match Go implementations. --- .../cel/extensions/CelStringExtensions.java | 76 +++++++++++++++++++ .../extensions/CelStringExtensionsTest.java | 54 +++++++++++++ 2 files changed, 130 insertions(+) diff --git a/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java index 10caa7db8..faf30c2b2 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java @@ -137,6 +137,17 @@ public enum Function { SimpleType.STRING, SimpleType.STRING)), CelFunctionBinding.from("string_lower_ascii", String.class, Ascii::toLowerCase)), + QUOTE( + CelFunctionDecl.newFunctionDeclaration( + "strings.quote", + CelOverloadDecl.newGlobalOverload( + "strings_quote", + "Takes the given string and makes it safe to print (without any formatting" + + " due to escape sequences). If any invalid UTF-8 characters are" + + " encountered, they are replaced with \\uFFFD.", + SimpleType.STRING, + ImmutableList.of(SimpleType.STRING))), + CelFunctionBinding.from("strings_quote", String.class, CelStringExtensions::quote)), REPLACE( CelFunctionDecl.newFunctionDeclaration( "replace", @@ -164,6 +175,16 @@ public enum Function { "string_replace_string_string_int", ImmutableList.of(String.class, String.class, String.class, Long.class), CelStringExtensions::replace)), + REVERSE( + CelFunctionDecl.newFunctionDeclaration( + "reverse", + CelOverloadDecl.newMemberOverload( + "string_reverse", + "Returns a new string whose characters are the same as the target string," + + " only formatted in reverse order.", + SimpleType.STRING, + SimpleType.STRING)), + CelFunctionBinding.from("string_reverse", String.class, CelStringExtensions::reverse)), SPLIT( CelFunctionDecl.newFunctionDeclaration( "split", @@ -449,6 +470,57 @@ private static Long lastIndexOf(CelCodePointArray str, CelCodePointArray substr, return -1L; } + private static String quote(String s) { + StringBuilder sb = new StringBuilder(s.length() + 2); + sb.append('"'); + for (int i = 0; i < s.length(); ) { + int codePoint = s.codePointAt(i); + if (!Character.isValidCodePoint(codePoint) + || Character.isLowSurrogate(s.charAt(i)) + || (Character.isHighSurrogate(s.charAt(i)) + && (i + 1 >= s.length() || !Character.isLowSurrogate(s.charAt(i + 1))))) { + sb.append('\uFFFD'); + i++; + continue; + } + switch (codePoint) { + case '\u0007': + sb.append("\\a"); + break; + case '\b': + sb.append("\\b"); + break; + case '\f': + sb.append("\\f"); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + case '\u000B': + sb.append("\\v"); + break; + case '\\': + sb.append("\\\\"); + break; + case '"': + sb.append("\\\""); + break; + default: + sb.appendCodePoint(codePoint); + break; + } + i += Character.charCount(codePoint); + } + sb.append('"'); + return sb.toString(); + } + private static String replaceAll(Object[] objects) { return replace((String) objects[0], (String) objects[1], (String) objects[2], -1); } @@ -504,6 +576,10 @@ private static String replace(String text, String searchString, String replaceme return sb.append(textCpa.slice(start, textCpa.length())).toString(); } + private static String reverse(String s) { + return new StringBuilder(s).reverse().toString(); + } + private static List split(String str, String separator) { return split(str, separator, Integer.MAX_VALUE); } diff --git a/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java index 6ea9b702c..ad0d6d679 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java @@ -70,7 +70,9 @@ public void library() { "lastIndexOf", "lowerAscii", "replace", + "reverse", "split", + "strings.quote", "substring", "trim", "upperAscii"); @@ -1467,6 +1469,58 @@ public void stringExtension_functionSubset_success() throws Exception { assertThat(evaluatedResult).isEqualTo(true); } + @Test + @TestParameters("{string: 'abcd', expectedResult: 'dcba'}") + @TestParameters("{string: '', expectedResult: ''}") + @TestParameters("{string: 'a', expectedResult: 'a'}") + @TestParameters("{string: 'hello world', expectedResult: 'dlrow olleh'}") + @TestParameters("{string: 'ab가cd', expectedResult: 'dc가ba'}") + public void reverse_success(String string, String expectedResult) throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile("s.reverse()").getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + Object evaluatedResult = program.eval(ImmutableMap.of("s", string)); + + assertThat(evaluatedResult).isEqualTo(expectedResult); + } + + @Test + public void reverse_unicode() throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile("s.reverse()").getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + Object evaluatedResult = program.eval(ImmutableMap.of("s", "😁😑😦")); + + assertThat(evaluatedResult).isEqualTo("😦😑😁"); + } + + @Test + @TestParameters("{string: 'hello', expectedResult: '\"hello\"'}") + @TestParameters("{string: '', expectedResult: '\"\"'}") + @TestParameters("{string: 'contains \\\"quotes\\\"', expectedResult: '\"contains \\\\\\\"quotes\\\\\\\"\"'}") + public void quote_success(String string, String expectedResult) throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile("strings.quote(s)").getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + Object evaluatedResult = program.eval(ImmutableMap.of("s", string)); + + assertThat(evaluatedResult).isEqualTo(expectedResult); + } + + @Test + public void quote_escapesSpecialCharacters() throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile("strings.quote(s)").getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + Object evaluatedResult = + program.eval( + ImmutableMap.of( + "s", "\u0007bell\u000Bvtab\bback\ffeed\rret\nline\ttab\\slash 가 😁")); + + assertThat(evaluatedResult) + .isEqualTo("\"\\abell\\vvtab\\bback\\ffeed\\rret\\nline\\ttab\\\\slash 가 😁\""); + } + @Test public void stringExtension_compileUnallowedFunction_throws() { CelCompiler celCompiler = From 6207811efff2d3b1d3dd5f0528e67906a90a955c Mon Sep 17 00:00:00 2001 From: "Philip K. Warren" Date: Fri, 27 Mar 2026 17:48:43 -0500 Subject: [PATCH 026/204] Enable strings.quote conformance tests --- conformance/src/test/java/dev/cel/conformance/BUILD.bazel | 2 -- 1 file changed, 2 deletions(-) diff --git a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel index fb2b1a159..ea9041433 100644 --- a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -120,7 +120,6 @@ _TESTS_TO_SKIP_LEGACY = [ # Skip until fixed. "fields/qualified_identifier_resolution/map_value_repeat_key_heterogeneous", # TODO: Add strings.format and strings.quote. - "string_ext/quote", "string_ext/format", "string_ext/format_errors", @@ -149,7 +148,6 @@ _TESTS_TO_SKIP_LEGACY = [ _TESTS_TO_SKIP_PLANNER = [ # TODO: Add strings.format and strings.quote. - "string_ext/quote", "string_ext/format", "string_ext/format_errors", From 0e1a30f5eb31d0f1a313a755d253ca0f1a3389d8 Mon Sep 17 00:00:00 2001 From: "Philip K. Warren" Date: Fri, 27 Mar 2026 17:55:53 -0500 Subject: [PATCH 027/204] Fix comments in bazel TESTS_TO_SKIP --- conformance/src/test/java/dev/cel/conformance/BUILD.bazel | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel index ea9041433..c0e7ad2bc 100644 --- a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -119,7 +119,7 @@ _TESTS_TO_SKIP_LEGACY = [ # Skip until fixed. "fields/qualified_identifier_resolution/map_value_repeat_key_heterogeneous", - # TODO: Add strings.format and strings.quote. + # TODO: Add strings.format. "string_ext/format", "string_ext/format_errors", @@ -147,7 +147,7 @@ _TESTS_TO_SKIP_LEGACY = [ ] _TESTS_TO_SKIP_PLANNER = [ - # TODO: Add strings.format and strings.quote. + # TODO: Add strings.format. "string_ext/format", "string_ext/format_errors", From 57e795ad58d77dd462864a8e322fecd4a56514be Mon Sep 17 00:00:00 2001 From: "Philip K. Warren" Date: Fri, 27 Mar 2026 18:13:28 -0500 Subject: [PATCH 028/204] Attempt to fix failing test --- .../src/test/java/dev/cel/extensions/CelExtensionsTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java index 61922f70f..192630ea3 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java @@ -168,6 +168,7 @@ public void getAllFunctionNames() { "join", "lastIndexOf", "lowerAscii", + "strings.quote", "replace", "split", "substring", From 943ae333723ed1ffe05975cdd4437d1b35a546fe Mon Sep 17 00:00:00 2001 From: "Philip K. Warren" Date: Sun, 29 Mar 2026 10:43:53 -0500 Subject: [PATCH 029/204] Add additional tests and address review feedback --- .../cel/extensions/CelStringExtensions.java | 18 +++++++-- .../extensions/CelStringExtensionsTest.java | 37 +++++++++++++++++-- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java index faf30c2b2..e89b81071 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java @@ -475,10 +475,7 @@ private static String quote(String s) { sb.append('"'); for (int i = 0; i < s.length(); ) { int codePoint = s.codePointAt(i); - if (!Character.isValidCodePoint(codePoint) - || Character.isLowSurrogate(s.charAt(i)) - || (Character.isHighSurrogate(s.charAt(i)) - && (i + 1 >= s.length() || !Character.isLowSurrogate(s.charAt(i + 1))))) { + if (isMalformedUtf16(s, i, codePoint)) { sb.append('\uFFFD'); i++; continue; @@ -521,6 +518,19 @@ private static String quote(String s) { return sb.toString(); } + private static boolean isMalformedUtf16(String s, int index, int codePoint) { + char currentChar = s.charAt(index); + if (!Character.isValidCodePoint(codePoint)) { + return true; + } + if (Character.isLowSurrogate(currentChar)) { + return true; + } + // Check for unpaired high surrogate + return Character.isHighSurrogate(currentChar) + && (index + 1 >= s.length() || !Character.isLowSurrogate(s.charAt(index + 1))); + } + private static String replaceAll(Object[] objects) { return replace((String) objects[0], (String) objects[1], (String) objects[2], -1); } diff --git a/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java index ad0d6d679..58a1bff99 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java @@ -33,6 +33,8 @@ import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelRuntime; import dev.cel.runtime.CelRuntimeFactory; + +import java.nio.charset.StandardCharsets; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; @@ -1485,19 +1487,23 @@ public void reverse_success(String string, String expectedResult) throws Excepti } @Test - public void reverse_unicode() throws Exception { + @TestParameters("{string: '😁😑😦', expectedResult: '😦😑😁'}") + @TestParameters("{string: '\u180e\u200b\u200c\u200d\u2060\ufeff', expectedResult: '\ufeff\u2060\u200d\u200c\u200b\u180e'}") + public void reverse_unicode(String string, String expectedResult) throws Exception { CelAbstractSyntaxTree ast = COMPILER.compile("s.reverse()").getAst(); CelRuntime.Program program = RUNTIME.createProgram(ast); - Object evaluatedResult = program.eval(ImmutableMap.of("s", "😁😑😦")); + Object evaluatedResult = program.eval(ImmutableMap.of("s", string)); - assertThat(evaluatedResult).isEqualTo("😦😑😁"); + assertThat(evaluatedResult).isEqualTo(expectedResult); } @Test @TestParameters("{string: 'hello', expectedResult: '\"hello\"'}") @TestParameters("{string: '', expectedResult: '\"\"'}") @TestParameters("{string: 'contains \\\"quotes\\\"', expectedResult: '\"contains \\\\\\\"quotes\\\\\\\"\"'}") + @TestParameters("{string: 'ends with \\\\', expectedResult: '\"ends with \\\\\\\\\"'}") + @TestParameters("{string: '\\\\ starts with', expectedResult: '\"\\\\\\\\ starts with\"'}") public void quote_success(String string, String expectedResult) throws Exception { CelAbstractSyntaxTree ast = COMPILER.compile("strings.quote(s)").getAst(); CelRuntime.Program program = RUNTIME.createProgram(ast); @@ -1507,6 +1513,18 @@ public void quote_success(String string, String expectedResult) throws Exception assertThat(evaluatedResult).isEqualTo(expectedResult); } + @Test + public void quote_singleWithDoubleQuotes() throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile( + "strings.quote('single-quote with \"double quote\"') == \"\\\"single-quote with \\\\\\\"double quote\\\\\\\"\\\"\"" + ).getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + Object evaluatedResult = program.eval(); + + assertThat(evaluatedResult).isEqualTo(true); + } + @Test public void quote_escapesSpecialCharacters() throws Exception { CelAbstractSyntaxTree ast = COMPILER.compile("strings.quote(s)").getAst(); @@ -1521,6 +1539,19 @@ public void quote_escapesSpecialCharacters() throws Exception { .isEqualTo("\"\\abell\\vvtab\\bback\\ffeed\\rret\\nline\\ttab\\\\slash 가 😁\""); } + @Test + public void quote_escapesMalformed() throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile("strings.quote(s)").getAst(); + CelRuntime.Program program = RUNTIME.createProgram(ast); + + Object evaluatedResult = + program.eval( + ImmutableMap.of( + "s", new String(new byte[]{'f','i','l','l','e','r',' ',(byte)0x9f}, StandardCharsets.UTF_8))); + + assertThat(evaluatedResult).isEqualTo("\"filler \uFFFD\""); + } + @Test public void stringExtension_compileUnallowedFunction_throws() { CelCompiler celCompiler = From 03d0a3621f721e05bbd5eecf6234a588246a397f Mon Sep 17 00:00:00 2001 From: "Philip K. Warren" Date: Sun, 29 Mar 2026 11:57:43 -0500 Subject: [PATCH 030/204] Additional malformed unicode tests --- .../extensions/CelStringExtensionsTest.java | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java index 58a1bff99..0a3c595be 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java @@ -1540,16 +1540,28 @@ public void quote_escapesSpecialCharacters() throws Exception { } @Test - public void quote_escapesMalformed() throws Exception { + @TestParameters({"{rawString: !!binary 'ZmlsbGVyIJ8=', expectedResult: '\"filler \uFFFD\"'}"}) // "filler \x9f" + public void quote_escapesMalformed(byte[] rawString, String expectedResult) throws Exception { CelAbstractSyntaxTree ast = COMPILER.compile("strings.quote(s)").getAst(); CelRuntime.Program program = RUNTIME.createProgram(ast); - Object evaluatedResult = - program.eval( - ImmutableMap.of( - "s", new String(new byte[]{'f','i','l','l','e','r',' ',(byte)0x9f}, StandardCharsets.UTF_8))); + Object evaluatedResult = program.eval(ImmutableMap.of("s", new String(rawString, StandardCharsets.UTF_8))); + + assertThat(evaluatedResult).isEqualTo(expectedResult); + } - assertThat(evaluatedResult).isEqualTo("\"filler \uFFFD\""); + @Test + public void quote_escapesMalformed_endWithHighSurrogate() throws Exception { + CelRuntime.Program program = RUNTIME.createProgram(COMPILER.compile("strings.quote(s)").getAst()); + assertThat(program.eval(ImmutableMap.of("s", "end with high surrogate \uD83D"))) + .isEqualTo("\"end with high surrogate \uFFFD\""); + } + + @Test + public void quote_escapesMalformed_unpairedHighSurrogate() throws Exception { + CelRuntime.Program program = RUNTIME.createProgram(COMPILER.compile("strings.quote(s)").getAst()); + assertThat(program.eval(ImmutableMap.of("s", "bad pair \uD83DA"))) + .isEqualTo("\"bad pair \uFFFDA\""); } @Test From 79ff206b89f6b54767e4dd84f648b049d560ea89 Mon Sep 17 00:00:00 2001 From: "Philip K. Warren" Date: Sun, 29 Mar 2026 13:53:13 -0500 Subject: [PATCH 031/204] Add quote and reverse functions to docs --- .../main/java/dev/cel/extensions/README.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/extensions/src/main/java/dev/cel/extensions/README.md b/extensions/src/main/java/dev/cel/extensions/README.md index 10c5217e8..c3fbf8c54 100644 --- a/extensions/src/main/java/dev/cel/extensions/README.md +++ b/extensions/src/main/java/dev/cel/extensions/README.md @@ -474,6 +474,19 @@ Examples: 'TacoCat'.lowerAscii() // returns 'tacocat' 'TacoCÆt Xii'.lowerAscii() // returns 'tacocÆt xii' +### Quote + +Takes the given string and makes it safe to print (without any formatting due +to escape sequences). +If any invalid UTF-8 characters are encountered, they are replaced with \uFFFD. + + strings.quote() + +Examples: + + strings.quote('single-quote with "double quote"') // returns '"single-quote with \"double quote\""' + strings.quote("two escape sequences \a\n") // returns '"two escape sequences \\a\\n"' + ### Replace Returns a new string based on the target, which replaces the occurrences of a @@ -493,6 +506,20 @@ Examples: 'hello hello'.replace('he', 'we', 1) // returns 'wello hello' 'hello hello'.replace('he', 'we', 0) // returns 'hello hello' +### Reverse + +Returns a new string whose characters are the same as the target string, only +formatted in reverse order. +This function relies on converting strings to Unicode code point arrays in +order to reverse. + + .reverse() -> + +Examples: + + 'gums'.reverse() // returns 'smug' + 'John Smith'.reverse() // returns 'htimS nhoJ' + ### Split Returns a mutable list of strings split from the input by the given separator. The From 576064d30bf241a96fdacda3660d5067d6da8f92 Mon Sep 17 00:00:00 2001 From: Tristan Swadell Date: Mon, 30 Mar 2026 10:53:04 -0700 Subject: [PATCH 032/204] Enable quoted identifiers by default PiperOrigin-RevId: 891799347 --- common/src/main/java/dev/cel/common/CelOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/dev/cel/common/CelOptions.java b/common/src/main/java/dev/cel/common/CelOptions.java index 0c348c8a8..c39e0fea8 100644 --- a/common/src/main/java/dev/cel/common/CelOptions.java +++ b/common/src/main/java/dev/cel/common/CelOptions.java @@ -139,7 +139,7 @@ public static Builder newBuilder() { .retainRepeatedUnaryOperators(false) .retainUnbalancedLogicalExpressions(false) .enableHiddenAccumulatorVar(true) - .enableQuotedIdentifierSyntax(false) + .enableQuotedIdentifierSyntax(true) // Type-Checker options .enableCompileTimeOverloadResolution(false) .enableHomogeneousLiterals(false) From aaec509f18bcf65193188a0791d6c8980ef53316 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 30 Mar 2026 15:13:03 -0700 Subject: [PATCH 033/204] Switch from enhanced for-loop to indexed one to improve comprehension performance PiperOrigin-RevId: 891934916 --- .../dev/cel/runtime/planner/NamespacedAttribute.java | 10 ++++++---- .../dev/cel/runtime/planner/RelativeAttribute.java | 5 +++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java index ed37eada1..d51336d80 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java @@ -77,8 +77,9 @@ public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { if (partialVars != null) { ImmutableList patterns = partialVars.unknowns(); - for (Qualifier qualifier : qualifiers) { - attr = attr.qualify(CelAttribute.Qualifier.fromGeneric(qualifier.value())); + // Avoid enhanced for loop to prevent UnmodifiableIterator from being allocated + for (int i = 0; i < qualifiers.size(); i++) { + attr = attr.qualify(CelAttribute.Qualifier.fromGeneric(qualifiers.get(i).value())); } CelAttributePattern partialMatch = findPartialMatchingPattern(attr, patterns).orElse(null); @@ -178,8 +179,9 @@ private static Object applyQualifiers( Object value, CelValueConverter celValueConverter, ImmutableList qualifiers) { Object obj = celValueConverter.toRuntimeValue(value); - for (Qualifier qualifier : qualifiers) { - obj = qualifier.qualify(obj); + // Avoid enhanced for loop to prevent UnmodifiableIterator from being allocated + for (int i = 0; i < qualifiers.size(); i++) { + obj = qualifiers.get(i).qualify(obj); } return celValueConverter.maybeUnwrap(obj); diff --git a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java index 1ab2fa3e7..addbeb4d0 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java @@ -40,8 +40,9 @@ public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { obj = celValueConverter.toRuntimeValue(obj); - for (Qualifier qualifier : qualifiers) { - obj = qualifier.qualify(obj); + // Avoid enhanced for loop to prevent UnmodifiableIterator from being allocated + for (int i = 0; i < qualifiers.size(); i++) { + obj = qualifiers.get(i).qualify(obj); } return celValueConverter.maybeUnwrap(obj); From 6ca2c4d48013bbe7d96fe5e72657f0e0330b7179 Mon Sep 17 00:00:00 2001 From: "Philip K. Warren" Date: Mon, 30 Mar 2026 21:30:22 -0500 Subject: [PATCH 034/204] Remove test and fix line length --- .../cel/extensions/CelStringExtensionsTest.java | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java index 0a3c595be..27152191b 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java @@ -1515,9 +1515,9 @@ public void quote_success(String string, String expectedResult) throws Exception @Test public void quote_singleWithDoubleQuotes() throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile( - "strings.quote('single-quote with \"double quote\"') == \"\\\"single-quote with \\\\\\\"double quote\\\\\\\"\\\"\"" - ).getAst(); + String expr = "strings.quote('single-quote with \"double quote\"')"; + String expected = "\"\\\"single-quote with \\\\\\\"double quote\\\\\\\"\\\"\""; + CelAbstractSyntaxTree ast = COMPILER.compile(expr + " == " + expected).getAst(); CelRuntime.Program program = RUNTIME.createProgram(ast); Object evaluatedResult = program.eval(); @@ -1539,17 +1539,6 @@ public void quote_escapesSpecialCharacters() throws Exception { .isEqualTo("\"\\abell\\vvtab\\bback\\ffeed\\rret\\nline\\ttab\\\\slash 가 😁\""); } - @Test - @TestParameters({"{rawString: !!binary 'ZmlsbGVyIJ8=', expectedResult: '\"filler \uFFFD\"'}"}) // "filler \x9f" - public void quote_escapesMalformed(byte[] rawString, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("strings.quote(s)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(ImmutableMap.of("s", new String(rawString, StandardCharsets.UTF_8))); - - assertThat(evaluatedResult).isEqualTo(expectedResult); - } - @Test public void quote_escapesMalformed_endWithHighSurrogate() throws Exception { CelRuntime.Program program = RUNTIME.createProgram(COMPILER.compile("strings.quote(s)").getAst()); From cbe0104bac6fd115bdc46f06faca6fdf5462d225 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 31 Mar 2026 13:26:56 -0700 Subject: [PATCH 035/204] Optimize unary and binary function calls to avoid array allocation PiperOrigin-RevId: 892511211 --- .../dev/cel/runtime/CelFunctionBinding.java | 29 ++++++- .../dev/cel/runtime/CelFunctionOverload.java | 61 +++++++++++---- .../dev/cel/runtime/DefaultDispatcher.java | 66 +++++++++------- .../dev/cel/runtime/FunctionBindingImpl.java | 29 +++++++ .../java/dev/cel/runtime/planner/BUILD.bazel | 16 ++++ .../dev/cel/runtime/planner/EvalBinary.java | 75 +++++++++++++++++++ .../dev/cel/runtime/planner/EvalHelpers.java | 45 ++++++++--- .../dev/cel/runtime/planner/EvalUnary.java | 4 +- .../cel/runtime/planner/ProgramPlanner.java | 3 + 9 files changed, 271 insertions(+), 57 deletions(-) create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java diff --git a/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java index c7b63926b..06e5facdf 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java @@ -54,7 +54,20 @@ public interface CelFunctionBinding { @SuppressWarnings("unchecked") static CelFunctionBinding from( String overloadId, Class arg, CelFunctionOverload.Unary impl) { - return from(overloadId, ImmutableList.of(arg), (args) -> impl.apply((T) args[0])); + return from( + overloadId, + ImmutableList.of(arg), + new CelFunctionOverload() { + @Override + public Object apply(Object[] args) throws CelEvaluationException { + return impl.apply((T) args[0]); + } + + @Override + public Object apply(Object arg1) throws CelEvaluationException { + return impl.apply((T) arg1); + } + }); } /** @@ -65,7 +78,19 @@ static CelFunctionBinding from( static CelFunctionBinding from( String overloadId, Class arg1, Class arg2, CelFunctionOverload.Binary impl) { return from( - overloadId, ImmutableList.of(arg1, arg2), (args) -> impl.apply((T1) args[0], (T2) args[1])); + overloadId, + ImmutableList.of(arg1, arg2), + new CelFunctionOverload() { + @Override + public Object apply(Object[] args) throws CelEvaluationException { + return impl.apply((T1) args[0], (T2) args[1]); + } + + @Override + public Object apply(Object arg1, Object arg2) throws CelEvaluationException { + return impl.apply((T1) arg1, (T2) arg2); + } + }); } /** Create a function binding from the {@code overloadId}, {@code argTypes}, and {@code impl}. */ diff --git a/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java index 3e30a2146..e1bdbf886 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java @@ -26,6 +26,16 @@ public interface CelFunctionOverload { /** Evaluate a set of arguments throwing a {@code CelException} on error. */ Object apply(Object[] args) throws CelEvaluationException; + /** Fast-path for unary function execution to avoid Object[] allocation. */ + default Object apply(Object arg) throws CelEvaluationException { + return apply(new Object[] {arg}); + } + + /** Fast-path for binary function execution to avoid Object[] allocation. */ + default Object apply(Object arg1, Object arg2) throws CelEvaluationException { + return apply(new Object[] {arg1, arg2}); + } + /** * Helper interface for describing unary functions where the type-parameter is used to improve * compile-time correctness of function bindings. @@ -57,27 +67,46 @@ static boolean canHandle( for (int i = 0; i < parameterTypes.size(); i++) { Class paramType = parameterTypes.get(i); Object arg = arguments[i]; - if (arg == null) { - // null can be assigned to messages, maps, and to objects. - // TODO: Remove null special casing - if (paramType != Object.class && !Map.class.isAssignableFrom(paramType)) { - return false; - } - continue; + boolean result = canHandleArg(arg, paramType, isStrict); + if (!result) { + return false; } + } + return true; + } - if (arg instanceof Exception || arg instanceof CelUnknownSet) { - // Only non-strict functions can accept errors/unknowns as arguments to a function - if (!isStrict) { - // Skip assignability check below, but continue to validate remaining args - continue; - } - } + static boolean canHandle(Object arg, ImmutableList> parameterTypes, boolean isStrict) { + if (parameterTypes.size() != 1) { + return false; + } + return canHandleArg(arg, parameterTypes.get(0), isStrict); + } + + static boolean canHandle( + Object arg1, Object arg2, ImmutableList> parameterTypes, boolean isStrict) { + if (parameterTypes.size() != 2) { + return false; + } + return canHandleArg(arg1, parameterTypes.get(0), isStrict) + && canHandleArg(arg2, parameterTypes.get(1), isStrict); + } - if (!paramType.isAssignableFrom(arg.getClass())) { + static boolean canHandleArg(Object arg, Class paramType, boolean isStrict) { + // null can be assigned to messages, maps, and to objects. + // TODO: Remove null special casing + if (arg == null) { + if (paramType != Object.class && !Map.class.isAssignableFrom(paramType)) { return false; } + return true; } - return true; + + if (arg instanceof Exception || arg instanceof CelUnknownSet) { + if (!isStrict) { + return true; + } + } + + return paramType.isAssignableFrom(arg.getClass()); } } diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java index 35e3b76a3..87cb07945 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java @@ -200,19 +200,48 @@ public DefaultDispatcher build() { for (Map.Entry entry : overloads.entrySet()) { String overloadId = entry.getKey(); OverloadEntry overloadEntry = entry.getValue(); + CelFunctionOverload overloadImpl = overloadEntry.overload(); + + CelFunctionOverload guardedApply; + if (overloadImpl instanceof DynamicDispatchOverload) { + // Dynamic dispatcher already does its own internal canHandle checks + guardedApply = overloadImpl; + } else { + boolean isStrict = overloadEntry.isStrict(); + ImmutableList> argTypes = overloadEntry.argTypes(); + + guardedApply = + new CelFunctionOverload() { + @Override + public Object apply(Object[] args) throws CelEvaluationException { + if (CelFunctionOverload.canHandle(args, argTypes, isStrict)) { + return overloadImpl.apply(args); + } + throw new CelOverloadNotFoundException(overloadId); + } + + @Override + public Object apply(Object arg) throws CelEvaluationException { + if (CelFunctionOverload.canHandle(arg, argTypes, isStrict)) { + return overloadImpl.apply(arg); + } + throw new CelOverloadNotFoundException(overloadId); + } + + @Override + public Object apply(Object arg1, Object arg2) throws CelEvaluationException { + if (CelFunctionOverload.canHandle(arg1, arg2, argTypes, isStrict)) { + return overloadImpl.apply(arg1, arg2); + } + throw new CelOverloadNotFoundException(overloadId); + } + }; + } + resolvedOverloads.put( overloadId, CelResolvedOverload.of( - overloadId, - args -> - guardedOp( - overloadId, - args, - overloadEntry.argTypes(), - overloadEntry.isStrict(), - overloadEntry.overload()), - overloadEntry.isStrict(), - overloadEntry.argTypes())); + overloadId, guardedApply, overloadEntry.isStrict(), overloadEntry.argTypes())); } return new DefaultDispatcher(resolvedOverloads.buildOrThrow()); @@ -223,23 +252,6 @@ private Builder() { } } - /** Creates an invocation guard around the overload definition. */ - private static Object guardedOp( - String functionName, - Object[] args, - ImmutableList> argTypes, - boolean isStrict, - CelFunctionOverload overload) - throws CelEvaluationException { - // Argument checking for DynamicDispatch is handled inside the overload's apply method itself. - if (overload instanceof DynamicDispatchOverload - || CelFunctionOverload.canHandle(args, argTypes, isStrict)) { - return overload.apply(args); - } - - throw new CelOverloadNotFoundException(functionName); - } - DefaultDispatcher(ImmutableMap overloads) { this.overloads = overloads; } diff --git a/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java b/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java index faea853f8..1f47f1dfd 100644 --- a/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java @@ -145,6 +145,35 @@ public Object apply(Object[] args) throws CelEvaluationException { .collect(toImmutableList())); } + @Override + public Object apply(Object arg) throws CelEvaluationException { + for (CelFunctionBinding overload : overloadBindings) { + if (CelFunctionOverload.canHandle(arg, overload.getArgTypes(), overload.isStrict())) { + return overload.getDefinition().apply(arg); + } + } + throw new CelOverloadNotFoundException( + functionName, + overloadBindings.stream() + .map(CelFunctionBinding::getOverloadId) + .collect(toImmutableList())); + } + + @Override + public Object apply(Object arg1, Object arg2) throws CelEvaluationException { + for (CelFunctionBinding overload : overloadBindings) { + if (CelFunctionOverload.canHandle( + arg1, arg2, overload.getArgTypes(), overload.isStrict())) { + return overload.getDefinition().apply(arg1, arg2); + } + } + throw new CelOverloadNotFoundException( + functionName, + overloadBindings.stream() + .map(CelFunctionBinding::getOverloadId) + .collect(toImmutableList())); + } + ImmutableSet getOverloadBindings() { return overloadBindings; } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index 3c18b192f..fc70118e4 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -17,6 +17,7 @@ java_library( ":error_metadata", ":eval_and", ":eval_attribute", + ":eval_binary", ":eval_conditional", ":eval_const", ":eval_create_list", @@ -232,6 +233,21 @@ java_library( ], ) +java_library( + name = "eval_binary", + srcs = ["EvalBinary.java"], + deps = [ + ":eval_helpers", + ":execution_frame", + ":planned_interpretable", + "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "//runtime:resolved_overload", + ], +) + java_library( name = "eval_var_args_call", srcs = ["EvalVarArgsCall.java"], diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java new file mode 100644 index 000000000..7771da3e6 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java @@ -0,0 +1,75 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; +import static dev.cel.runtime.planner.EvalHelpers.evalStrictly; + +import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelResolvedOverload; +import dev.cel.runtime.GlobalResolver; + +final class EvalBinary extends PlannedInterpretable { + + private final CelResolvedOverload resolvedOverload; + private final PlannedInterpretable arg1; + private final PlannedInterpretable arg2; + private final CelValueConverter celValueConverter; + + @Override + public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object argVal1 = + resolvedOverload.isStrict() + ? evalStrictly(arg1, resolver, frame) + : evalNonstrictly(arg1, resolver, frame); + Object argVal2 = + resolvedOverload.isStrict() + ? evalStrictly(arg2, resolver, frame) + : evalNonstrictly(arg2, resolver, frame); + + AccumulatedUnknowns unknowns = AccumulatedUnknowns.maybeMerge(null, argVal1); + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVal2); + + if (unknowns != null) { + return unknowns; + } + + return EvalHelpers.dispatch(resolvedOverload, celValueConverter, argVal1, argVal2); + } + + static EvalBinary create( + long exprId, + CelResolvedOverload resolvedOverload, + PlannedInterpretable arg1, + PlannedInterpretable arg2, + CelValueConverter celValueConverter) { + return new EvalBinary(exprId, resolvedOverload, arg1, arg2, celValueConverter); + } + + private EvalBinary( + long exprId, + CelResolvedOverload resolvedOverload, + PlannedInterpretable arg1, + PlannedInterpretable arg2, + CelValueConverter celValueConverter) { + super(exprId); + this.resolvedOverload = resolvedOverload; + this.arg1 = arg1; + this.arg2 = arg2; + this.celValueConverter = celValueConverter; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java index 38b060b92..5c1dd80b3 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java @@ -61,17 +61,44 @@ static Object dispatch( try { Object result = overload.getDefinition().apply(args); return valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result)); - } catch (CelRuntimeException e) { - // Function dispatch failure that's already been handled -- just propagate. - throw e; } catch (RuntimeException e) { - // Unexpected function dispatch failure. - throw new IllegalArgumentException( - String.format( - "Function '%s' failed with arg(s) '%s'", - overload.getOverloadId(), Joiner.on(", ").join(args)), - e); + throw handleDispatchException(e, overload, args); + } + } + + static Object dispatch(CelResolvedOverload overload, CelValueConverter valueConverter, Object arg) + throws CelEvaluationException { + try { + Object result = overload.getDefinition().apply(arg); + return valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result)); + } catch (RuntimeException e) { + throw handleDispatchException(e, overload, arg); + } + } + + static Object dispatch( + CelResolvedOverload overload, CelValueConverter valueConverter, Object arg1, Object arg2) + throws CelEvaluationException { + try { + Object result = overload.getDefinition().apply(arg1, arg2); + return valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result)); + } catch (RuntimeException e) { + throw handleDispatchException(e, overload, arg1, arg2); + } + } + + private static RuntimeException handleDispatchException( + RuntimeException e, CelResolvedOverload overload, Object... args) { + if (e instanceof CelRuntimeException) { + // Function dispatch failure that's already been handled -- just propagate. + return e; } + // Unexpected function dispatch failure. + return new IllegalArgumentException( + String.format( + "Function '%s' failed with arg(s) '%s'", + overload.getOverloadId(), Joiner.on(", ").join(args)), + e); } private EvalHelpers() {} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java index c715ff032..322648ee3 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java @@ -34,9 +34,7 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval resolvedOverload.isStrict() ? evalStrictly(arg, resolver, frame) : evalNonstrictly(arg, resolver, frame); - Object[] arguments = new Object[] {argVal}; - - return EvalHelpers.dispatch(resolvedOverload, celValueConverter, arguments); + return EvalHelpers.dispatch(resolvedOverload, celValueConverter, argVal); } static EvalUnary create( diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index 7935e4838..b144c4ec9 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -276,6 +276,9 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { return EvalZeroArity.create(expr.id(), resolvedOverload, celValueConverter); case 1: return EvalUnary.create(expr.id(), resolvedOverload, evaluatedArgs[0], celValueConverter); + case 2: + return EvalBinary.create( + expr.id(), resolvedOverload, evaluatedArgs[0], evaluatedArgs[1], celValueConverter); default: return EvalVarArgsCall.create( expr.id(), resolvedOverload, evaluatedArgs, celValueConverter); From 05cb0a114333b3317378a58a98be5480995fd0d3 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 31 Mar 2026 15:33:35 -0700 Subject: [PATCH 036/204] Remove createStruct planinng overhead for type-checked ASTs PiperOrigin-RevId: 892572698 --- .../cel/runtime/planner/ProgramPlanner.java | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index b144c4ec9..add918f64 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -343,7 +343,7 @@ private Optional maybeInterceptOptionalCalls( private PlannedInterpretable planCreateStruct(CelExpr celExpr, PlannerContext ctx) { CelStruct struct = celExpr.struct(); - CelType structType = resolveStructType(struct); + CelType structType = resolveStructType(celExpr, ctx); ImmutableList entries = struct.entries(); String[] keys = new String[entries.size()]; @@ -489,7 +489,17 @@ private ResolvedFunction resolveFunction( return ResolvedFunction.newBuilder().setFunctionName(functionName).setTarget(target).build(); } - private CelType resolveStructType(CelStruct struct) { + private CelType resolveStructType(CelExpr expr, PlannerContext ctx) { + CelType checkedType = ctx.typeMap().get(expr.id()); + if (checkedType != null) { + CelKind kind = checkedType.kind(); + // Type-checked ASTs do not need a type-provider lookup as long as it's of expected kind. + if (isValidStructKind(kind)) { + return checkedType; + } + } + + CelStruct struct = expr.struct(); String messageName = struct.messageName(); for (String typeName : container.resolveCandidateNames(messageName)) { CelType structType = typeProvider.findType(typeName).orElse(null); @@ -499,9 +509,7 @@ private CelType resolveStructType(CelStruct struct) { CelKind kind = structType.kind(); - if (!kind.equals(CelKind.STRUCT) - && !kind.equals(CelKind.TIMESTAMP) - && !kind.equals(CelKind.DURATION)) { + if (!isValidStructKind(kind)) { throw new IllegalArgumentException( String.format( "Expected struct type for %s, got %s", structType.name(), structType.kind())); @@ -513,6 +521,12 @@ private CelType resolveStructType(CelStruct struct) { throw new IllegalArgumentException("Undefined type name: " + messageName); } + private static boolean isValidStructKind(CelKind kind) { + return kind.equals(CelKind.STRUCT) + || kind.equals(CelKind.TIMESTAMP) + || kind.equals(CelKind.DURATION); + } + /** Converts a given expression into a qualified name, if possible. */ private Optional toQualifiedName(CelExpr operand) { switch (operand.getKind()) { From 8e9f1acaa882a858167285093f43722c8f021a7b Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 31 Mar 2026 17:25:32 -0700 Subject: [PATCH 037/204] Optimize attribute qualification process and empty message creation PiperOrigin-RevId: 892624189 --- .../cel/common/internal/DefaultMessageFactory.java | 2 +- .../src/main/java/dev/cel/runtime/CelAttribute.java | 13 ++++++++----- .../java/dev/cel/runtime/CelAttributePattern.java | 13 ++++++++----- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/common/src/main/java/dev/cel/common/internal/DefaultMessageFactory.java b/common/src/main/java/dev/cel/common/internal/DefaultMessageFactory.java index 4a021cd90..68d05e127 100644 --- a/common/src/main/java/dev/cel/common/internal/DefaultMessageFactory.java +++ b/common/src/main/java/dev/cel/common/internal/DefaultMessageFactory.java @@ -52,7 +52,7 @@ public Optional newBuilder(String messageName) { DefaultInstanceMessageFactory.getInstance().getPrototype(descriptor.get()); if (message.isPresent()) { - return message.map(Message::toBuilder); + return message.map(Message::newBuilderForType); } return Optional.of(DynamicMessage.newBuilder(descriptor.get())); diff --git a/runtime/src/main/java/dev/cel/runtime/CelAttribute.java b/runtime/src/main/java/dev/cel/runtime/CelAttribute.java index 6080dbaa1..f04418e0c 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/CelAttribute.java @@ -17,7 +17,6 @@ import com.google.auto.value.AutoOneOf; import com.google.auto.value.AutoValue; import com.google.common.base.Preconditions; -import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.Immutable; @@ -184,9 +183,13 @@ public static CelAttribute create(String rootIdentifier) { */ public static CelAttribute fromQualifiedIdentifier(String qualifiedIdentifier) { ImmutableList.Builder qualifiers = ImmutableList.builder(); - Splitter.on(".") - .split(qualifiedIdentifier) - .forEach((element) -> qualifiers.add(Qualifier.ofString(element))); + int start = 0; + int next; + while ((next = qualifiedIdentifier.indexOf('.', start)) != -1) { + qualifiers.add(Qualifier.ofString(qualifiedIdentifier.substring(start, next))); + start = next + 1; + } + qualifiers.add(Qualifier.ofString(qualifiedIdentifier.substring(start))); return new AutoValue_CelAttribute(qualifiers.build()); } @@ -206,7 +209,7 @@ public CelAttribute qualify(Qualifier qualifier) { return EMPTY; } return new AutoValue_CelAttribute( - ImmutableList.builder().addAll(qualifiers()).add(qualifier).build()); + ImmutableList.builderWithExpectedSize(qualifiers().size() + 1).addAll(qualifiers()).add(qualifier).build()); } @Override diff --git a/runtime/src/main/java/dev/cel/runtime/CelAttributePattern.java b/runtime/src/main/java/dev/cel/runtime/CelAttributePattern.java index 9075cd7a8..ff5f3f5bf 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelAttributePattern.java +++ b/runtime/src/main/java/dev/cel/runtime/CelAttributePattern.java @@ -18,7 +18,6 @@ import com.google.auto.value.AutoValue; import com.google.common.base.Preconditions; -import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; @@ -62,9 +61,13 @@ public static CelAttributePattern create(String rootIdentifier) { */ public static CelAttributePattern fromQualifiedIdentifier(String qualifiedIdentifier) { ImmutableList.Builder qualifiers = ImmutableList.builder(); - Splitter.on(".") - .split(qualifiedIdentifier) - .forEach((String element) -> qualifiers.add(CelAttribute.Qualifier.ofString(element))); + int start = 0; + int next; + while ((next = qualifiedIdentifier.indexOf('.', start)) != -1) { + qualifiers.add(CelAttribute.Qualifier.ofString(qualifiedIdentifier.substring(start, next))); + start = next + 1; + } + qualifiers.add(CelAttribute.Qualifier.ofString(qualifiedIdentifier.substring(start))); return new AutoValue_CelAttributePattern(qualifiers.build()); } @@ -74,7 +77,7 @@ public static CelAttributePattern fromQualifiedIdentifier(String qualifiedIdenti /** Create a new attribute pattern that specifies a subfield of this pattern. */ public CelAttributePattern qualify(CelAttribute.Qualifier qualifier) { return new AutoValue_CelAttributePattern( - ImmutableList.builder() + ImmutableList.builderWithExpectedSize(qualifiers().size() + 1) .addAll(qualifiers()) .add(qualifier) .build()); From ce4d2179fc69458234367d1f61ea3fe1f80a178d Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 1 Apr 2026 15:03:56 -0700 Subject: [PATCH 038/204] Move fast-path unary/binary apply methods into an internal interface PiperOrigin-RevId: 893131554 --- .../src/main/java/dev/cel/runtime/BUILD.bazel | 10 ++++- .../dev/cel/runtime/CelFunctionBinding.java | 8 ++-- .../dev/cel/runtime/CelFunctionOverload.java | 9 ----- .../cel/runtime/CelLateFunctionBindings.java | 2 +- .../dev/cel/runtime/CelResolvedOverload.java | 38 +++++++++++++++++- .../dev/cel/runtime/DefaultDispatcher.java | 39 +------------------ .../dev/cel/runtime/FunctionBindingImpl.java | 8 ++-- .../runtime/OptimizedFunctionOverload.java | 35 +++++++++++++++++ .../dev/cel/runtime/planner/EvalHelpers.java | 6 +-- 9 files changed, 94 insertions(+), 61 deletions(-) create mode 100644 runtime/src/main/java/dev/cel/runtime/OptimizedFunctionOverload.java diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 2681c17de..6f0607de4 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -129,7 +129,6 @@ java_library( "//:auto_value", "//common:error_codes", "//common/annotations", - "//common/exceptions:overload_not_found", "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -151,7 +150,6 @@ cel_android_library( "//:auto_value", "//common:error_codes", "//common/annotations", - "//common/exceptions:overload_not_found", "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", "@maven_android//:com_google_guava_guava", @@ -790,6 +788,7 @@ java_library( name = "function_overload", srcs = [ "CelFunctionOverload.java", + "OptimizedFunctionOverload.java", ], tags = [ ], @@ -805,6 +804,7 @@ cel_android_library( name = "function_overload_android", srcs = [ "CelFunctionOverload.java", + "OptimizedFunctionOverload.java", ], deps = [ ":evaluation_exception", @@ -1306,9 +1306,12 @@ java_library( tags = [ ], deps = [ + ":evaluation_exception", + ":function_binding", ":function_overload", "//:auto_value", "//common/annotations", + "//common/exceptions:overload_not_found", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], @@ -1320,9 +1323,12 @@ cel_android_library( tags = [ ], deps = [ + ":evaluation_exception", + ":function_binding_android", ":function_overload_android", "//:auto_value", "//common/annotations", + "//common/exceptions:overload_not_found", "@maven//:com_google_errorprone_error_prone_annotations", "@maven_android//:com_google_guava_guava", ], diff --git a/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java index 06e5facdf..88be0d3c3 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java @@ -51,13 +51,13 @@ public interface CelFunctionBinding { boolean isStrict(); /** Create a unary function binding from the {@code overloadId}, {@code arg}, and {@code impl}. */ - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") // Safe from CelFunctionOverload.canHandle check before invocation static CelFunctionBinding from( String overloadId, Class arg, CelFunctionOverload.Unary impl) { return from( overloadId, ImmutableList.of(arg), - new CelFunctionOverload() { + new OptimizedFunctionOverload() { @Override public Object apply(Object[] args) throws CelEvaluationException { return impl.apply((T) args[0]); @@ -74,13 +74,13 @@ public Object apply(Object arg1) throws CelEvaluationException { * Create a binary function binding from the {@code overloadId}, {@code arg1}, {@code arg2}, and * {@code impl}. */ - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") // Safe from CelFunctionOverload.canHandle check before invocation static CelFunctionBinding from( String overloadId, Class arg1, Class arg2, CelFunctionOverload.Binary impl) { return from( overloadId, ImmutableList.of(arg1, arg2), - new CelFunctionOverload() { + new OptimizedFunctionOverload() { @Override public Object apply(Object[] args) throws CelEvaluationException { return impl.apply((T1) args[0], (T2) args[1]); diff --git a/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java index e1bdbf886..c5f75096d 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java @@ -26,15 +26,6 @@ public interface CelFunctionOverload { /** Evaluate a set of arguments throwing a {@code CelException} on error. */ Object apply(Object[] args) throws CelEvaluationException; - /** Fast-path for unary function execution to avoid Object[] allocation. */ - default Object apply(Object arg) throws CelEvaluationException { - return apply(new Object[] {arg}); - } - - /** Fast-path for binary function execution to avoid Object[] allocation. */ - default Object apply(Object arg1, Object arg2) throws CelEvaluationException { - return apply(new Object[] {arg1, arg2}); - } /** * Helper interface for describing unary functions where the type-parameter is used to improve diff --git a/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java b/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java index c1f4b236f..3d75845cf 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java +++ b/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java @@ -65,7 +65,7 @@ public static CelLateFunctionBindings from(Collection functi private static CelResolvedOverload createResolvedOverload(CelFunctionBinding binding) { return CelResolvedOverload.of( binding.getOverloadId(), - (args) -> binding.getDefinition().apply(args), + binding.getDefinition(), binding.isStrict(), binding.getArgTypes()); } diff --git a/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java b/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java index 2bcdf3a2d..7063720a1 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java +++ b/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java @@ -18,6 +18,7 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import dev.cel.common.annotations.Internal; +import dev.cel.common.exceptions.CelOverloadNotFoundException; import java.util.List; /** @@ -52,6 +53,33 @@ public abstract class CelResolvedOverload { /** The function definition. */ public abstract CelFunctionOverload getDefinition(); + abstract OptimizedFunctionOverload getOptimizedDefinition(); + + public Object invoke(Object[] args) throws CelEvaluationException { + // Note: canHandle check is handled separately in DynamicDispatchOverload + if (isDynamicDispatch() + || CelFunctionOverload.canHandle(args, getParameterTypes(), isStrict())) { + return getDefinition().apply(args); + } + throw new CelOverloadNotFoundException(getOverloadId()); + } + + public Object invoke(Object arg) throws CelEvaluationException { + if (isDynamicDispatch() + || CelFunctionOverload.canHandle(arg, getParameterTypes(), isStrict())) { + return getOptimizedDefinition().apply(arg); + } + throw new CelOverloadNotFoundException(getOverloadId()); + } + + public Object invoke(Object arg1, Object arg2) throws CelEvaluationException { + if (isDynamicDispatch() + || CelFunctionOverload.canHandle(arg1, arg2, getParameterTypes(), isStrict())) { + return getOptimizedDefinition().apply(arg1, arg2); + } + throw new CelOverloadNotFoundException(getOverloadId()); + } + /** * Creates a new resolved overload from the given overload id, parameter types, and definition. */ @@ -71,8 +99,12 @@ public static CelResolvedOverload of( CelFunctionOverload definition, boolean isStrict, List> parameterTypes) { + OptimizedFunctionOverload optimizedDef = + (definition instanceof OptimizedFunctionOverload) + ? (OptimizedFunctionOverload) definition + : definition::apply; return new AutoValue_CelResolvedOverload( - overloadId, ImmutableList.copyOf(parameterTypes), isStrict, definition); + overloadId, ImmutableList.copyOf(parameterTypes), isStrict, definition, optimizedDef); } /** @@ -81,4 +113,8 @@ public static CelResolvedOverload of( boolean canHandle(Object[] arguments) { return CelFunctionOverload.canHandle(arguments, getParameterTypes(), isStrict()); } + + private boolean isDynamicDispatch() { + return getDefinition() instanceof FunctionBindingImpl.DynamicDispatchOverload; + } } diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java index 87cb07945..d6ddf3965 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java @@ -26,7 +26,6 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.common.CelErrorCode; import dev.cel.common.annotations.Internal; -import dev.cel.common.exceptions.CelOverloadNotFoundException; import dev.cel.runtime.FunctionBindingImpl.DynamicDispatchOverload; import java.util.ArrayList; import java.util.Collection; @@ -202,46 +201,10 @@ public DefaultDispatcher build() { OverloadEntry overloadEntry = entry.getValue(); CelFunctionOverload overloadImpl = overloadEntry.overload(); - CelFunctionOverload guardedApply; - if (overloadImpl instanceof DynamicDispatchOverload) { - // Dynamic dispatcher already does its own internal canHandle checks - guardedApply = overloadImpl; - } else { - boolean isStrict = overloadEntry.isStrict(); - ImmutableList> argTypes = overloadEntry.argTypes(); - - guardedApply = - new CelFunctionOverload() { - @Override - public Object apply(Object[] args) throws CelEvaluationException { - if (CelFunctionOverload.canHandle(args, argTypes, isStrict)) { - return overloadImpl.apply(args); - } - throw new CelOverloadNotFoundException(overloadId); - } - - @Override - public Object apply(Object arg) throws CelEvaluationException { - if (CelFunctionOverload.canHandle(arg, argTypes, isStrict)) { - return overloadImpl.apply(arg); - } - throw new CelOverloadNotFoundException(overloadId); - } - - @Override - public Object apply(Object arg1, Object arg2) throws CelEvaluationException { - if (CelFunctionOverload.canHandle(arg1, arg2, argTypes, isStrict)) { - return overloadImpl.apply(arg1, arg2); - } - throw new CelOverloadNotFoundException(overloadId); - } - }; - } - resolvedOverloads.put( overloadId, CelResolvedOverload.of( - overloadId, guardedApply, overloadEntry.isStrict(), overloadEntry.argTypes())); + overloadId, overloadImpl, overloadEntry.isStrict(), overloadEntry.argTypes())); } return new DefaultDispatcher(resolvedOverloads.buildOrThrow()); diff --git a/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java b/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java index 1f47f1dfd..c1306ce19 100644 --- a/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java @@ -126,7 +126,7 @@ private DynamicDispatchBinding( } @Immutable - static final class DynamicDispatchOverload implements CelFunctionOverload { + static final class DynamicDispatchOverload implements OptimizedFunctionOverload { private final String functionName; private final ImmutableSet overloadBindings; @@ -149,7 +149,8 @@ public Object apply(Object[] args) throws CelEvaluationException { public Object apply(Object arg) throws CelEvaluationException { for (CelFunctionBinding overload : overloadBindings) { if (CelFunctionOverload.canHandle(arg, overload.getArgTypes(), overload.isStrict())) { - return overload.getDefinition().apply(arg); + OptimizedFunctionOverload def = (OptimizedFunctionOverload) overload.getDefinition(); + return def.apply(arg); } } throw new CelOverloadNotFoundException( @@ -164,7 +165,8 @@ public Object apply(Object arg1, Object arg2) throws CelEvaluationException { for (CelFunctionBinding overload : overloadBindings) { if (CelFunctionOverload.canHandle( arg1, arg2, overload.getArgTypes(), overload.isStrict())) { - return overload.getDefinition().apply(arg1, arg2); + OptimizedFunctionOverload def = (OptimizedFunctionOverload) overload.getDefinition(); + return def.apply(arg1, arg2); } } throw new CelOverloadNotFoundException( diff --git a/runtime/src/main/java/dev/cel/runtime/OptimizedFunctionOverload.java b/runtime/src/main/java/dev/cel/runtime/OptimizedFunctionOverload.java new file mode 100644 index 000000000..fde8bcc15 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/OptimizedFunctionOverload.java @@ -0,0 +1,35 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import com.google.errorprone.annotations.Immutable; + +/** + * Internal interface to support fast-path Unary and Binary evaluations, avoiding Object[] + * allocation. + */ +@Immutable +interface OptimizedFunctionOverload extends CelFunctionOverload { + + /** Fast-path for unary function execution to avoid Object[] allocation. */ + default Object apply(Object arg) throws CelEvaluationException { + return apply(new Object[] {arg}); + } + + /** Fast-path for binary function execution to avoid Object[] allocation. */ + default Object apply(Object arg1, Object arg2) throws CelEvaluationException { + return apply(new Object[] {arg1, arg2}); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java index 5c1dd80b3..a30f91880 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java @@ -59,7 +59,7 @@ static Object dispatch( CelResolvedOverload overload, CelValueConverter valueConverter, Object[] args) throws CelEvaluationException { try { - Object result = overload.getDefinition().apply(args); + Object result = overload.invoke(args); return valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result)); } catch (RuntimeException e) { throw handleDispatchException(e, overload, args); @@ -69,7 +69,7 @@ static Object dispatch( static Object dispatch(CelResolvedOverload overload, CelValueConverter valueConverter, Object arg) throws CelEvaluationException { try { - Object result = overload.getDefinition().apply(arg); + Object result = overload.invoke(arg); return valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result)); } catch (RuntimeException e) { throw handleDispatchException(e, overload, arg); @@ -80,7 +80,7 @@ static Object dispatch( CelResolvedOverload overload, CelValueConverter valueConverter, Object arg1, Object arg2) throws CelEvaluationException { try { - Object result = overload.getDefinition().apply(arg1, arg2); + Object result = overload.invoke(arg1, arg2); return valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result)); } catch (RuntimeException e) { throw handleDispatchException(e, overload, arg1, arg2); From 4d00593223f7373eeda605353180fed78d2067f9 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 1 Apr 2026 15:28:25 -0700 Subject: [PATCH 039/204] Fix partial evaluation to properly check for comprehension bound variables for planner PiperOrigin-RevId: 893142702 --- .../dev/cel/runtime/planner/ActivationWrapper.java | 3 +++ .../java/dev/cel/runtime/planner/EvalFold.java | 5 +++++ .../cel/runtime/planner/NamespacedAttribute.java | 13 ++++++++++++- .../dev/cel/runtime/PlannerInterpreterTest.java | 5 +++++ .../planner_unknownResultSet_success.baseline | 14 +++++++++++++- testing/src/main/java/dev/cel/testing/BUILD.bazel | 1 + .../java/dev/cel/testing/BaseInterpreterTest.java | 3 ++- 7 files changed, 41 insertions(+), 3 deletions(-) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ActivationWrapper.java b/runtime/src/main/java/dev/cel/runtime/planner/ActivationWrapper.java index f844ab232..8883ac12c 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ActivationWrapper.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ActivationWrapper.java @@ -19,4 +19,7 @@ /** Identifies a resolver that can be unwrapped to bypass local variable state. */ public interface ActivationWrapper extends GlobalResolver { GlobalResolver unwrap(); + + /** Returns true if the given name is bound by this local activation wrapper. */ + boolean isLocallyBound(String name); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java index 197db42ad..2631bf0b9 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java @@ -175,6 +175,11 @@ public GlobalResolver unwrap() { return resolver; } + @Override + public boolean isLocallyBound(String name) { + return name.equals(accuVar) || name.equals(iterVar) || name.equals(iterVar2); + } + @Override public @Nullable Object resolve(String name) { if (name.equals(accuVar)) { diff --git a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java index d51336d80..0000ad764 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java @@ -75,7 +75,7 @@ public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { PartialVars partialVars = frame.partialVars().orElse(null); - if (partialVars != null) { + if (partialVars != null && !isLocallyBound(resolver, name)) { ImmutableList patterns = partialVars.unknowns(); // Avoid enhanced for loop to prevent UnmodifiableIterator from being allocated for (int i = 0; i < qualifiers.size(); i++) { @@ -151,6 +151,17 @@ private static Long getEnumValue(EnumType enumType, String field) { String.format("Field %s was not found on enum %s", enumType.name(), field))); } + private boolean isLocallyBound(GlobalResolver resolver, String name) { + while (resolver instanceof ActivationWrapper) { + ActivationWrapper wrapper = (ActivationWrapper) resolver; + if (wrapper.isLocallyBound(name)) { + return true; + } + resolver = wrapper.unwrap(); + } + return false; + } + private GlobalResolver unwrapToNonLocal(GlobalResolver resolver) { while (resolver instanceof ActivationWrapper) { resolver = ((ActivationWrapper) resolver).unwrap(); diff --git a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java index 181842ab4..2b0e53298 100644 --- a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java +++ b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java @@ -283,6 +283,11 @@ public void planner_unknownResultSet_success() { declareVariable("unknown_list", ListType.create(SimpleType.INT)); source = "unknown_list.map(x, x)"; runTest(variables, CelAttributePattern.fromQualifiedIdentifier("unknown_list")); + + clearAllDeclarations(); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + source = "cel.bind(x, [1, 2, 3], 1 in x)"; + runTest(variables, CelAttributePattern.fromQualifiedIdentifier("x")); } @Test diff --git a/runtime/src/test/resources/planner_unknownResultSet_success.baseline b/runtime/src/test/resources/planner_unknownResultSet_success.baseline index 2f2c218d0..c5e8867db 100644 --- a/runtime/src/test/resources/planner_unknownResultSet_success.baseline +++ b/runtime/src/test/resources/planner_unknownResultSet_success.baseline @@ -458,4 +458,16 @@ single_timestamp { seconds: 15 } , unknown_attributes=[unknown_list]} -result: CelUnknownSet{attributes=[unknown_list], unknownExprIds=[1]} \ No newline at end of file +result: CelUnknownSet{attributes=[unknown_list], unknownExprIds=[1]} + +Source: cel.bind(x, [1, 2, 3], 1 in x) +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_string: "test" +single_timestamp { + seconds: 15 +} +, unknown_attributes=[x]} +result: true \ No newline at end of file diff --git a/testing/src/main/java/dev/cel/testing/BUILD.bazel b/testing/src/main/java/dev/cel/testing/BUILD.bazel index 2ecabdf05..5ee142200 100644 --- a/testing/src/main/java/dev/cel/testing/BUILD.bazel +++ b/testing/src/main/java/dev/cel/testing/BUILD.bazel @@ -87,6 +87,7 @@ java_library( "//common/types:message_type_provider", "//common/types:type_providers", "//common/values:cel_byte_string", + "//extensions", "//extensions:optional_library", "//runtime", "//runtime:function_binding", diff --git a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java index 42cb5e41b..bda56a19e 100644 --- a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java +++ b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java @@ -74,6 +74,7 @@ import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum; import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; +import dev.cel.extensions.CelExtensions; import dev.cel.extensions.CelOptionalLibrary; import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; @@ -153,7 +154,7 @@ protected void prepareCompiler(CelTypeProvider typeProvider) { this.celCompiler = celCompiler .toCompilerBuilder() - .addLibraries(CelOptionalLibrary.INSTANCE) + .addLibraries(CelOptionalLibrary.INSTANCE, CelExtensions.bindings()) .setOptions(celOptions) .build(); } From 46bae721769da226f2d1fc560d4e8f8dce47e2f0 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 2 Apr 2026 13:30:20 -0700 Subject: [PATCH 040/204] Fix constant folding to not error when sub-asts contain unbound variables CEL-Java fix for xref: https://github.com/google/cel-go/issues/1296 PiperOrigin-RevId: 893670998 --- .../dev/cel/optimizer/optimizers/BUILD.bazel | 2 + .../optimizers/ConstantFoldingOptimizer.java | 22 ++++- .../dev/cel/optimizer/optimizers/BUILD.bazel | 1 + .../ConstantFoldingOptimizerTest.java | 97 ++++++++++++++----- .../java/dev/cel/runtime/PartialVars.java | 7 +- 5 files changed, 97 insertions(+), 32 deletions(-) diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel index 7984cf3ba..c887f3d15 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -35,6 +35,8 @@ java_library( "//optimizer:mutable_ast", "//optimizer:optimization_exception", "//runtime", + "//runtime:partial_vars", + "//runtime:unknown_attributes", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java index ada73ce56..c017911f9 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java @@ -30,7 +30,6 @@ import dev.cel.common.CelValidationException; import dev.cel.common.Operator; import dev.cel.common.ast.CelConstant; -import dev.cel.common.ast.CelExpr; import dev.cel.common.ast.CelExpr.ExprKind.Kind; import dev.cel.common.ast.CelMutableExpr; import dev.cel.common.ast.CelMutableExpr.CelMutableCall; @@ -47,7 +46,10 @@ import dev.cel.optimizer.AstMutator; import dev.cel.optimizer.CelAstOptimizer; import dev.cel.optimizer.CelOptimizationException; +import dev.cel.runtime.CelAttribute.Qualifier; +import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.PartialVars; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; @@ -282,7 +284,7 @@ private Optional maybeFold( throws CelOptimizationException { Object result; try { - result = evaluateExpr(cel, CelMutableExprConverter.fromMutableExpr(node.expr())); + result = evaluateExpr(cel, node); } catch (CelValidationException | CelEvaluationException e) { throw new CelOptimizationException( "Constant folding failure. Failed to evaluate subtree due to: " + e.getMessage(), e); @@ -674,13 +676,23 @@ private CelMutableAst pruneOptionalStructElements(CelMutableAst ast, CelMutableE } @CanIgnoreReturnValue - private static Object evaluateExpr(Cel cel, CelExpr expr) + private static Object evaluateExpr(Cel cel, CelNavigableMutableExpr navigableMutableExpr) throws CelValidationException, CelEvaluationException { + ImmutableList attributePatterns = + navigableMutableExpr + .allNodes() + .filter(node -> node.getKind().equals(Kind.IDENT)) + .map(node -> node.expr().ident().name()) + .filter(Qualifier::isLegalIdentifier) + .map(CelAttributePattern::create) + .collect(toImmutableList()); CelAbstractSyntaxTree ast = - CelAbstractSyntaxTree.newParsedAst(expr, CelSource.newBuilder().build()); + CelAbstractSyntaxTree.newParsedAst( + CelMutableExprConverter.fromMutableExpr(navigableMutableExpr.expr()), + CelSource.newBuilder().build()); ast = cel.check(ast).getAst(); - return cel.createProgram(ast).eval(); + return cel.createProgram(ast).eval(PartialVars.of(attributePatterns)); } /** Options to configure how Constant Folding behave. */ diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel index d91e48f54..b0c48682a 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -11,6 +11,7 @@ java_library( deps = [ # "//java/com/google/testing/testsize:annotations", "//bundle:cel", + "//bundle:cel_experimental_factory", "//common:cel_ast", "//common:cel_source", "//common:compiler_common", diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java index a8cadf83a..bbb5c6e7e 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java @@ -18,9 +18,12 @@ import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; +import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.bundle.Cel; +import dev.cel.bundle.CelBuilder; +import dev.cel.bundle.CelExperimentalFactory; import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; @@ -47,9 +50,23 @@ @RunWith(TestParameterInjector.class) public class ConstantFoldingOptimizerTest { private static final CelOptions CEL_OPTIONS = - CelOptions.current().populateMacroCalls(true).build(); - private static final Cel CEL = - CelFactory.standardCelBuilder() + CelOptions.current() + .populateMacroCalls(true) + .enableHeterogeneousNumericComparisons(true) + .build(); + + private static final CelUnparser CEL_UNPARSER = CelUnparserFactory.newUnparser(); + + @SuppressWarnings("ImmutableEnumChecker") // test only + private enum RuntimeEnv { + LEGACY(setupEnv(CelFactory.standardCelBuilder())), + PLANNER(setupEnv(CelExperimentalFactory.plannerCelBuilder())); + + private final Cel cel; + private final CelOptimizer celOptimizer; + + private static Cel setupEnv(CelBuilder celBuilder) { + return celBuilder .addVar("x", SimpleType.DYN) .addVar("y", SimpleType.DYN) .addVar("list_var", ListType.create(SimpleType.STRING)) @@ -84,13 +101,28 @@ public class ConstantFoldingOptimizerTest { CelExtensions.sets(CEL_OPTIONS), CelExtensions.encoders(CEL_OPTIONS)) .build(); + } + + RuntimeEnv(Cel cel) { + this.cel = cel; + this.celOptimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(cel) + .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) + .build(); + } + + private CelBuilder newCelBuilder() { + switch (this) { + case LEGACY: + return CelFactory.standardCelBuilder(); + case PLANNER: + return CelExperimentalFactory.plannerCelBuilder(); + } + throw new AssertionError("Unknown RuntimeEnv: " + this); + } + } - private static final CelOptimizer CEL_OPTIMIZER = - CelOptimizerFactory.standardCelOptimizerBuilder(CEL) - .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) - .build(); - - private static final CelUnparser CEL_UNPARSER = CelUnparserFactory.newUnparser(); + @TestParameter RuntimeEnv runtimeEnv; @Test @TestParameters("{source: 'null', expected: 'null'}") @@ -238,9 +270,9 @@ public class ConstantFoldingOptimizerTest { // TODO: Support folding lists with mixed types. This requires mutable lists. // @TestParameters("{source: 'dyn([1]) + [1.0]'}") public void constantFold_success(String source, String expected) throws Exception { - CelAbstractSyntaxTree ast = CEL.compile(source).getAst(); + CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(source).getAst(); - CelAbstractSyntaxTree optimizedAst = CEL_OPTIMIZER.optimize(ast); + CelAbstractSyntaxTree optimizedAst = runtimeEnv.celOptimizer.optimize(ast); assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo(expected); } @@ -285,12 +317,13 @@ public void constantFold_success(String source, String expected) throws Exceptio public void constantFold_macros_macroCallMetadataPopulated(String source, String expected) throws Exception { Cel cel = - CelFactory.standardCelBuilder() + runtimeEnv + .newCelBuilder() .addVar("x", SimpleType.DYN) .addVar("y", SimpleType.DYN) .addMessageTypes(TestAllTypes.getDescriptor()) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .setOptions(CelOptions.current().populateMacroCalls(true).build()) + .setOptions(CEL_OPTIONS) .addCompilerLibraries( CelExtensions.bindings(), CelExtensions.optional(), CelExtensions.comprehensions()) .addRuntimeLibraries(CelExtensions.optional(), CelExtensions.comprehensions()) @@ -330,12 +363,17 @@ public void constantFold_macros_macroCallMetadataPopulated(String source, String @TestParameters("{source: 'false ? false : cel.bind(a, true, a)'}") public void constantFold_macros_withoutMacroCallMetadata(String source) throws Exception { Cel cel = - CelFactory.standardCelBuilder() + runtimeEnv + .newCelBuilder() .addVar("x", SimpleType.DYN) .addVar("y", SimpleType.DYN) .addMessageTypes(TestAllTypes.getDescriptor()) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .setOptions(CelOptions.current().populateMacroCalls(false).build()) + .setOptions( + CelOptions.current() + .enableHeterogeneousNumericComparisons(true) + .populateMacroCalls(false) + .build()) .addCompilerLibraries(CelExtensions.bindings(), CelOptionalLibrary.INSTANCE) .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) .build(); @@ -378,21 +416,22 @@ public void constantFold_macros_withoutMacroCallMetadata(String source) throws E @TestParameters("{source: 'duration(\"1h\")'}") @TestParameters("{source: '[true].exists(x, x == get_true())'}") @TestParameters("{source: 'get_list([1, 2]).map(x, x * 2)'}") + @TestParameters("{source: '[(x - 1 > 3) ? (x - 1) : 5].exists(x, x - 1 > 3)'}") public void constantFold_noOp(String source) throws Exception { - CelAbstractSyntaxTree ast = CEL.compile(source).getAst(); + CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(source).getAst(); - CelAbstractSyntaxTree optimizedAst = CEL_OPTIMIZER.optimize(ast); + CelAbstractSyntaxTree optimizedAst = runtimeEnv.celOptimizer.optimize(ast); assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo(source); } @Test public void constantFold_addFoldableFunction_success() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("get_true() == get_true()").getAst(); + CelAbstractSyntaxTree ast = runtimeEnv.cel.compile("get_true() == get_true()").getAst(); ConstantFoldingOptions options = ConstantFoldingOptions.newBuilder().addFoldableFunctions("get_true").build(); CelOptimizer optimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + CelOptimizerFactory.standardCelOptimizerBuilder(runtimeEnv.cel) .addAstOptimizers(ConstantFoldingOptimizer.newInstance(options)) .build(); @@ -403,7 +442,7 @@ public void constantFold_addFoldableFunction_success() throws Exception { @Test public void constantFold_withExpectedResultTypeSet_success() throws Exception { - Cel cel = CelFactory.standardCelBuilder().setResultType(SimpleType.STRING).build(); + Cel cel = runtimeEnv.newCelBuilder().setResultType(SimpleType.STRING).build(); CelOptimizer optimizer = CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) @@ -419,10 +458,11 @@ public void constantFold_withExpectedResultTypeSet_success() throws Exception { public void constantFold_withMacroCallPopulated_comprehensionsAreReplacedWithNotSet() throws Exception { Cel cel = - CelFactory.standardCelBuilder() + runtimeEnv + .newCelBuilder() .addVar("x", SimpleType.DYN) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .setOptions(CelOptions.current().populateMacroCalls(true).build()) + .setOptions(CEL_OPTIONS) .build(); CelOptimizer celOptimizer = CelOptimizerFactory.standardCelOptimizerBuilder(cel) @@ -492,9 +532,9 @@ public void constantFold_withMacroCallPopulated_comprehensionsAreReplacedWithNot @Test public void constantFold_astProducesConsistentlyNumberedIds() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("[1] + [2] + [3]").getAst(); + CelAbstractSyntaxTree ast = runtimeEnv.cel.compile("[1] + [2] + [3]").getAst(); - CelAbstractSyntaxTree optimizedAst = CEL_OPTIMIZER.optimize(ast); + CelAbstractSyntaxTree optimizedAst = runtimeEnv.celOptimizer.optimize(ast); assertThat(optimizedAst.getExpr().toString()) .isEqualTo( @@ -515,8 +555,13 @@ public void iterationLimitReached_throws() throws Exception { sb.append(" + ").append(i); } // 0 + 1 + 2 + 3 + ... 200 Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().maxParseRecursionDepth(200).build()) + runtimeEnv + .newCelBuilder() + .setOptions( + CelOptions.current() + .enableHeterogeneousNumericComparisons(true) + .maxParseRecursionDepth(200) + .build()) .build(); CelAbstractSyntaxTree ast = cel.compile(sb.toString()).getAst(); CelOptimizer optimizer = diff --git a/runtime/src/main/java/dev/cel/runtime/PartialVars.java b/runtime/src/main/java/dev/cel/runtime/PartialVars.java index 1cd081040..f195880d0 100644 --- a/runtime/src/main/java/dev/cel/runtime/PartialVars.java +++ b/runtime/src/main/java/dev/cel/runtime/PartialVars.java @@ -37,7 +37,12 @@ public abstract class PartialVars { /** Constructs a new {@code PartialVars} from one or more {@link CelAttributePattern}s. */ public static PartialVars of(CelAttributePattern... unknownAttributes) { - return of((unused) -> Optional.empty(), ImmutableList.copyOf(unknownAttributes)); + return of(ImmutableList.copyOf(unknownAttributes)); + } + + /** Constructs a new {@code PartialVars} from a list of {@link CelAttributePattern}s. */ + public static PartialVars of(Iterable unknownAttributes) { + return of((unused) -> Optional.empty(), unknownAttributes); } /** From 7d73658c7529d7308294e30aee51e23a36d5028a Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 3 Apr 2026 10:17:16 -0700 Subject: [PATCH 041/204] Reject invalid unicode literals in the parser PiperOrigin-RevId: 894137619 --- .../dev/cel/common/internal/Constants.java | 6 ++++++ .../cel/parser/CelParserParameterizedTest.java | 4 ++++ .../src/test/resources/parser_errors.baseline | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/common/src/main/java/dev/cel/common/internal/Constants.java b/common/src/main/java/dev/cel/common/internal/Constants.java index d2c0719ec..49bca7489 100644 --- a/common/src/main/java/dev/cel/common/internal/Constants.java +++ b/common/src/main/java/dev/cel/common/internal/Constants.java @@ -207,6 +207,9 @@ private static void decodeString( continue; } skipNewline = false; + if (codePoint >= MIN_SURROGATE && codePoint <= MAX_SURROGATE) { + throw new ParseException("Invalid unicode code point", seqOffset); + } buffer.appendCodePoint(codePoint); } else { // Normalize '\r' and '\r\n' to '\n'. @@ -231,6 +234,9 @@ private static void decodeString( // For raw literals, all escapes are valid and those characters come through literally in // the string. buffer.appendCodePoint('\\'); + if (codePoint >= MIN_SURROGATE && codePoint <= MAX_SURROGATE) { + throw new ParseException("Invalid unicode code point", seqOffset); + } buffer.appendCodePoint(codePoint); continue; } diff --git a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java index 58b45ddab..b7474041d 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java @@ -248,6 +248,10 @@ public void parser_errors() { runTest(PARSER, "1 + +"); runTest(PARSER, "\"\\xFh\""); runTest(PARSER, "\"\\a\\b\\f\\n\\r\\t\\v\\'\\\"\\\\\\? Illegal escape \\>\""); + runTest(PARSER, "'\uD800'"); + runTest(PARSER, "'\uDFFF'"); + runTest(PARSER, "r\"\\\uD800\""); + runTest(PARSER, "as"); runTest(PARSER, "break"); runTest(PARSER, "const"); diff --git a/parser/src/test/resources/parser_errors.baseline b/parser/src/test/resources/parser_errors.baseline index 9f4b96825..998bbd487 100644 --- a/parser/src/test/resources/parser_errors.baseline +++ b/parser/src/test/resources/parser_errors.baseline @@ -85,6 +85,24 @@ ERROR: :1:43: mismatched input '' expecting {'[', '{', '(', '.', '-' | "\a\b\f\n\r\t\v\'\"\\\? Illegal escape \>" | ..........................................^ +I: '?' +=====> +E: ERROR: :1:1: Invalid unicode code point + | '?' + | ^ + +I: '?' +=====> +E: ERROR: :1:1: Invalid unicode code point + | '?' + | ^ + +I: r"\?" +=====> +E: ERROR: :1:1: Invalid unicode code point + | r"\?" + | ^ + I: as =====> E: ERROR: :1:1: reserved identifier: as From 664c31b9b58b7086df87ec24f066878431c3fc2d Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 3 Apr 2026 12:53:48 -0700 Subject: [PATCH 042/204] Implement cel.@block for planner PiperOrigin-RevId: 894210773 --- .../main/java/dev/cel/extensions/BUILD.bazel | 1 + .../cel/extensions/CelBindingsExtensions.java | 14 +- .../extensions/CelBindingsExtensionsTest.java | 3 +- .../dev/cel/optimizer/optimizers/BUILD.bazel | 3 + .../SubexpressionOptimizerBaselineTest.java | 159 +++++++++++------- .../SubexpressionOptimizerTest.java | 140 ++++++++++++--- ...old_before_subexpression_unparsed.baseline | 2 +- .../resources/subexpression_unparsed.baseline | 2 +- .../java/dev/cel/runtime/planner/BUILD.bazel | 62 +++---- .../cel/runtime/planner/BlockMemoizer.java | 72 ++++++++ .../dev/cel/runtime/planner/EvalBlock.java | 67 ++++++++ .../cel/runtime/planner/ExecutionFrame.java | 12 ++ .../cel/runtime/planner/ProgramPlanner.java | 59 ++++++- 13 files changed, 466 insertions(+), 130 deletions(-) create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/BlockMemoizer.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/EvalBlock.java diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel index ed2d19d6f..77663f2fa 100644 --- a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel @@ -142,6 +142,7 @@ java_library( deps = [ "//common:compiler_common", "//common/ast", + "//common/types", "//compiler:compiler_builder", "//extensions:extension_library", "//parser:macro", diff --git a/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java index 5eb2c2e8c..0e6537334 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java @@ -22,7 +22,11 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelIssue; +import dev.cel.common.CelOverloadDecl; import dev.cel.common.ast.CelExpr; +import dev.cel.common.types.ListType; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.TypeParamType; import dev.cel.compiler.CelCompilerLibrary; import dev.cel.parser.CelMacro; import dev.cel.parser.CelMacroExprFactory; @@ -62,7 +66,15 @@ public int version() { @Override public ImmutableSet functions() { - return ImmutableSet.of(); + // TODO: Add bindings for block once decorator support is available. + return ImmutableSet.of( + CelFunctionDecl.newFunctionDeclaration( + "cel.@block", + CelOverloadDecl.newGlobalOverload( + "cel_block_list", + TypeParamType.create("T"), + ListType.create(SimpleType.DYN), + TypeParamType.create("T")))); } @Override diff --git a/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java index bc98c9816..ff9e31432 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java @@ -63,7 +63,8 @@ public void library() { CelExtensions.getExtensionLibrary("bindings", CelOptions.DEFAULT); assertThat(library.name()).isEqualTo("bindings"); assertThat(library.latest().version()).isEqualTo(0); - assertThat(library.version(0).functions()).isEmpty(); + assertThat(library.version(0).functions().stream().map(CelFunctionDecl::name)) + .containsExactly("cel.@block"); assertThat(library.version(0).macros().stream().map(CelMacro::getFunction)) .containsExactly("bind"); } diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel index b0c48682a..734aa6879 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -33,6 +33,9 @@ java_library( "//parser:unparser", "//runtime", "//runtime:function_binding", + "//runtime:partial_vars", + "//runtime:program", + "//runtime:unknown_attributes", "//testing:baseline_test_case", "@maven//:junit_junit", "@maven//:com_google_testparameterinjector_test_parameter_injector", diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java index 802ef3037..74e3b5b32 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java @@ -24,6 +24,7 @@ // import com.google.testing.testsize.MediumTest; import dev.cel.bundle.Cel; import dev.cel.bundle.CelBuilder; +import dev.cel.bundle.CelExperimentalFactory; import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; @@ -43,6 +44,7 @@ import dev.cel.parser.CelUnparserFactory; import dev.cel.runtime.CelFunctionBinding; import dev.cel.testing.BaselineTestCase; +import java.util.EnumSet; import java.util.Optional; import org.junit.Before; import org.junit.Test; @@ -51,6 +53,50 @@ // @MediumTest @RunWith(TestParameterInjector.class) public class SubexpressionOptimizerBaselineTest extends BaselineTestCase { + private enum RuntimeEnv { + LEGACY(setupCelEnv(CelFactory.standardCelBuilder())), + PLANNER(setupCelEnv(CelExperimentalFactory.plannerCelBuilder())); + + private final Cel cel; + + private static Cel setupCelEnv(CelBuilder celBuilder) { + return celBuilder + .addMessageTypes(TestAllTypes.getDescriptor()) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions( + CelOptions.current() + .populateMacroCalls(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .addCompilerLibraries( + CelExtensions.optional(), CelExtensions.bindings(), CelExtensions.comprehensions()) + .addRuntimeLibraries(CelExtensions.optional(), CelExtensions.comprehensions()) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "pure_custom_func", + newGlobalOverload("pure_custom_func_overload", SimpleType.INT, SimpleType.INT)), + CelFunctionDecl.newFunctionDeclaration( + "non_pure_custom_func", + newGlobalOverload( + "non_pure_custom_func_overload", SimpleType.INT, SimpleType.INT))) + .addFunctionBindings( + // This is pure, but for the purposes of excluding it as a CSE candidate, pretend that + // it isn't. + CelFunctionBinding.from("non_pure_custom_func_overload", Long.class, val -> val), + CelFunctionBinding.from("pure_custom_func_overload", Long.class, val -> val)) + .addVar("x", SimpleType.DYN) + .addVar("y", SimpleType.DYN) + .addVar("opt_x", OptionalType.create(SimpleType.DYN)) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .build(); + } + + RuntimeEnv(Cel cel) { + this.cel = cel; + } + } + private static final CelUnparser CEL_UNPARSER = CelUnparserFactory.newUnparser(); private static final TestAllTypes TEST_ALL_TYPES_INPUT = TestAllTypes.newBuilder() @@ -67,7 +113,6 @@ public class SubexpressionOptimizerBaselineTest extends BaselineTestCase { .putMapInt32Int64(2, 2) .putMapStringString("key", "A"))) .build(); - private static final Cel CEL = newCelBuilder().build(); private static final SubexpressionOptimizerOptions OPTIMIZER_COMMON_OPTIONS = SubexpressionOptimizerOptions.newBuilder() @@ -90,45 +135,49 @@ protected String baselineFileName() { return overriddenBaseFilePath; } + @TestParameter RuntimeEnv runtimeEnv; + @Test public void allOptimizers_producesSameEvaluationResult( @TestParameter CseTestOptimizer cseTestOptimizer, @TestParameter CseTestCase cseTestCase) throws Exception { skipBaselineVerification(); - CelAbstractSyntaxTree ast = CEL.compile(cseTestCase.source).getAst(); + CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(cseTestCase.source).getAst(); ImmutableMap inputMap = ImmutableMap.of("msg", TEST_ALL_TYPES_INPUT, "x", 5L, "y", 6L, "opt_x", Optional.of(5L)); - Object expectedEvalResult = CEL.createProgram(ast).eval(inputMap); + Object expectedEvalResult = runtimeEnv.cel.createProgram(ast).eval(inputMap); - CelAbstractSyntaxTree optimizedAst = cseTestOptimizer.cseOptimizer.optimize(ast); + CelAbstractSyntaxTree optimizedAst = cseTestOptimizer.newCseOptimizer(runtimeEnv).optimize(ast); - Object optimizedEvalResult = CEL.createProgram(optimizedAst).eval(inputMap); + Object optimizedEvalResult = runtimeEnv.cel.createProgram(optimizedAst).eval(inputMap); assertThat(optimizedEvalResult).isEqualTo(expectedEvalResult); } @Test public void subexpression_unparsed() throws Exception { - for (CseTestCase cseTestCase : CseTestCase.values()) { + for (CseTestCase cseTestCase : EnumSet.allOf(CseTestCase.class)) { testOutput().println("Test case: " + cseTestCase.name()); testOutput().println("Source: " + cseTestCase.source); testOutput().println("=====>"); - CelAbstractSyntaxTree ast = CEL.compile(cseTestCase.source).getAst(); + CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(cseTestCase.source).getAst(); boolean resultPrinted = false; for (CseTestOptimizer cseTestOptimizer : CseTestOptimizer.values()) { String optimizerName = cseTestOptimizer.name(); CelAbstractSyntaxTree optimizedAst; try { - optimizedAst = cseTestOptimizer.cseOptimizer.optimize(ast); + optimizedAst = cseTestOptimizer.newCseOptimizer(runtimeEnv).optimize(ast); } catch (Exception e) { testOutput().printf("[%s]: Optimization Error: %s", optimizerName, e); continue; } if (!resultPrinted) { Object optimizedEvalResult = - CEL.createProgram(optimizedAst) + runtimeEnv + .cel + .createProgram(optimizedAst) .eval( ImmutableMap.of( - "msg", TEST_ALL_TYPES_INPUT, "x", 5L, "opt_x", Optional.of(5L))); + "msg", TEST_ALL_TYPES_INPUT, "x", 5L, "y", 6L, "opt_x", Optional.of(5L))); testOutput().println("Result: " + optimizedEvalResult); resultPrinted = true; } @@ -145,22 +194,24 @@ public void subexpression_unparsed() throws Exception { @Test public void constfold_before_subexpression_unparsed() throws Exception { - for (CseTestCase cseTestCase : CseTestCase.values()) { + for (CseTestCase cseTestCase : EnumSet.allOf(CseTestCase.class)) { testOutput().println("Test case: " + cseTestCase.name()); testOutput().println("Source: " + cseTestCase.source); testOutput().println("=====>"); - CelAbstractSyntaxTree ast = CEL.compile(cseTestCase.source).getAst(); + CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(cseTestCase.source).getAst(); boolean resultPrinted = false; - for (CseTestOptimizer cseTestOptimizer : CseTestOptimizer.values()) { + for (CseTestOptimizer cseTestOptimizer : EnumSet.allOf(CseTestOptimizer.class)) { String optimizerName = cseTestOptimizer.name(); CelAbstractSyntaxTree optimizedAst = - cseTestOptimizer.cseWithConstFoldingOptimizer.optimize(ast); + cseTestOptimizer.newCseWithConstFoldingOptimizer(runtimeEnv).optimize(ast); if (!resultPrinted) { Object optimizedEvalResult = - CEL.createProgram(optimizedAst) + runtimeEnv + .cel + .createProgram(optimizedAst) .eval( ImmutableMap.of( - "msg", TEST_ALL_TYPES_INPUT, "x", 5L, "opt_x", Optional.of(5L))); + "msg", TEST_ALL_TYPES_INPUT, "x", 5L, "y", 6L, "opt_x", Optional.of(5L))); testOutput().println("Result: " + optimizedEvalResult); resultPrinted = true; } @@ -179,12 +230,13 @@ public void constfold_before_subexpression_unparsed() throws Exception { public void subexpression_ast(@TestParameter CseTestOptimizer cseTestOptimizer) throws Exception { String testBasefileName = "subexpression_ast_" + Ascii.toLowerCase(cseTestOptimizer.name()); overriddenBaseFilePath = String.format("%s%s.baseline", testdataDir(), testBasefileName); - for (CseTestCase cseTestCase : CseTestCase.values()) { + for (CseTestCase cseTestCase : EnumSet.allOf(CseTestCase.class)) { testOutput().println("Test case: " + cseTestCase.name()); testOutput().println("Source: " + cseTestCase.source); testOutput().println("=====>"); - CelAbstractSyntaxTree ast = CEL.compile(cseTestCase.source).getAst(); - CelAbstractSyntaxTree optimizedAst = cseTestOptimizer.cseOptimizer.optimize(ast); + CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(cseTestCase.source).getAst(); + CelAbstractSyntaxTree optimizedAst = + newCseOptimizer(runtimeEnv.cel, cseTestOptimizer.option).optimize(ast); testOutput().println(optimizedAst.getExpr()); } } @@ -193,7 +245,8 @@ public void subexpression_ast(@TestParameter CseTestOptimizer cseTestOptimizer) public void large_expressions_block_common_subexpr() throws Exception { CelOptimizer celOptimizer = newCseOptimizer( - CEL, SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()); + runtimeEnv.cel, + SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()); runLargeTestCases(celOptimizer); } @@ -202,7 +255,7 @@ public void large_expressions_block_common_subexpr() throws Exception { public void large_expressions_block_recursion_depth_1() throws Exception { CelOptimizer celOptimizer = newCseOptimizer( - CEL, + runtimeEnv.cel, SubexpressionOptimizerOptions.newBuilder() .populateMacroCalls(true) .subexpressionMaxRecursionDepth(1) @@ -215,7 +268,7 @@ public void large_expressions_block_recursion_depth_1() throws Exception { public void large_expressions_block_recursion_depth_2() throws Exception { CelOptimizer celOptimizer = newCseOptimizer( - CEL, + runtimeEnv.cel, SubexpressionOptimizerOptions.newBuilder() .populateMacroCalls(true) .subexpressionMaxRecursionDepth(2) @@ -228,7 +281,7 @@ public void large_expressions_block_recursion_depth_2() throws Exception { public void large_expressions_block_recursion_depth_3() throws Exception { CelOptimizer celOptimizer = newCseOptimizer( - CEL, + runtimeEnv.cel, SubexpressionOptimizerOptions.newBuilder() .populateMacroCalls(true) .subexpressionMaxRecursionDepth(3) @@ -238,15 +291,16 @@ public void large_expressions_block_recursion_depth_3() throws Exception { } private void runLargeTestCases(CelOptimizer celOptimizer) throws Exception { - for (CseLargeTestCase cseTestCase : CseLargeTestCase.values()) { + for (CseLargeTestCase cseTestCase : EnumSet.allOf(CseLargeTestCase.class)) { testOutput().println("Test case: " + cseTestCase.name()); testOutput().println("Source: " + cseTestCase.source); testOutput().println("=====>"); - CelAbstractSyntaxTree ast = CEL.compile(cseTestCase.source).getAst(); - + CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(cseTestCase.source).getAst(); CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); Object optimizedEvalResult = - CEL.createProgram(optimizedAst) + runtimeEnv + .cel + .createProgram(optimizedAst) .eval( ImmutableMap.of("msg", TEST_ALL_TYPES_INPUT, "x", 5L, "opt_x", Optional.of(5L))); testOutput().println("Result: " + optimizedEvalResult); @@ -260,33 +314,6 @@ private void runLargeTestCases(CelOptimizer celOptimizer) throws Exception { } } - private static CelBuilder newCelBuilder() { - return CelFactory.standardCelBuilder() - .addMessageTypes(TestAllTypes.getDescriptor()) - .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .setOptions(CelOptions.current().populateMacroCalls(true).build()) - .addCompilerLibraries( - CelExtensions.optional(), CelExtensions.bindings(), CelExtensions.comprehensions()) - .addRuntimeLibraries(CelExtensions.optional(), CelExtensions.comprehensions()) - .addFunctionDeclarations( - CelFunctionDecl.newFunctionDeclaration( - "pure_custom_func", - newGlobalOverload("pure_custom_func_overload", SimpleType.INT, SimpleType.INT)), - CelFunctionDecl.newFunctionDeclaration( - "non_pure_custom_func", - newGlobalOverload("non_pure_custom_func_overload", SimpleType.INT, SimpleType.INT))) - .addFunctionBindings( - // This is pure, but for the purposes of excluding it as a CSE candidate, pretend that - // it isn't. - CelFunctionBinding.from("non_pure_custom_func_overload", Long.class, val -> val), - CelFunctionBinding.from("pure_custom_func_overload", Long.class, val -> val)) - .addVar("x", SimpleType.DYN) - .addVar("y", SimpleType.DYN) - .addVar("opt_x", OptionalType.create(SimpleType.DYN)) - .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); - } - private static CelOptimizer newCseOptimizer(Cel cel, SubexpressionOptimizerOptions options) { return CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers(SubexpressionOptimizer.newInstance(options)) @@ -315,17 +342,23 @@ private enum CseTestOptimizer { BLOCK_RECURSION_DEPTH_9( OPTIMIZER_COMMON_OPTIONS.toBuilder().subexpressionMaxRecursionDepth(9).build()); - private final CelOptimizer cseOptimizer; - private final CelOptimizer cseWithConstFoldingOptimizer; + private final SubexpressionOptimizerOptions option; CseTestOptimizer(SubexpressionOptimizerOptions option) { - this.cseOptimizer = newCseOptimizer(CEL, option); - this.cseWithConstFoldingOptimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(CEL) - .addAstOptimizers( - ConstantFoldingOptimizer.getInstance(), - SubexpressionOptimizer.newInstance(option)) - .build(); + this.option = option; + } + + // Defers building the optimizer until the test runs + private CelOptimizer newCseOptimizer(RuntimeEnv env) { + return SubexpressionOptimizerBaselineTest.newCseOptimizer(env.cel, option); + } + + // Defers building the optimizer until the test runs + private CelOptimizer newCseWithConstFoldingOptimizer(RuntimeEnv env) { + return CelOptimizerFactory.standardCelOptimizerBuilder(env.cel) + .addAstOptimizers( + ConstantFoldingOptimizer.getInstance(), SubexpressionOptimizer.newInstance(option)) + .build(); } } diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java index 2289a7d4a..6e39bab28 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java @@ -26,6 +26,7 @@ import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.bundle.Cel; import dev.cel.bundle.CelBuilder; +import dev.cel.bundle.CelExperimentalFactory; import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelFunctionDecl; @@ -52,10 +53,14 @@ import dev.cel.parser.CelStandardMacro; import dev.cel.parser.CelUnparser; import dev.cel.parser.CelUnparserFactory; +import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelRuntime; import dev.cel.runtime.CelRuntimeFactory; +import dev.cel.runtime.CelUnknownSet; +import dev.cel.runtime.PartialVars; +import dev.cel.runtime.Program; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; @@ -64,10 +69,40 @@ @RunWith(TestParameterInjector.class) public class SubexpressionOptimizerTest { - private static final Cel CEL = newCelBuilder().build(); + private enum RuntimeEnv { + LEGACY( + setupCelEnv(CelFactory.standardCelBuilder()), + setupCelForEvaluatingBlock(CelFactory.standardCelBuilder())), + PLANNER( + setupCelEnv(CelExperimentalFactory.plannerCelBuilder()), + setupCelForEvaluatingBlock(CelExperimentalFactory.plannerCelBuilder())); - private static final Cel CEL_FOR_EVALUATING_BLOCK = - CelFactory.standardCelBuilder() + private final Cel cel; + private final Cel celForEvaluatingBlock; + + private static Cel setupCelEnv(CelBuilder celBuilder) { + return celBuilder + .addMessageTypes(TestAllTypes.getDescriptor()) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions( + CelOptions.current() + .populateMacroCalls(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .addCompilerLibraries(CelExtensions.bindings(), CelExtensions.strings()) + .addRuntimeLibraries(CelExtensions.strings()) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "non_pure_custom_func", + newGlobalOverload( + "non_pure_custom_func_overload", SimpleType.INT, SimpleType.INT))) + .addVar("x", SimpleType.DYN) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .build(); + } + + private static Cel setupCelForEvaluatingBlock(CelBuilder celBuilder) { + return celBuilder .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .addFunctionDeclarations( // These are test only declarations, as the actual function is made internal using @ @@ -98,6 +133,15 @@ public class SubexpressionOptimizerTest { .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) .build(); + } + + RuntimeEnv(Cel cel, Cel celForEvaluatingBlock) { + this.cel = cel; + this.celForEvaluatingBlock = celForEvaluatingBlock; + } + } + + @TestParameter RuntimeEnv runtimeEnv; private static final CelUnparser CEL_UNPARSER = CelUnparserFactory.newUnparser(); @@ -115,8 +159,8 @@ private static CelBuilder newCelBuilder() { .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); } - private static CelOptimizer newCseOptimizer(SubexpressionOptimizerOptions options) { - return CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + private CelOptimizer newCseOptimizer(SubexpressionOptimizerOptions options) { + return CelOptimizerFactory.standardCelOptimizerBuilder(runtimeEnv.cel) .addAstOptimizers(SubexpressionOptimizer.newInstance(options)) .build(); } @@ -130,15 +174,56 @@ public void cse_resultTypeSet_celBlockOptimizationSuccess() throws Exception { SubexpressionOptimizer.newInstance( SubexpressionOptimizerOptions.newBuilder().build())) .build(); - CelAbstractSyntaxTree ast = CEL.compile("size('a') + size('a') == 2").getAst(); + CelAbstractSyntaxTree ast = runtimeEnv.cel.compile("size('a') + size('a') == 2").getAst(); CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); - assertThat(CEL.createProgram(optimizedAst).eval()).isEqualTo(true); + assertThat(runtimeEnv.cel.createProgram(optimizedAst).eval()).isEqualTo(true); assertThat(CEL_UNPARSER.unparse(optimizedAst)) .isEqualTo("cel.@block([size(\"a\")], @index0 + @index0 == 2)"); } + @Test + public void cse_indexEvaluationErrors_throws() throws Exception { + CelAbstractSyntaxTree ast = + runtimeEnv.cel.compile("\"abc\".charAt(10) + \"abc\".charAt(10)").getAst(); + CelOptimizer optimizedOptimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(runtimeEnv.cel) + .addAstOptimizers(SubexpressionOptimizer.getInstance()) + .build(); + + CelAbstractSyntaxTree optimizedAst = optimizedOptimizer.optimize(ast); + + String unparsed = CEL_UNPARSER.unparse(optimizedAst); + assertThat(unparsed).isEqualTo("cel.@block([\"abc\".charAt(10)], @index0 + @index0)"); + + Program program = runtimeEnv.cel.createProgram(optimizedAst); + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> program.eval(ImmutableMap.of())); + assertThat(e).hasMessageThat().contains("charAt failure: Index out of range: 10"); + } + + @Test + public void cse_withUnknownAttributes() throws Exception { + CelAbstractSyntaxTree ast = runtimeEnv.cel.compile("size(\"a\") == 1 ? x.y : x.y").getAst(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(runtimeEnv.cel) + .addAstOptimizers(SubexpressionOptimizer.getInstance()) + .build(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo("cel.@block([x.y], (size(\"a\") == 1) ? @index0 : @index0)"); + + Object result = + runtimeEnv + .cel + .createProgram(optimizedAst) + .eval(PartialVars.of(CelAttributePattern.fromQualifiedIdentifier("x"))); + assertThat(result).isInstanceOf(CelUnknownSet.class); + } + private enum CseNoOpTestCase { // Nothing to optimize NO_COMMON_SUBEXPR("size(\"hello\")"), @@ -169,7 +254,7 @@ private enum CseNoOpTestCase { @Test public void cse_withCelBind_noop(@TestParameter CseNoOpTestCase testCase) throws Exception { - CelAbstractSyntaxTree ast = CEL.compile(testCase.source).getAst(); + CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(testCase.source).getAst(); CelAbstractSyntaxTree optimizedAst = newCseOptimizer(SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()) @@ -181,7 +266,7 @@ public void cse_withCelBind_noop(@TestParameter CseNoOpTestCase testCase) throws @Test public void cse_withCelBlock_noop(@TestParameter CseNoOpTestCase testCase) throws Exception { - CelAbstractSyntaxTree ast = CEL.compile(testCase.source).getAst(); + CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(testCase.source).getAst(); CelAbstractSyntaxTree optimizedAst = newCseOptimizer(SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()) @@ -194,7 +279,7 @@ public void cse_withCelBlock_noop(@TestParameter CseNoOpTestCase testCase) throw @Test public void cse_withComprehensionStructureRetained() throws Exception { CelAbstractSyntaxTree ast = - CEL.compile("['foo'].map(x, [x+x]) + ['foo'].map(x, [x+x, x+x])").getAst(); + runtimeEnv.cel.compile("['foo'].map(x, [x+x]) + ['foo'].map(x, [x+x, x+x])").getAst(); CelOptimizer celOptimizer = newCseOptimizer( SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()); @@ -210,10 +295,12 @@ public void cse_withComprehensionStructureRetained() throws Exception { @Test public void cse_applyConstFoldingBefore() throws Exception { CelAbstractSyntaxTree ast = - CEL.compile("size([1+1+1]) + size([1+1+1]) + size([1,1+1+1]) + size([1,1+1+1]) + x") + runtimeEnv + .cel + .compile("size([1+1+1]) + size([1+1+1]) + size([1,1+1+1]) + size([1,1+1+1]) + x") .getAst(); CelOptimizer optimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + CelOptimizerFactory.standardCelOptimizerBuilder(runtimeEnv.cel) .addAstOptimizers( ConstantFoldingOptimizer.getInstance(), SubexpressionOptimizer.newInstance( @@ -228,10 +315,12 @@ public void cse_applyConstFoldingBefore() throws Exception { @Test public void cse_applyConstFoldingAfter() throws Exception { CelAbstractSyntaxTree ast = - CEL.compile("size([1+1+1]) + size([1+1+1]) + size([1,1+1+1]) + size([1,1+1+1]) + x") + runtimeEnv + .cel + .compile("size([1+1+1]) + size([1+1+1]) + size([1,1+1+1]) + size([1,1+1+1]) + x") .getAst(); CelOptimizer optimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + CelOptimizerFactory.standardCelOptimizerBuilder(runtimeEnv.cel) .addAstOptimizers( SubexpressionOptimizer.newInstance( SubexpressionOptimizerOptions.newBuilder().build()), @@ -246,9 +335,9 @@ public void cse_applyConstFoldingAfter() throws Exception { @Test public void cse_applyConstFoldingAfter_nothingToFold() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("size(x) + size(x)").getAst(); + CelAbstractSyntaxTree ast = runtimeEnv.cel.compile("size(x) + size(x)").getAst(); CelOptimizer optimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + CelOptimizerFactory.standardCelOptimizerBuilder(runtimeEnv.cel) .addAstOptimizers( SubexpressionOptimizer.newInstance( SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()), @@ -271,7 +360,7 @@ public void iterationLimitReached_throws() throws Exception { largeExprBuilder.append("+"); } } - CelAbstractSyntaxTree ast = CEL.compile(largeExprBuilder.toString()).getAst(); + CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(largeExprBuilder.toString()).getAst(); CelOptimizationException e = assertThrows( @@ -287,9 +376,9 @@ public void iterationLimitReached_throws() throws Exception { @Test public void celBlock_astExtensionTagged() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("size(x) + size(x)").getAst(); + CelAbstractSyntaxTree ast = runtimeEnv.cel.compile("size(x) + size(x)").getAst(); CelOptimizer optimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + CelOptimizerFactory.standardCelOptimizerBuilder(runtimeEnv.cel) .addAstOptimizers( SubexpressionOptimizer.newInstance( SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()), @@ -322,7 +411,7 @@ private enum BlockTestCase { public void block_success(@TestParameter BlockTestCase testCase) throws Exception { CelAbstractSyntaxTree ast = compileUsingInternalFunctions(testCase.source); - Object evaluatedResult = CEL_FOR_EVALUATING_BLOCK.createProgram(ast).eval(); + Object evaluatedResult = runtimeEnv.celForEvaluatingBlock.createProgram(ast).eval(); assertThat(evaluatedResult).isNotNull(); } @@ -584,7 +673,7 @@ public void block_containsCycle_throws() throws Exception { CelAbstractSyntaxTree ast = compileUsingInternalFunctions("cel.block([index1,index0],index0)"); CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> CEL.createProgram(ast).eval()); + assertThrows(CelEvaluationException.class, () -> runtimeEnv.cel.createProgram(ast).eval()); assertThat(e).hasMessageThat().contains("Cycle detected: @index0"); } @@ -595,7 +684,7 @@ public void block_lazyEvaluationContainsError_cleansUpCycleState() throws Except "cel.block([1/0 > 0], (index0 && false) || (index0 && true))"); CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> CEL.createProgram(ast).eval()); + assertThrows(CelEvaluationException.class, () -> runtimeEnv.cel.createProgram(ast).eval()); assertThat(e).hasMessageThat().contains("/ by zero"); assertThat(e).hasMessageThat().doesNotContain("Cycle detected"); @@ -605,9 +694,10 @@ public void block_lazyEvaluationContainsError_cleansUpCycleState() throws Except * Converts AST containing cel.block related test functions to internal functions (e.g: cel.block * -> cel.@block) */ - private static CelAbstractSyntaxTree compileUsingInternalFunctions(String expression) + private CelAbstractSyntaxTree compileUsingInternalFunctions(String expression) throws CelValidationException { - CelAbstractSyntaxTree astToModify = CEL_FOR_EVALUATING_BLOCK.compile(expression).getAst(); + CelAbstractSyntaxTree astToModify = + runtimeEnv.celForEvaluatingBlock.compile(expression).getAst(); CelMutableAst mutableAst = CelMutableAst.fromCelAst(astToModify); CelNavigableMutableAst.fromAst(mutableAst) .getRoot() @@ -629,6 +719,6 @@ private static CelAbstractSyntaxTree compileUsingInternalFunctions(String expres indexExpr.ident().setName(internalIdentName); }); - return CEL_FOR_EVALUATING_BLOCK.check(mutableAst.toParsedAst()).getAst(); + return runtimeEnv.celForEvaluatingBlock.check(mutableAst.toParsedAst()).getAst(); } } diff --git a/optimizer/src/test/resources/constfold_before_subexpression_unparsed.baseline b/optimizer/src/test/resources/constfold_before_subexpression_unparsed.baseline index 55da856cd..9139c7a35 100644 --- a/optimizer/src/test/resources/constfold_before_subexpression_unparsed.baseline +++ b/optimizer/src/test/resources/constfold_before_subexpression_unparsed.baseline @@ -526,7 +526,7 @@ Result: [[[foofoo, foofoo, foofoo, foofoo], [foofoo, foofoo, foofoo, foofoo]], [ Test case: MACRO_SHADOWED_VARIABLE_COMP_V2_1 Source: [x - y - 1 > 3 ? x - y - 1 : 5].exists(x, y, x - y - 1 > 3) || x - y - 1 > 3 =====> -Result: CelUnknownSet{attributes=[], unknownExprIds=[6]} +Result: false [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([x - y - 1, @index0 > 3], [@index1 ? @index0 : 5].exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index1) [BLOCK_RECURSION_DEPTH_1]: cel.@block([x - y, @index0 - 1, @index1 > 3, @index2 ? @index1 : 5, [@index3]], @index4.exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index2) [BLOCK_RECURSION_DEPTH_2]: cel.@block([x - y - 1, @index0 > 3, [@index1 ? @index0 : 5]], @index2.exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index1) diff --git a/optimizer/src/test/resources/subexpression_unparsed.baseline b/optimizer/src/test/resources/subexpression_unparsed.baseline index e0edc8987..780664a14 100644 --- a/optimizer/src/test/resources/subexpression_unparsed.baseline +++ b/optimizer/src/test/resources/subexpression_unparsed.baseline @@ -526,7 +526,7 @@ Result: [[[foofoo, foofoo, foofoo, foofoo], [foofoo, foofoo, foofoo, foofoo]], [ Test case: MACRO_SHADOWED_VARIABLE_COMP_V2_1 Source: [x - y - 1 > 3 ? x - y - 1 : 5].exists(x, y, x - y - 1 > 3) || x - y - 1 > 3 =====> -Result: CelUnknownSet{attributes=[], unknownExprIds=[6]} +Result: false [BLOCK_COMMON_SUBEXPR_ONLY]: cel.@block([x - y - 1, @index0 > 3], [@index1 ? @index0 : 5].exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index1) [BLOCK_RECURSION_DEPTH_1]: cel.@block([x - y, @index0 - 1, @index1 > 3, @index2 ? @index1 : 5, [@index3]], @index4.exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index2) [BLOCK_RECURSION_DEPTH_2]: cel.@block([x - y - 1, @index0 > 3, [@index1 ? @index0 : 5]], @index2.exists(@it:0:0, @it2:0:0, @it:0:0 - @it2:0:0 - 1 > 3) || @index1) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index fc70118e4..cb2ad5a82 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -18,6 +18,7 @@ java_library( ":eval_and", ":eval_attribute", ":eval_binary", + ":eval_block", ":eval_conditional", ":eval_const", ":eval_create_list", @@ -67,7 +68,6 @@ java_library( srcs = ["PlannedProgram.java"], deps = [ ":error_metadata", - ":execution_frame", ":localized_evaluation_exception", ":planned_interpretable", "//:auto_value", @@ -92,11 +92,9 @@ java_library( name = "eval_const", srcs = ["EvalConstant.java"], deps = [ - ":execution_frame", ":planned_interpretable", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", ], ) @@ -123,7 +121,6 @@ java_library( deps = [ ":activation_wrapper", ":eval_helpers", - ":execution_frame", ":planned_interpretable", ":qualifier", "//common:container", @@ -183,8 +180,8 @@ java_library( srcs = ["EvalAttribute.java"], deps = [ ":attribute", - ":execution_frame", ":interpretable_attribute", + ":planned_interpretable", ":qualifier", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", @@ -195,8 +192,8 @@ java_library( name = "eval_test_only", srcs = ["EvalTestOnly.java"], deps = [ - ":execution_frame", ":interpretable_attribute", + ":planned_interpretable", ":presence_test_qualifier", ":qualifier", "//runtime:evaluation_exception", @@ -210,7 +207,6 @@ java_library( srcs = ["EvalZeroArity.java"], deps = [ ":eval_helpers", - ":execution_frame", ":planned_interpretable", "//common/values", "//runtime:evaluation_exception", @@ -224,7 +220,6 @@ java_library( srcs = ["EvalUnary.java"], deps = [ ":eval_helpers", - ":execution_frame", ":planned_interpretable", "//common/values", "//runtime:evaluation_exception", @@ -238,7 +233,6 @@ java_library( srcs = ["EvalBinary.java"], deps = [ ":eval_helpers", - ":execution_frame", ":planned_interpretable", "//common/values", "//runtime:accumulated_unknowns", @@ -253,7 +247,6 @@ java_library( srcs = ["EvalVarArgsCall.java"], deps = [ ":eval_helpers", - ":execution_frame", ":planned_interpretable", "//common/values", "//runtime:accumulated_unknowns", @@ -268,7 +261,6 @@ java_library( srcs = ["EvalLateBoundCall.java"], deps = [ ":eval_helpers", - ":execution_frame", ":planned_interpretable", "//common/exceptions:overload_not_found", "//common/values", @@ -285,7 +277,6 @@ java_library( srcs = ["EvalOr.java"], deps = [ ":eval_helpers", - ":execution_frame", ":planned_interpretable", "//common/values", "//runtime:accumulated_unknowns", @@ -299,7 +290,6 @@ java_library( srcs = ["EvalAnd.java"], deps = [ ":eval_helpers", - ":execution_frame", ":planned_interpretable", "//common/values", "//runtime:accumulated_unknowns", @@ -312,7 +302,6 @@ java_library( name = "eval_conditional", srcs = ["EvalConditional.java"], deps = [ - ":execution_frame", ":planned_interpretable", "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", @@ -326,7 +315,6 @@ java_library( srcs = ["EvalCreateStruct.java"], deps = [ ":eval_helpers", - ":execution_frame", ":planned_interpretable", "//common/types:type_providers", "//common/values", @@ -344,7 +332,6 @@ java_library( srcs = ["EvalCreateList.java"], deps = [ ":eval_helpers", - ":execution_frame", ":planned_interpretable", "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", @@ -359,7 +346,6 @@ java_library( srcs = ["EvalCreateMap.java"], deps = [ ":eval_helpers", - ":execution_frame", ":localized_evaluation_exception", ":planned_interpretable", "//common/exceptions:duplicate_key", @@ -377,7 +363,6 @@ java_library( srcs = ["EvalFold.java"], deps = [ ":activation_wrapper", - ":execution_frame", ":planned_interpretable", "//runtime:accumulated_unknowns", "//runtime:concatenated_list_view", @@ -389,24 +374,10 @@ java_library( ], ) -java_library( - name = "execution_frame", - srcs = ["ExecutionFrame.java"], - deps = [ - "//common:options", - "//common/exceptions:iteration_budget_exceeded", - "//runtime:evaluation_exception", - "//runtime:function_resolver", - "//runtime:partial_vars", - "//runtime:resolved_overload", - ], -) - java_library( name = "eval_helpers", srcs = ["EvalHelpers.java"], deps = [ - ":execution_frame", ":localized_evaluation_exception", ":planned_interpretable", "//common:error_codes", @@ -440,11 +411,20 @@ java_library( java_library( name = "planned_interpretable", - srcs = ["PlannedInterpretable.java"], + srcs = [ + "BlockMemoizer.java", + "ExecutionFrame.java", + "PlannedInterpretable.java", + ], deps = [ - ":execution_frame", + ":localized_evaluation_exception", + "//common:options", + "//common/exceptions:iteration_budget_exceeded", "//runtime:evaluation_exception", + "//runtime:function_resolver", "//runtime:interpretable", + "//runtime:partial_vars", + "//runtime:resolved_overload", "@maven//:com_google_errorprone_error_prone_annotations", ], ) @@ -454,7 +434,6 @@ java_library( srcs = ["EvalOptionalOr.java"], deps = [ ":eval_helpers", - ":execution_frame", ":planned_interpretable", "//common/exceptions:overload_not_found", "//runtime:accumulated_unknowns", @@ -469,7 +448,6 @@ java_library( srcs = ["EvalOptionalOrValue.java"], deps = [ ":eval_helpers", - ":execution_frame", ":planned_interpretable", "//common/exceptions:overload_not_found", "//runtime:accumulated_unknowns", @@ -484,7 +462,6 @@ java_library( srcs = ["EvalOptionalSelectField.java"], deps = [ ":eval_helpers", - ":execution_frame", ":planned_interpretable", "//common/values", "//runtime:accumulated_unknowns", @@ -493,3 +470,14 @@ java_library( "@maven//:com_google_guava_guava", ], ) + +java_library( + name = "eval_block", + srcs = ["EvalBlock.java"], + deps = [ + ":planned_interpretable", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BlockMemoizer.java b/runtime/src/main/java/dev/cel/runtime/planner/BlockMemoizer.java new file mode 100644 index 000000000..978029b3d --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/BlockMemoizer.java @@ -0,0 +1,72 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.GlobalResolver; +import java.util.Arrays; + +/** Handles memoization, lazy evaluation, and cycle detection for cel.@block slots. */ +final class BlockMemoizer { + + private static final Object IN_PROGRESS = new Object(); + private static final Object UNSET = new Object(); + + private final PlannedInterpretable[] slotExprs; + private final Object[] slotVals; + private final ExecutionFrame frame; + + static BlockMemoizer create(PlannedInterpretable[] slotExprs, ExecutionFrame frame) { + return new BlockMemoizer(slotExprs, frame); + } + + private BlockMemoizer(PlannedInterpretable[] slotExprs, ExecutionFrame frame) { + this.slotExprs = slotExprs; + this.frame = frame; + this.slotVals = new Object[slotExprs.length]; + Arrays.fill(this.slotVals, UNSET); + } + + Object resolveSlot(int idx, GlobalResolver resolver) { + Object val = slotVals[idx]; + + // Already evaluated + if (val != UNSET && val != IN_PROGRESS) { + if (val instanceof RuntimeException) { + throw (RuntimeException) val; + } + return val; + } + + if (val == IN_PROGRESS) { + throw new IllegalStateException("Cycle detected: @index" + idx); + } + + slotVals[idx] = IN_PROGRESS; + try { + Object result = slotExprs[idx].eval(resolver, frame); + slotVals[idx] = result; + return result; + } catch (CelEvaluationException e) { + LocalizedEvaluationException localizedException = + new LocalizedEvaluationException(e, e.getErrorCode(), slotExprs[idx].exprId()); + slotVals[idx] = localizedException; + throw localizedException; + } catch (RuntimeException e) { + slotVals[idx] = e; + throw e; + } + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalBlock.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalBlock.java new file mode 100644 index 000000000..41ad4034e --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalBlock.java @@ -0,0 +1,67 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.GlobalResolver; + +/** Eval implementation of {@code cel.@block}. */ +@Immutable +final class EvalBlock extends PlannedInterpretable { + + @SuppressWarnings("Immutable") // Array not mutated after creation + private final PlannedInterpretable[] slotExprs; + + private final PlannedInterpretable resultExpr; + + static EvalBlock create( + long exprId, PlannedInterpretable[] slotExprs, PlannedInterpretable resultExpr) { + return new EvalBlock(exprId, slotExprs, resultExpr); + } + + private EvalBlock( + long exprId, PlannedInterpretable[] slotExprs, PlannedInterpretable resultExpr) { + super(exprId); + this.slotExprs = slotExprs; + this.resultExpr = resultExpr; + } + + @Override + public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + BlockMemoizer memoizer = BlockMemoizer.create(slotExprs, frame); + frame.setBlockMemoizer(memoizer); + return resultExpr.eval(resolver, frame); + } + + @Immutable + static final class EvalBlockSlot extends PlannedInterpretable { + private final int slotIndex; + + static EvalBlockSlot create(long exprId, int slotIndex) { + return new EvalBlockSlot(exprId, slotIndex); + } + + private EvalBlockSlot(long exprId, int slotIndex) { + super(exprId); + this.slotIndex = slotIndex; + } + + @Override + public Object eval(GlobalResolver resolver, ExecutionFrame frame) { + return frame.getBlockMemoizer().resolveSlot(slotIndex, resolver); + } + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java b/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java index e29c68dd8..282b7c83a 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java @@ -30,6 +30,7 @@ final class ExecutionFrame { private final CelFunctionResolver functionResolver; private final PartialVars partialVars; private int iterationCount; + private BlockMemoizer blockMemoizer; Optional findOverload( String functionName, Collection overloadIds, Object[] args) @@ -49,6 +50,17 @@ void incrementIterations() { } } + void setBlockMemoizer(BlockMemoizer blockMemoizer) { + if (this.blockMemoizer != null) { + throw new IllegalStateException("BlockMemoizer is already initialized"); + } + this.blockMemoizer = blockMemoizer; + } + + BlockMemoizer getBlockMemoizer() { + return blockMemoizer; + } + static ExecutionFrame create( CelFunctionResolver functionResolver, PartialVars partialVars, CelOptions celOptions) { return new ExecutionFrame( diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index add918f64..9bd5f3ecd 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -164,6 +164,11 @@ private PlannedInterpretable planIdent(CelExpr celExpr, PlannerContext ctx) { } String identName = celExpr.ident().name(); + PlannedInterpretable blockSlot = maybeInterceptBlockSlot(celExpr.id(), identName).orElse(null); + if (blockSlot != null) { + return blockSlot; + } + if (ctx.isLocalVar(identName)) { return EvalAttribute.create(celExpr.id(), attributeFactory.newAbsoluteAttribute(identName)); } @@ -196,11 +201,42 @@ private PlannedInterpretable planCheckedIdent( return EvalConstant.create(id, identType); } + String identName = identRef.name(); + PlannedInterpretable blockSlot = maybeInterceptBlockSlot(id, identName).orElse(null); + if (blockSlot != null) { + return blockSlot; + } + return EvalAttribute.create(id, attributeFactory.newAbsoluteAttribute(identRef.name())); } + private Optional maybeInterceptBlockSlot(long id, String identName) { + if (!identName.startsWith("@index")) { + return Optional.empty(); + } + if (identName.length() <= 6) { + throw new IllegalArgumentException("Malformed block slot identifier: " + identName); + } + try { + int slotIndex = Integer.parseInt(identName.substring(6)); + if (slotIndex < 0) { + throw new IllegalArgumentException("Negative block slot index: " + identName); + } + return Optional.of(EvalBlock.EvalBlockSlot.create(id, slotIndex)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid block slot index: " + identName, e); + } + } + private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { ResolvedFunction resolvedFunction = resolveFunction(expr, ctx.referenceMap()); + String functionName = resolvedFunction.functionName(); + + PlannedInterpretable blockCall = maybeInterceptBlockCall(functionName, expr, ctx).orElse(null); + if (blockCall != null) { + return blockCall; + } + CelExpr target = resolvedFunction.target().orElse(null); int argCount = expr.call().args().size(); if (target != null) { @@ -220,7 +256,6 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { evaluatedArgs[argIndex + offset] = plan(args.get(argIndex), ctx); } - String functionName = resolvedFunction.functionName(); Operator operator = Operator.findReverse(functionName).orElse(null); if (operator != null) { switch (operator) { @@ -285,6 +320,28 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { } } + private Optional maybeInterceptBlockCall( + String functionName, CelExpr expr, PlannerContext ctx) { + if (!functionName.equals("cel.@block")) { + return Optional.empty(); + } + + CelCall blockCall = expr.call(); + + if (blockCall.args().size() != 2) { + throw new IllegalArgumentException( + "Expected 2 arguments for cel.@block call. Got: " + blockCall.args().size()); + } + + CelList exprList = blockCall.args().get(0).list(); + PlannedInterpretable[] slotExprs = new PlannedInterpretable[exprList.elements().size()]; + for (int i = 0; i < slotExprs.length; i++) { + slotExprs[i] = plan(exprList.elements().get(i), ctx); + } + PlannedInterpretable resultExpr = plan(blockCall.args().get(1), ctx); + return Optional.of(EvalBlock.create(expr.id(), slotExprs, resultExpr)); + } + /** * Intercepts a potential optional function call. * From 288c3b935d7c1a3df47921a73b3605e347649b02 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 13 Apr 2026 17:41:17 -0700 Subject: [PATCH 043/204] Add cel.@block test coverage for parsed-only mode PiperOrigin-RevId: 899267021 --- .../CelComprehensionsExtensions.java | 35 +++++++------------ .../SubexpressionOptimizerBaselineTest.java | 31 ++++++++++++++-- .../SubexpressionOptimizerTest.java | 23 +++++++++++- 3 files changed, 63 insertions(+), 26 deletions(-) diff --git a/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java index 23663f02e..7c298a773 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java @@ -118,29 +118,18 @@ public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) { @Override public void setRuntimeOptions( CelRuntimeBuilder runtimeBuilder, RuntimeEquality runtimeEquality, CelOptions celOptions) { - for (Function function : functions) { - for (CelOverloadDecl overload : function.functionDecl.overloads()) { - switch (overload.overloadId()) { - case MAP_INSERT_OVERLOAD_MAP_MAP: - runtimeBuilder.addFunctionBindings( - CelFunctionBinding.from( - MAP_INSERT_OVERLOAD_MAP_MAP, - Map.class, - Map.class, - (map1, map2) -> mapInsertMap(map1, map2, runtimeEquality))); - break; - case MAP_INSERT_OVERLOAD_KEY_VALUE: - runtimeBuilder.addFunctionBindings( - CelFunctionBinding.from( - MAP_INSERT_OVERLOAD_KEY_VALUE, - ImmutableList.of(Map.class, Object.class, Object.class), - args -> mapInsertKeyValue(args, runtimeEquality))); - break; - default: - // Nothing to add. - } - } - } + runtimeBuilder.addFunctionBindings( + CelFunctionBinding.fromOverloads( + MAP_INSERT_FUNCTION, + CelFunctionBinding.from( + MAP_INSERT_OVERLOAD_MAP_MAP, + Map.class, + Map.class, + (map1, map2) -> mapInsertMap(map1, map2, runtimeEquality)), + CelFunctionBinding.from( + MAP_INSERT_OVERLOAD_KEY_VALUE, + ImmutableList.of(Map.class, Object.class, Object.class), + args -> mapInsertKeyValue(args, runtimeEquality)))); } @Override diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java index 74e3b5b32..9db04ceac 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java @@ -83,8 +83,13 @@ private static Cel setupCelEnv(CelBuilder celBuilder) { .addFunctionBindings( // This is pure, but for the purposes of excluding it as a CSE candidate, pretend that // it isn't. - CelFunctionBinding.from("non_pure_custom_func_overload", Long.class, val -> val), - CelFunctionBinding.from("pure_custom_func_overload", Long.class, val -> val)) + CelFunctionBinding.fromOverloads( + "non_pure_custom_func", + CelFunctionBinding.from("non_pure_custom_func_overload", Long.class, val -> val))) + .addFunctionBindings( + CelFunctionBinding.fromOverloads( + "pure_custom_func", + CelFunctionBinding.from("pure_custom_func_overload", Long.class, val -> val))) .addVar("x", SimpleType.DYN) .addVar("y", SimpleType.DYN) .addVar("opt_x", OptionalType.create(SimpleType.DYN)) @@ -153,6 +158,28 @@ public void allOptimizers_producesSameEvaluationResult( assertThat(optimizedEvalResult).isEqualTo(expectedEvalResult); } + @Test + public void allOptimizers_producesSameEvaluationResult_parsedOnly( + @TestParameter CseTestCase cseTestCase, @TestParameter CseTestOptimizer cseTestOptimizer) + throws Exception { + skipBaselineVerification(); + if (runtimeEnv == RuntimeEnv.LEGACY) { + return; + } + CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(cseTestCase.source).getAst(); + ImmutableMap inputMap = + ImmutableMap.of("msg", TEST_ALL_TYPES_INPUT, "x", 5L, "y", 6L, "opt_x", Optional.of(5L)); + Object expectedEvalResult = runtimeEnv.cel.createProgram(ast).eval(inputMap); + + CelAbstractSyntaxTree optimizedAst = cseTestOptimizer.newCseOptimizer(runtimeEnv).optimize(ast); + CelAbstractSyntaxTree parsedOnlyOptimizedAst = + CelAbstractSyntaxTree.newParsedAst(optimizedAst.getExpr(), optimizedAst.getSource()); + + Object optimizedEvalResult = + runtimeEnv.cel.createProgram(parsedOnlyOptimizedAst).eval(inputMap); + assertThat(optimizedEvalResult).isEqualTo(expectedEvalResult); + } + @Test public void subexpression_unparsed() throws Exception { for (CseTestCase cseTestCase : EnumSet.allOf(CseTestCase.class)) { diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java index 6e39bab28..23459e5d8 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java @@ -416,6 +416,19 @@ public void block_success(@TestParameter BlockTestCase testCase) throws Exceptio assertThat(evaluatedResult).isNotNull(); } + @Test + public void block_success_parsedOnly(@TestParameter BlockTestCase testCase) throws Exception { + if (runtimeEnv == RuntimeEnv.LEGACY) { + return; + } + CelAbstractSyntaxTree ast = + compileUsingInternalFunctions(testCase.source, /* parsedOnly= */ true); + + Object evaluatedResult = runtimeEnv.celForEvaluatingBlock.createProgram(ast).eval(); + + assertThat(evaluatedResult).isNotNull(); + } + @Test @SuppressWarnings("Immutable") // Test only public void lazyEval_blockIndexNeverReferenced() throws Exception { @@ -694,7 +707,7 @@ public void block_lazyEvaluationContainsError_cleansUpCycleState() throws Except * Converts AST containing cel.block related test functions to internal functions (e.g: cel.block * -> cel.@block) */ - private CelAbstractSyntaxTree compileUsingInternalFunctions(String expression) + private CelAbstractSyntaxTree compileUsingInternalFunctions(String expression, boolean parsedOnly) throws CelValidationException { CelAbstractSyntaxTree astToModify = runtimeEnv.celForEvaluatingBlock.compile(expression).getAst(); @@ -719,6 +732,14 @@ private CelAbstractSyntaxTree compileUsingInternalFunctions(String expression) indexExpr.ident().setName(internalIdentName); }); + if (parsedOnly) { + return mutableAst.toParsedAst(); + } return runtimeEnv.celForEvaluatingBlock.check(mutableAst.toParsedAst()).getAst(); } + + private CelAbstractSyntaxTree compileUsingInternalFunctions(String expression) + throws CelValidationException { + return compileUsingInternalFunctions(expression, false); + } } From 207dca5fa5be53a60099f5f47167d57bbf134013 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 13 Apr 2026 17:50:57 -0700 Subject: [PATCH 044/204] Refactor tests to inject a runtime environment to invoke planner and legacy tests PiperOrigin-RevId: 899271038 --- .../src/test/java/dev/cel/bundle/BUILD.bazel | 2 +- .../test/java/dev/cel/bundle/CelImplTest.java | 60 +++--- .../dev/cel/optimizer/optimizers/BUILD.bazel | 2 +- .../ConstantFoldingOptimizerTest.java | 151 ++++++------- .../SubexpressionOptimizerBaselineTest.java | 152 ++++++-------- .../SubexpressionOptimizerTest.java | 198 ++++++++---------- testing/BUILD.bazel | 5 + .../src/main/java/dev/cel/testing/BUILD.bazel | 9 + .../dev/cel/testing/CelRuntimeFlavor.java | 38 ++++ 9 files changed, 306 insertions(+), 311 deletions(-) create mode 100644 testing/src/main/java/dev/cel/testing/CelRuntimeFlavor.java diff --git a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel index 2901e1ff9..265f6d89c 100644 --- a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel +++ b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel @@ -17,7 +17,6 @@ java_library( deps = [ "//:java_truth", "//bundle:cel", - "//bundle:cel_experimental_factory", "//bundle:cel_impl", "//bundle:environment", "//bundle:environment_exception", @@ -56,6 +55,7 @@ java_library( "//runtime:evaluation_listener", "//runtime:function_binding", "//runtime:unknown_attributes", + "//testing:cel_runtime_flavor", "//testing/protos:single_file_extension_java_proto", "//testing/protos:single_file_java_proto", "@cel_spec//proto/cel/expr:checked_java_proto", diff --git a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java index 22ef7e2f4..a3ad60d40 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java @@ -114,6 +114,7 @@ import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.CelVariableResolver; import dev.cel.runtime.UnknownContext; +import dev.cel.testing.CelRuntimeFlavor; import dev.cel.testing.testdata.SingleFile; import dev.cel.testing.testdata.SingleFileExtensionsProto; import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; @@ -2144,8 +2145,9 @@ public void toBuilder_isImmutable() { } @Test - public void eval_withJsonFieldName(@TestParameter RuntimeEnv runtimeEnv) throws Exception { - Cel cel = runtimeEnv.cel; + public void eval_withJsonFieldName(@TestParameter CelRuntimeFlavor runtimeFlavor) + throws Exception { + Cel cel = setupEnv(runtimeFlavor.builder()); CelAbstractSyntaxTree ast = cel.compile( "file.int32_snake_case_json_name == 1 && " @@ -2176,8 +2178,9 @@ public void eval_withJsonFieldName(@TestParameter RuntimeEnv runtimeEnv) throws } @Test - public void eval_withJsonFieldName_fieldsFallBack(@TestParameter RuntimeEnv runtimeEnv) throws Exception { - Cel cel = runtimeEnv.cel; + public void eval_withJsonFieldName_fieldsFallBack(@TestParameter CelRuntimeFlavor runtimeFlavor) + throws Exception { + Cel cel = setupEnv(runtimeFlavor.builder()); CelAbstractSyntaxTree ast = cel.compile( "dyn(file).int32_snake_case_json_name == 1 && " @@ -2206,8 +2209,9 @@ public void eval_withJsonFieldName_fieldsFallBack(@TestParameter RuntimeEnv runt } @Test - public void eval_withJsonFieldName_extensionFields(@TestParameter RuntimeEnv runtimeEnv) throws Exception { - Cel cel = runtimeEnv.cel; + public void eval_withJsonFieldName_extensionFields(@TestParameter CelRuntimeFlavor runtimeFlavor) + throws Exception { + Cel cel = setupEnv(runtimeFlavor.builder()); CelAbstractSyntaxTree ast = cel.compile( "proto.getExt(file, dev.cel.testing.testdata.int64CamelCaseJsonName) == 5 &&" @@ -2317,33 +2321,21 @@ private static TypeProvider aliasingProvider(ImmutableMap typeAlia }; } - private enum RuntimeEnv { - LEGACY(setupEnv(CelFactory.standardCelBuilder())), - PLANNER(setupEnv(CelExperimentalFactory.plannerCelBuilder())) - ; - - private final Cel cel; - - private static Cel setupEnv(CelBuilder celBuilder) { - ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); - SingleFileExtensionsProto.registerAllExtensions(extensionRegistry); - return celBuilder - .addVar("file", StructTypeReference.create(SingleFile.getDescriptor().getFullName())) - .addMessageTypes(SingleFile.getDescriptor()) - .addFileTypes(SingleFileExtensionsProto.getDescriptor()) - .addCompilerLibraries(CelExtensions.protos()) - .setExtensionRegistry(extensionRegistry) - .setOptions( - CelOptions.current() - .enableJsonFieldNames(true) - .enableHeterogeneousNumericComparisons(true) - .enableQuotedIdentifierSyntax(true) - .build()) - .build(); - } - - RuntimeEnv(Cel cel) { - this.cel = cel; - } + private static Cel setupEnv(CelBuilder celBuilder) { + ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); + SingleFileExtensionsProto.registerAllExtensions(extensionRegistry); + return celBuilder + .addVar("file", StructTypeReference.create(SingleFile.getDescriptor().getFullName())) + .addMessageTypes(SingleFile.getDescriptor()) + .addFileTypes(SingleFileExtensionsProto.getDescriptor()) + .addCompilerLibraries(CelExtensions.protos()) + .setExtensionRegistry(extensionRegistry) + .setOptions( + CelOptions.current() + .enableJsonFieldNames(true) + .enableHeterogeneousNumericComparisons(true) + .enableQuotedIdentifierSyntax(true) + .build()) + .build(); } } diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel index 734aa6879..d1220a41a 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -11,7 +11,6 @@ java_library( deps = [ # "//java/com/google/testing/testsize:annotations", "//bundle:cel", - "//bundle:cel_experimental_factory", "//common:cel_ast", "//common:cel_source", "//common:compiler_common", @@ -37,6 +36,7 @@ java_library( "//runtime:program", "//runtime:unknown_attributes", "//testing:baseline_test_case", + "//testing:cel_runtime_flavor", "@maven//:junit_junit", "@maven//:com_google_testparameterinjector_test_parameter_injector", "//:java_truth", diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java index bbb5c6e7e..33dc2d941 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java @@ -23,8 +23,6 @@ import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.bundle.Cel; import dev.cel.bundle.CelBuilder; -import dev.cel.bundle.CelExperimentalFactory; -import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; @@ -44,6 +42,8 @@ import dev.cel.parser.CelUnparser; import dev.cel.parser.CelUnparserFactory; import dev.cel.runtime.CelFunctionBinding; +import dev.cel.testing.CelRuntimeFlavor; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -57,72 +57,57 @@ public class ConstantFoldingOptimizerTest { private static final CelUnparser CEL_UNPARSER = CelUnparserFactory.newUnparser(); - @SuppressWarnings("ImmutableEnumChecker") // test only - private enum RuntimeEnv { - LEGACY(setupEnv(CelFactory.standardCelBuilder())), - PLANNER(setupEnv(CelExperimentalFactory.plannerCelBuilder())); - - private final Cel cel; - private final CelOptimizer celOptimizer; - - private static Cel setupEnv(CelBuilder celBuilder) { - return celBuilder - .addVar("x", SimpleType.DYN) - .addVar("y", SimpleType.DYN) - .addVar("list_var", ListType.create(SimpleType.STRING)) - .addVar("map_var", MapType.create(SimpleType.STRING, SimpleType.STRING)) - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .addFunctionDeclarations( - CelFunctionDecl.newFunctionDeclaration( - "get_true", - CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL)), - CelFunctionDecl.newFunctionDeclaration( - "get_list", - CelOverloadDecl.newGlobalOverload( - "get_list_overload", - ListType.create(SimpleType.INT), - ListType.create(SimpleType.INT)))) - .addFunctionBindings( - CelFunctionBinding.from("get_true_overload", ImmutableList.of(), unused -> true)) - .addMessageTypes(TestAllTypes.getDescriptor()) - .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) - .setOptions(CEL_OPTIONS) - .addCompilerLibraries( - CelExtensions.bindings(), - CelOptionalLibrary.INSTANCE, - CelExtensions.math(CEL_OPTIONS), - CelExtensions.strings(), - CelExtensions.sets(CEL_OPTIONS), - CelExtensions.encoders(CEL_OPTIONS)) - .addRuntimeLibraries( - CelOptionalLibrary.INSTANCE, - CelExtensions.math(CEL_OPTIONS), - CelExtensions.strings(), - CelExtensions.sets(CEL_OPTIONS), - CelExtensions.encoders(CEL_OPTIONS)) - .build(); - } - - RuntimeEnv(Cel cel) { - this.cel = cel; - this.celOptimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(cel) - .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) - .build(); - } - - private CelBuilder newCelBuilder() { - switch (this) { - case LEGACY: - return CelFactory.standardCelBuilder(); - case PLANNER: - return CelExperimentalFactory.plannerCelBuilder(); - } - throw new AssertionError("Unknown RuntimeEnv: " + this); - } + @TestParameter CelRuntimeFlavor runtimeFlavor; + + private Cel cel; + private CelOptimizer celOptimizer; + + @Before + public void setUp() { + this.cel = setupEnv(runtimeFlavor.builder()); + this.celOptimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(this.cel) + .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) + .build(); } - @TestParameter RuntimeEnv runtimeEnv; + private static Cel setupEnv(CelBuilder celBuilder) { + return celBuilder + .addVar("x", SimpleType.DYN) + .addVar("y", SimpleType.DYN) + .addVar("list_var", ListType.create(SimpleType.STRING)) + .addVar("map_var", MapType.create(SimpleType.STRING, SimpleType.STRING)) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "get_true", + CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL)), + CelFunctionDecl.newFunctionDeclaration( + "get_list", + CelOverloadDecl.newGlobalOverload( + "get_list_overload", + ListType.create(SimpleType.INT), + ListType.create(SimpleType.INT)))) + .addFunctionBindings( + CelFunctionBinding.from("get_true_overload", ImmutableList.of(), unused -> true)) + .addMessageTypes(TestAllTypes.getDescriptor()) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .setOptions(CEL_OPTIONS) + .addCompilerLibraries( + CelExtensions.bindings(), + CelOptionalLibrary.INSTANCE, + CelExtensions.math(CEL_OPTIONS), + CelExtensions.strings(), + CelExtensions.sets(CEL_OPTIONS), + CelExtensions.encoders(CEL_OPTIONS)) + .addRuntimeLibraries( + CelOptionalLibrary.INSTANCE, + CelExtensions.math(CEL_OPTIONS), + CelExtensions.strings(), + CelExtensions.sets(CEL_OPTIONS), + CelExtensions.encoders(CEL_OPTIONS)) + .build(); + } @Test @TestParameters("{source: 'null', expected: 'null'}") @@ -270,9 +255,9 @@ private CelBuilder newCelBuilder() { // TODO: Support folding lists with mixed types. This requires mutable lists. // @TestParameters("{source: 'dyn([1]) + [1.0]'}") public void constantFold_success(String source, String expected) throws Exception { - CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(source).getAst(); + CelAbstractSyntaxTree ast = cel.compile(source).getAst(); - CelAbstractSyntaxTree optimizedAst = runtimeEnv.celOptimizer.optimize(ast); + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo(expected); } @@ -317,8 +302,8 @@ public void constantFold_success(String source, String expected) throws Exceptio public void constantFold_macros_macroCallMetadataPopulated(String source, String expected) throws Exception { Cel cel = - runtimeEnv - .newCelBuilder() + runtimeFlavor + .builder() .addVar("x", SimpleType.DYN) .addVar("y", SimpleType.DYN) .addMessageTypes(TestAllTypes.getDescriptor()) @@ -363,8 +348,8 @@ public void constantFold_macros_macroCallMetadataPopulated(String source, String @TestParameters("{source: 'false ? false : cel.bind(a, true, a)'}") public void constantFold_macros_withoutMacroCallMetadata(String source) throws Exception { Cel cel = - runtimeEnv - .newCelBuilder() + runtimeFlavor + .builder() .addVar("x", SimpleType.DYN) .addVar("y", SimpleType.DYN) .addMessageTypes(TestAllTypes.getDescriptor()) @@ -418,20 +403,20 @@ public void constantFold_macros_withoutMacroCallMetadata(String source) throws E @TestParameters("{source: 'get_list([1, 2]).map(x, x * 2)'}") @TestParameters("{source: '[(x - 1 > 3) ? (x - 1) : 5].exists(x, x - 1 > 3)'}") public void constantFold_noOp(String source) throws Exception { - CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(source).getAst(); + CelAbstractSyntaxTree ast = cel.compile(source).getAst(); - CelAbstractSyntaxTree optimizedAst = runtimeEnv.celOptimizer.optimize(ast); + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo(source); } @Test public void constantFold_addFoldableFunction_success() throws Exception { - CelAbstractSyntaxTree ast = runtimeEnv.cel.compile("get_true() == get_true()").getAst(); + CelAbstractSyntaxTree ast = cel.compile("get_true() == get_true()").getAst(); ConstantFoldingOptions options = ConstantFoldingOptions.newBuilder().addFoldableFunctions("get_true").build(); CelOptimizer optimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(runtimeEnv.cel) + CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers(ConstantFoldingOptimizer.newInstance(options)) .build(); @@ -442,7 +427,7 @@ public void constantFold_addFoldableFunction_success() throws Exception { @Test public void constantFold_withExpectedResultTypeSet_success() throws Exception { - Cel cel = runtimeEnv.newCelBuilder().setResultType(SimpleType.STRING).build(); + Cel cel = runtimeFlavor.builder().setResultType(SimpleType.STRING).build(); CelOptimizer optimizer = CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) @@ -458,8 +443,8 @@ public void constantFold_withExpectedResultTypeSet_success() throws Exception { public void constantFold_withMacroCallPopulated_comprehensionsAreReplacedWithNotSet() throws Exception { Cel cel = - runtimeEnv - .newCelBuilder() + runtimeFlavor + .builder() .addVar("x", SimpleType.DYN) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .setOptions(CEL_OPTIONS) @@ -532,9 +517,9 @@ public void constantFold_withMacroCallPopulated_comprehensionsAreReplacedWithNot @Test public void constantFold_astProducesConsistentlyNumberedIds() throws Exception { - CelAbstractSyntaxTree ast = runtimeEnv.cel.compile("[1] + [2] + [3]").getAst(); + CelAbstractSyntaxTree ast = cel.compile("[1] + [2] + [3]").getAst(); - CelAbstractSyntaxTree optimizedAst = runtimeEnv.celOptimizer.optimize(ast); + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); assertThat(optimizedAst.getExpr().toString()) .isEqualTo( @@ -555,8 +540,8 @@ public void iterationLimitReached_throws() throws Exception { sb.append(" + ").append(i); } // 0 + 1 + 2 + 3 + ... 200 Cel cel = - runtimeEnv - .newCelBuilder() + runtimeFlavor + .builder() .setOptions( CelOptions.current() .enableHeterogeneousNumericComparisons(true) diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java index 9db04ceac..04e4e6a1d 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerBaselineTest.java @@ -24,8 +24,6 @@ // import com.google.testing.testsize.MediumTest; import dev.cel.bundle.Cel; import dev.cel.bundle.CelBuilder; -import dev.cel.bundle.CelExperimentalFactory; -import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; @@ -44,6 +42,7 @@ import dev.cel.parser.CelUnparserFactory; import dev.cel.runtime.CelFunctionBinding; import dev.cel.testing.BaselineTestCase; +import dev.cel.testing.CelRuntimeFlavor; import java.util.EnumSet; import java.util.Optional; import org.junit.Before; @@ -53,53 +52,41 @@ // @MediumTest @RunWith(TestParameterInjector.class) public class SubexpressionOptimizerBaselineTest extends BaselineTestCase { - private enum RuntimeEnv { - LEGACY(setupCelEnv(CelFactory.standardCelBuilder())), - PLANNER(setupCelEnv(CelExperimentalFactory.plannerCelBuilder())); - - private final Cel cel; - - private static Cel setupCelEnv(CelBuilder celBuilder) { - return celBuilder - .addMessageTypes(TestAllTypes.getDescriptor()) - .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .setOptions( - CelOptions.current() - .populateMacroCalls(true) - .enableHeterogeneousNumericComparisons(true) - .build()) - .addCompilerLibraries( - CelExtensions.optional(), CelExtensions.bindings(), CelExtensions.comprehensions()) - .addRuntimeLibraries(CelExtensions.optional(), CelExtensions.comprehensions()) - .addFunctionDeclarations( - CelFunctionDecl.newFunctionDeclaration( - "pure_custom_func", - newGlobalOverload("pure_custom_func_overload", SimpleType.INT, SimpleType.INT)), - CelFunctionDecl.newFunctionDeclaration( - "non_pure_custom_func", - newGlobalOverload( - "non_pure_custom_func_overload", SimpleType.INT, SimpleType.INT))) - .addFunctionBindings( - // This is pure, but for the purposes of excluding it as a CSE candidate, pretend that - // it isn't. - CelFunctionBinding.fromOverloads( - "non_pure_custom_func", - CelFunctionBinding.from("non_pure_custom_func_overload", Long.class, val -> val))) - .addFunctionBindings( - CelFunctionBinding.fromOverloads( - "pure_custom_func", - CelFunctionBinding.from("pure_custom_func_overload", Long.class, val -> val))) - .addVar("x", SimpleType.DYN) - .addVar("y", SimpleType.DYN) - .addVar("opt_x", OptionalType.create(SimpleType.DYN)) - .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) - .build(); - } - - RuntimeEnv(Cel cel) { - this.cel = cel; - } + private static Cel setupCelEnv(CelBuilder celBuilder) { + return celBuilder + .addMessageTypes(TestAllTypes.getDescriptor()) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions( + CelOptions.current() + .populateMacroCalls(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .addCompilerLibraries( + CelExtensions.optional(), CelExtensions.bindings(), CelExtensions.comprehensions()) + .addRuntimeLibraries(CelExtensions.optional(), CelExtensions.comprehensions()) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "pure_custom_func", + newGlobalOverload("pure_custom_func_overload", SimpleType.INT, SimpleType.INT)), + CelFunctionDecl.newFunctionDeclaration( + "non_pure_custom_func", + newGlobalOverload("non_pure_custom_func_overload", SimpleType.INT, SimpleType.INT))) + .addFunctionBindings( + // This is pure, but for the purposes of excluding it as a CSE candidate, pretend that + // it isn't. + CelFunctionBinding.fromOverloads( + "non_pure_custom_func", + CelFunctionBinding.from("non_pure_custom_func_overload", Long.class, val -> val))) + .addFunctionBindings( + CelFunctionBinding.fromOverloads( + "pure_custom_func", + CelFunctionBinding.from("pure_custom_func_overload", Long.class, val -> val))) + .addVar("x", SimpleType.DYN) + .addVar("y", SimpleType.DYN) + .addVar("opt_x", OptionalType.create(SimpleType.DYN)) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .build(); } private static final CelUnparser CEL_UNPARSER = CelUnparserFactory.newUnparser(); @@ -129,6 +116,7 @@ private static Cel setupCelEnv(CelBuilder celBuilder) { @Before public void setUp() { + this.cel = setupCelEnv(runtimeFlavor.builder()); overriddenBaseFilePath = ""; } @@ -140,21 +128,23 @@ protected String baselineFileName() { return overriddenBaseFilePath; } - @TestParameter RuntimeEnv runtimeEnv; + @TestParameter CelRuntimeFlavor runtimeFlavor; + + private Cel cel; @Test public void allOptimizers_producesSameEvaluationResult( @TestParameter CseTestOptimizer cseTestOptimizer, @TestParameter CseTestCase cseTestCase) throws Exception { skipBaselineVerification(); - CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(cseTestCase.source).getAst(); + CelAbstractSyntaxTree ast = cel.compile(cseTestCase.source).getAst(); ImmutableMap inputMap = ImmutableMap.of("msg", TEST_ALL_TYPES_INPUT, "x", 5L, "y", 6L, "opt_x", Optional.of(5L)); - Object expectedEvalResult = runtimeEnv.cel.createProgram(ast).eval(inputMap); + Object expectedEvalResult = cel.createProgram(ast).eval(inputMap); - CelAbstractSyntaxTree optimizedAst = cseTestOptimizer.newCseOptimizer(runtimeEnv).optimize(ast); + CelAbstractSyntaxTree optimizedAst = cseTestOptimizer.newCseOptimizer(cel).optimize(ast); - Object optimizedEvalResult = runtimeEnv.cel.createProgram(optimizedAst).eval(inputMap); + Object optimizedEvalResult = cel.createProgram(optimizedAst).eval(inputMap); assertThat(optimizedEvalResult).isEqualTo(expectedEvalResult); } @@ -163,20 +153,19 @@ public void allOptimizers_producesSameEvaluationResult_parsedOnly( @TestParameter CseTestCase cseTestCase, @TestParameter CseTestOptimizer cseTestOptimizer) throws Exception { skipBaselineVerification(); - if (runtimeEnv == RuntimeEnv.LEGACY) { + if (runtimeFlavor.equals(CelRuntimeFlavor.LEGACY)) { return; } - CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(cseTestCase.source).getAst(); + CelAbstractSyntaxTree ast = cel.compile(cseTestCase.source).getAst(); ImmutableMap inputMap = ImmutableMap.of("msg", TEST_ALL_TYPES_INPUT, "x", 5L, "y", 6L, "opt_x", Optional.of(5L)); - Object expectedEvalResult = runtimeEnv.cel.createProgram(ast).eval(inputMap); + Object expectedEvalResult = cel.createProgram(ast).eval(inputMap); - CelAbstractSyntaxTree optimizedAst = cseTestOptimizer.newCseOptimizer(runtimeEnv).optimize(ast); + CelAbstractSyntaxTree optimizedAst = cseTestOptimizer.newCseOptimizer(cel).optimize(ast); CelAbstractSyntaxTree parsedOnlyOptimizedAst = CelAbstractSyntaxTree.newParsedAst(optimizedAst.getExpr(), optimizedAst.getSource()); - Object optimizedEvalResult = - runtimeEnv.cel.createProgram(parsedOnlyOptimizedAst).eval(inputMap); + Object optimizedEvalResult = cel.createProgram(parsedOnlyOptimizedAst).eval(inputMap); assertThat(optimizedEvalResult).isEqualTo(expectedEvalResult); } @@ -186,22 +175,20 @@ public void subexpression_unparsed() throws Exception { testOutput().println("Test case: " + cseTestCase.name()); testOutput().println("Source: " + cseTestCase.source); testOutput().println("=====>"); - CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(cseTestCase.source).getAst(); + CelAbstractSyntaxTree ast = cel.compile(cseTestCase.source).getAst(); boolean resultPrinted = false; for (CseTestOptimizer cseTestOptimizer : CseTestOptimizer.values()) { String optimizerName = cseTestOptimizer.name(); CelAbstractSyntaxTree optimizedAst; try { - optimizedAst = cseTestOptimizer.newCseOptimizer(runtimeEnv).optimize(ast); + optimizedAst = cseTestOptimizer.newCseOptimizer(cel).optimize(ast); } catch (Exception e) { testOutput().printf("[%s]: Optimization Error: %s", optimizerName, e); continue; } if (!resultPrinted) { Object optimizedEvalResult = - runtimeEnv - .cel - .createProgram(optimizedAst) + cel.createProgram(optimizedAst) .eval( ImmutableMap.of( "msg", TEST_ALL_TYPES_INPUT, "x", 5L, "y", 6L, "opt_x", Optional.of(5L))); @@ -225,17 +212,15 @@ public void constfold_before_subexpression_unparsed() throws Exception { testOutput().println("Test case: " + cseTestCase.name()); testOutput().println("Source: " + cseTestCase.source); testOutput().println("=====>"); - CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(cseTestCase.source).getAst(); + CelAbstractSyntaxTree ast = cel.compile(cseTestCase.source).getAst(); boolean resultPrinted = false; for (CseTestOptimizer cseTestOptimizer : EnumSet.allOf(CseTestOptimizer.class)) { String optimizerName = cseTestOptimizer.name(); CelAbstractSyntaxTree optimizedAst = - cseTestOptimizer.newCseWithConstFoldingOptimizer(runtimeEnv).optimize(ast); + cseTestOptimizer.newCseWithConstFoldingOptimizer(cel).optimize(ast); if (!resultPrinted) { Object optimizedEvalResult = - runtimeEnv - .cel - .createProgram(optimizedAst) + cel.createProgram(optimizedAst) .eval( ImmutableMap.of( "msg", TEST_ALL_TYPES_INPUT, "x", 5L, "y", 6L, "opt_x", Optional.of(5L))); @@ -261,9 +246,9 @@ public void subexpression_ast(@TestParameter CseTestOptimizer cseTestOptimizer) testOutput().println("Test case: " + cseTestCase.name()); testOutput().println("Source: " + cseTestCase.source); testOutput().println("=====>"); - CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(cseTestCase.source).getAst(); + CelAbstractSyntaxTree ast = cel.compile(cseTestCase.source).getAst(); CelAbstractSyntaxTree optimizedAst = - newCseOptimizer(runtimeEnv.cel, cseTestOptimizer.option).optimize(ast); + newCseOptimizer(cel, cseTestOptimizer.option).optimize(ast); testOutput().println(optimizedAst.getExpr()); } } @@ -272,8 +257,7 @@ public void subexpression_ast(@TestParameter CseTestOptimizer cseTestOptimizer) public void large_expressions_block_common_subexpr() throws Exception { CelOptimizer celOptimizer = newCseOptimizer( - runtimeEnv.cel, - SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()); + cel, SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()); runLargeTestCases(celOptimizer); } @@ -282,7 +266,7 @@ public void large_expressions_block_common_subexpr() throws Exception { public void large_expressions_block_recursion_depth_1() throws Exception { CelOptimizer celOptimizer = newCseOptimizer( - runtimeEnv.cel, + cel, SubexpressionOptimizerOptions.newBuilder() .populateMacroCalls(true) .subexpressionMaxRecursionDepth(1) @@ -295,7 +279,7 @@ public void large_expressions_block_recursion_depth_1() throws Exception { public void large_expressions_block_recursion_depth_2() throws Exception { CelOptimizer celOptimizer = newCseOptimizer( - runtimeEnv.cel, + cel, SubexpressionOptimizerOptions.newBuilder() .populateMacroCalls(true) .subexpressionMaxRecursionDepth(2) @@ -308,7 +292,7 @@ public void large_expressions_block_recursion_depth_2() throws Exception { public void large_expressions_block_recursion_depth_3() throws Exception { CelOptimizer celOptimizer = newCseOptimizer( - runtimeEnv.cel, + cel, SubexpressionOptimizerOptions.newBuilder() .populateMacroCalls(true) .subexpressionMaxRecursionDepth(3) @@ -322,12 +306,10 @@ private void runLargeTestCases(CelOptimizer celOptimizer) throws Exception { testOutput().println("Test case: " + cseTestCase.name()); testOutput().println("Source: " + cseTestCase.source); testOutput().println("=====>"); - CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(cseTestCase.source).getAst(); + CelAbstractSyntaxTree ast = cel.compile(cseTestCase.source).getAst(); CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); Object optimizedEvalResult = - runtimeEnv - .cel - .createProgram(optimizedAst) + cel.createProgram(optimizedAst) .eval( ImmutableMap.of("msg", TEST_ALL_TYPES_INPUT, "x", 5L, "opt_x", Optional.of(5L))); testOutput().println("Result: " + optimizedEvalResult); @@ -376,13 +358,13 @@ private enum CseTestOptimizer { } // Defers building the optimizer until the test runs - private CelOptimizer newCseOptimizer(RuntimeEnv env) { - return SubexpressionOptimizerBaselineTest.newCseOptimizer(env.cel, option); + private CelOptimizer newCseOptimizer(Cel cel) { + return SubexpressionOptimizerBaselineTest.newCseOptimizer(cel, option); } // Defers building the optimizer until the test runs - private CelOptimizer newCseWithConstFoldingOptimizer(RuntimeEnv env) { - return CelOptimizerFactory.standardCelOptimizerBuilder(env.cel) + private CelOptimizer newCseWithConstFoldingOptimizer(Cel cel) { + return CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers( ConstantFoldingOptimizer.getInstance(), SubexpressionOptimizer.newInstance(option)) .build(); diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java index 23459e5d8..e7387d7d8 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java @@ -26,7 +26,6 @@ import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.bundle.Cel; import dev.cel.bundle.CelBuilder; -import dev.cel.bundle.CelExperimentalFactory; import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelFunctionDecl; @@ -61,87 +60,80 @@ import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.PartialVars; import dev.cel.runtime.Program; +import dev.cel.testing.CelRuntimeFlavor; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) public class SubexpressionOptimizerTest { - private enum RuntimeEnv { - LEGACY( - setupCelEnv(CelFactory.standardCelBuilder()), - setupCelForEvaluatingBlock(CelFactory.standardCelBuilder())), - PLANNER( - setupCelEnv(CelExperimentalFactory.plannerCelBuilder()), - setupCelForEvaluatingBlock(CelExperimentalFactory.plannerCelBuilder())); - - private final Cel cel; - private final Cel celForEvaluatingBlock; - - private static Cel setupCelEnv(CelBuilder celBuilder) { - return celBuilder - .addMessageTypes(TestAllTypes.getDescriptor()) - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .setOptions( - CelOptions.current() - .populateMacroCalls(true) - .enableHeterogeneousNumericComparisons(true) - .build()) - .addCompilerLibraries(CelExtensions.bindings(), CelExtensions.strings()) - .addRuntimeLibraries(CelExtensions.strings()) - .addFunctionDeclarations( - CelFunctionDecl.newFunctionDeclaration( - "non_pure_custom_func", - newGlobalOverload( - "non_pure_custom_func_overload", SimpleType.INT, SimpleType.INT))) - .addVar("x", SimpleType.DYN) - .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) - .build(); - } - - private static Cel setupCelForEvaluatingBlock(CelBuilder celBuilder) { - return celBuilder - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .addFunctionDeclarations( - // These are test only declarations, as the actual function is made internal using @ - // symbol. - // If the main function declaration needs updating, be sure to update the test - // declaration as well. - CelFunctionDecl.newFunctionDeclaration( - "cel.block", - CelOverloadDecl.newGlobalOverload( - "block_test_only_overload", - SimpleType.DYN, - ListType.create(SimpleType.DYN), - SimpleType.DYN)), - SubexpressionOptimizer.newCelBlockFunctionDecl(SimpleType.DYN), - CelFunctionDecl.newFunctionDeclaration( - "get_true", - CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL))) - // Similarly, this is a test only decl (index0 -> @index0) - .addVarDeclarations( - CelVarDecl.newVarDeclaration("c0", SimpleType.DYN), - CelVarDecl.newVarDeclaration("c1", SimpleType.DYN), - CelVarDecl.newVarDeclaration("index0", SimpleType.DYN), - CelVarDecl.newVarDeclaration("index1", SimpleType.DYN), - CelVarDecl.newVarDeclaration("index2", SimpleType.DYN), - CelVarDecl.newVarDeclaration("@index0", SimpleType.DYN), - CelVarDecl.newVarDeclaration("@index1", SimpleType.DYN), - CelVarDecl.newVarDeclaration("@index2", SimpleType.DYN)) - .addMessageTypes(TestAllTypes.getDescriptor()) - .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) - .build(); - } + private static Cel setupCelEnv(CelBuilder celBuilder) { + return celBuilder + .addMessageTypes(TestAllTypes.getDescriptor()) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions( + CelOptions.current() + .populateMacroCalls(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .addCompilerLibraries(CelExtensions.bindings(), CelExtensions.strings()) + .addRuntimeLibraries(CelExtensions.strings()) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "non_pure_custom_func", + newGlobalOverload("non_pure_custom_func_overload", SimpleType.INT, SimpleType.INT))) + .addVar("x", SimpleType.DYN) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .build(); + } - RuntimeEnv(Cel cel, Cel celForEvaluatingBlock) { - this.cel = cel; - this.celForEvaluatingBlock = celForEvaluatingBlock; - } + private static Cel setupCelForEvaluatingBlock(CelBuilder celBuilder) { + return celBuilder + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addFunctionDeclarations( + // These are test only declarations, as the actual function is made internal using @ + // symbol. + // If the main function declaration needs updating, be sure to update the test + // declaration as well. + CelFunctionDecl.newFunctionDeclaration( + "cel.block", + CelOverloadDecl.newGlobalOverload( + "block_test_only_overload", + SimpleType.DYN, + ListType.create(SimpleType.DYN), + SimpleType.DYN)), + SubexpressionOptimizer.newCelBlockFunctionDecl(SimpleType.DYN), + CelFunctionDecl.newFunctionDeclaration( + "get_true", + CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL))) + // Similarly, this is a test only decl (index0 -> @index0) + .addVarDeclarations( + CelVarDecl.newVarDeclaration("c0", SimpleType.DYN), + CelVarDecl.newVarDeclaration("c1", SimpleType.DYN), + CelVarDecl.newVarDeclaration("index0", SimpleType.DYN), + CelVarDecl.newVarDeclaration("index1", SimpleType.DYN), + CelVarDecl.newVarDeclaration("index2", SimpleType.DYN), + CelVarDecl.newVarDeclaration("@index0", SimpleType.DYN), + CelVarDecl.newVarDeclaration("@index1", SimpleType.DYN), + CelVarDecl.newVarDeclaration("@index2", SimpleType.DYN)) + .addMessageTypes(TestAllTypes.getDescriptor()) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .build(); } - @TestParameter RuntimeEnv runtimeEnv; + @TestParameter CelRuntimeFlavor runtimeFlavor; + + private Cel cel; + private Cel celForEvaluatingBlock; + + @Before + public void setUp() { + this.cel = setupCelEnv(runtimeFlavor.builder()); + this.celForEvaluatingBlock = setupCelForEvaluatingBlock(runtimeFlavor.builder()); + } private static final CelUnparser CEL_UNPARSER = CelUnparserFactory.newUnparser(); @@ -160,7 +152,7 @@ private static CelBuilder newCelBuilder() { } private CelOptimizer newCseOptimizer(SubexpressionOptimizerOptions options) { - return CelOptimizerFactory.standardCelOptimizerBuilder(runtimeEnv.cel) + return CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers(SubexpressionOptimizer.newInstance(options)) .build(); } @@ -174,21 +166,20 @@ public void cse_resultTypeSet_celBlockOptimizationSuccess() throws Exception { SubexpressionOptimizer.newInstance( SubexpressionOptimizerOptions.newBuilder().build())) .build(); - CelAbstractSyntaxTree ast = runtimeEnv.cel.compile("size('a') + size('a') == 2").getAst(); + CelAbstractSyntaxTree ast = cel.compile("size('a') + size('a') == 2").getAst(); CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); - assertThat(runtimeEnv.cel.createProgram(optimizedAst).eval()).isEqualTo(true); + assertThat(cel.createProgram(optimizedAst).eval()).isEqualTo(true); assertThat(CEL_UNPARSER.unparse(optimizedAst)) .isEqualTo("cel.@block([size(\"a\")], @index0 + @index0 == 2)"); } @Test public void cse_indexEvaluationErrors_throws() throws Exception { - CelAbstractSyntaxTree ast = - runtimeEnv.cel.compile("\"abc\".charAt(10) + \"abc\".charAt(10)").getAst(); + CelAbstractSyntaxTree ast = cel.compile("\"abc\".charAt(10) + \"abc\".charAt(10)").getAst(); CelOptimizer optimizedOptimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(runtimeEnv.cel) + CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers(SubexpressionOptimizer.getInstance()) .build(); @@ -197,7 +188,7 @@ public void cse_indexEvaluationErrors_throws() throws Exception { String unparsed = CEL_UNPARSER.unparse(optimizedAst); assertThat(unparsed).isEqualTo("cel.@block([\"abc\".charAt(10)], @index0 + @index0)"); - Program program = runtimeEnv.cel.createProgram(optimizedAst); + Program program = cel.createProgram(optimizedAst); CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> program.eval(ImmutableMap.of())); assertThat(e).hasMessageThat().contains("charAt failure: Index out of range: 10"); @@ -205,9 +196,9 @@ public void cse_indexEvaluationErrors_throws() throws Exception { @Test public void cse_withUnknownAttributes() throws Exception { - CelAbstractSyntaxTree ast = runtimeEnv.cel.compile("size(\"a\") == 1 ? x.y : x.y").getAst(); + CelAbstractSyntaxTree ast = cel.compile("size(\"a\") == 1 ? x.y : x.y").getAst(); CelOptimizer optimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(runtimeEnv.cel) + CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers(SubexpressionOptimizer.getInstance()) .build(); @@ -217,9 +208,7 @@ public void cse_withUnknownAttributes() throws Exception { .isEqualTo("cel.@block([x.y], (size(\"a\") == 1) ? @index0 : @index0)"); Object result = - runtimeEnv - .cel - .createProgram(optimizedAst) + cel.createProgram(optimizedAst) .eval(PartialVars.of(CelAttributePattern.fromQualifiedIdentifier("x"))); assertThat(result).isInstanceOf(CelUnknownSet.class); } @@ -254,7 +243,7 @@ private enum CseNoOpTestCase { @Test public void cse_withCelBind_noop(@TestParameter CseNoOpTestCase testCase) throws Exception { - CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(testCase.source).getAst(); + CelAbstractSyntaxTree ast = cel.compile(testCase.source).getAst(); CelAbstractSyntaxTree optimizedAst = newCseOptimizer(SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()) @@ -266,7 +255,7 @@ public void cse_withCelBind_noop(@TestParameter CseNoOpTestCase testCase) throws @Test public void cse_withCelBlock_noop(@TestParameter CseNoOpTestCase testCase) throws Exception { - CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(testCase.source).getAst(); + CelAbstractSyntaxTree ast = cel.compile(testCase.source).getAst(); CelAbstractSyntaxTree optimizedAst = newCseOptimizer(SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()) @@ -279,7 +268,7 @@ public void cse_withCelBlock_noop(@TestParameter CseNoOpTestCase testCase) throw @Test public void cse_withComprehensionStructureRetained() throws Exception { CelAbstractSyntaxTree ast = - runtimeEnv.cel.compile("['foo'].map(x, [x+x]) + ['foo'].map(x, [x+x, x+x])").getAst(); + cel.compile("['foo'].map(x, [x+x]) + ['foo'].map(x, [x+x, x+x])").getAst(); CelOptimizer celOptimizer = newCseOptimizer( SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()); @@ -295,12 +284,10 @@ public void cse_withComprehensionStructureRetained() throws Exception { @Test public void cse_applyConstFoldingBefore() throws Exception { CelAbstractSyntaxTree ast = - runtimeEnv - .cel - .compile("size([1+1+1]) + size([1+1+1]) + size([1,1+1+1]) + size([1,1+1+1]) + x") + cel.compile("size([1+1+1]) + size([1+1+1]) + size([1,1+1+1]) + size([1,1+1+1]) + x") .getAst(); CelOptimizer optimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(runtimeEnv.cel) + CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers( ConstantFoldingOptimizer.getInstance(), SubexpressionOptimizer.newInstance( @@ -315,12 +302,10 @@ public void cse_applyConstFoldingBefore() throws Exception { @Test public void cse_applyConstFoldingAfter() throws Exception { CelAbstractSyntaxTree ast = - runtimeEnv - .cel - .compile("size([1+1+1]) + size([1+1+1]) + size([1,1+1+1]) + size([1,1+1+1]) + x") + cel.compile("size([1+1+1]) + size([1+1+1]) + size([1,1+1+1]) + size([1,1+1+1]) + x") .getAst(); CelOptimizer optimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(runtimeEnv.cel) + CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers( SubexpressionOptimizer.newInstance( SubexpressionOptimizerOptions.newBuilder().build()), @@ -335,9 +320,9 @@ public void cse_applyConstFoldingAfter() throws Exception { @Test public void cse_applyConstFoldingAfter_nothingToFold() throws Exception { - CelAbstractSyntaxTree ast = runtimeEnv.cel.compile("size(x) + size(x)").getAst(); + CelAbstractSyntaxTree ast = cel.compile("size(x) + size(x)").getAst(); CelOptimizer optimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(runtimeEnv.cel) + CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers( SubexpressionOptimizer.newInstance( SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()), @@ -360,7 +345,7 @@ public void iterationLimitReached_throws() throws Exception { largeExprBuilder.append("+"); } } - CelAbstractSyntaxTree ast = runtimeEnv.cel.compile(largeExprBuilder.toString()).getAst(); + CelAbstractSyntaxTree ast = cel.compile(largeExprBuilder.toString()).getAst(); CelOptimizationException e = assertThrows( @@ -376,9 +361,9 @@ public void iterationLimitReached_throws() throws Exception { @Test public void celBlock_astExtensionTagged() throws Exception { - CelAbstractSyntaxTree ast = runtimeEnv.cel.compile("size(x) + size(x)").getAst(); + CelAbstractSyntaxTree ast = cel.compile("size(x) + size(x)").getAst(); CelOptimizer optimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(runtimeEnv.cel) + CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers( SubexpressionOptimizer.newInstance( SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()), @@ -411,20 +396,20 @@ private enum BlockTestCase { public void block_success(@TestParameter BlockTestCase testCase) throws Exception { CelAbstractSyntaxTree ast = compileUsingInternalFunctions(testCase.source); - Object evaluatedResult = runtimeEnv.celForEvaluatingBlock.createProgram(ast).eval(); + Object evaluatedResult = celForEvaluatingBlock.createProgram(ast).eval(); assertThat(evaluatedResult).isNotNull(); } @Test public void block_success_parsedOnly(@TestParameter BlockTestCase testCase) throws Exception { - if (runtimeEnv == RuntimeEnv.LEGACY) { + if (runtimeFlavor.equals(CelRuntimeFlavor.LEGACY)) { return; } CelAbstractSyntaxTree ast = compileUsingInternalFunctions(testCase.source, /* parsedOnly= */ true); - Object evaluatedResult = runtimeEnv.celForEvaluatingBlock.createProgram(ast).eval(); + Object evaluatedResult = celForEvaluatingBlock.createProgram(ast).eval(); assertThat(evaluatedResult).isNotNull(); } @@ -686,7 +671,7 @@ public void block_containsCycle_throws() throws Exception { CelAbstractSyntaxTree ast = compileUsingInternalFunctions("cel.block([index1,index0],index0)"); CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> runtimeEnv.cel.createProgram(ast).eval()); + assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); assertThat(e).hasMessageThat().contains("Cycle detected: @index0"); } @@ -697,7 +682,7 @@ public void block_lazyEvaluationContainsError_cleansUpCycleState() throws Except "cel.block([1/0 > 0], (index0 && false) || (index0 && true))"); CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> runtimeEnv.cel.createProgram(ast).eval()); + assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); assertThat(e).hasMessageThat().contains("/ by zero"); assertThat(e).hasMessageThat().doesNotContain("Cycle detected"); @@ -709,8 +694,7 @@ public void block_lazyEvaluationContainsError_cleansUpCycleState() throws Except */ private CelAbstractSyntaxTree compileUsingInternalFunctions(String expression, boolean parsedOnly) throws CelValidationException { - CelAbstractSyntaxTree astToModify = - runtimeEnv.celForEvaluatingBlock.compile(expression).getAst(); + CelAbstractSyntaxTree astToModify = celForEvaluatingBlock.compile(expression).getAst(); CelMutableAst mutableAst = CelMutableAst.fromCelAst(astToModify); CelNavigableMutableAst.fromAst(mutableAst) .getRoot() @@ -735,7 +719,7 @@ private CelAbstractSyntaxTree compileUsingInternalFunctions(String expression, b if (parsedOnly) { return mutableAst.toParsedAst(); } - return runtimeEnv.celForEvaluatingBlock.check(mutableAst.toParsedAst()).getAst(); + return celForEvaluatingBlock.check(mutableAst.toParsedAst()).getAst(); } private CelAbstractSyntaxTree compileUsingInternalFunctions(String expression) diff --git a/testing/BUILD.bazel b/testing/BUILD.bazel index c1b2a92b4..b9e68f003 100644 --- a/testing/BUILD.bazel +++ b/testing/BUILD.bazel @@ -11,6 +11,11 @@ java_library( exports = ["//testing/src/main/java/dev/cel/testing:adorner"], ) +java_library( + name = "cel_runtime_flavor", + exports = ["//testing/src/main/java/dev/cel/testing:cel_runtime_flavor"], +) + java_library( name = "line_differ", exports = ["//testing/src/main/java/dev/cel/testing:line_differ"], diff --git a/testing/src/main/java/dev/cel/testing/BUILD.bazel b/testing/src/main/java/dev/cel/testing/BUILD.bazel index 5ee142200..0d94bc8fc 100644 --- a/testing/src/main/java/dev/cel/testing/BUILD.bazel +++ b/testing/src/main/java/dev/cel/testing/BUILD.bazel @@ -105,3 +105,12 @@ java_library( "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) + +java_library( + name = "cel_runtime_flavor", + srcs = ["CelRuntimeFlavor.java"], + deps = [ + "//bundle:cel", + "//bundle:cel_experimental_factory", + ], +) diff --git a/testing/src/main/java/dev/cel/testing/CelRuntimeFlavor.java b/testing/src/main/java/dev/cel/testing/CelRuntimeFlavor.java new file mode 100644 index 000000000..576e0c1d3 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/CelRuntimeFlavor.java @@ -0,0 +1,38 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.testing; + +import dev.cel.bundle.CelBuilder; +import dev.cel.bundle.CelExperimentalFactory; +import dev.cel.bundle.CelFactory; + +/** Enumeration of supported CEL runtime environments for testing. */ +public enum CelRuntimeFlavor { + LEGACY { + @Override + public CelBuilder builder() { + return CelFactory.standardCelBuilder(); + } + }, + PLANNER { + @Override + public CelBuilder builder() { + return CelExperimentalFactory.plannerCelBuilder(); + } + }; + + /** Returns a new {@link CelBuilder} instance for this runtime flavor. */ + public abstract CelBuilder builder(); +} From 8342a01ef2d64eb5790ea7b0c5a239042b53bc5c Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 14 Apr 2026 11:14:04 -0700 Subject: [PATCH 045/204] Add parsed-only evaluation test coverage to Regex Extensions PiperOrigin-RevId: 899682453 --- .../test/java/dev/cel/extensions/BUILD.bazel | 1 + .../extensions/CelRegexExtensionsTest.java | 89 ++++++++----------- .../src/main/java/dev/cel/testing/BUILD.bazel | 2 + 3 files changed, 42 insertions(+), 50 deletions(-) diff --git a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel index a9dbfaca2..0b6502410 100644 --- a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel @@ -40,6 +40,7 @@ java_library( "//runtime:lite_runtime_factory", "//runtime:partial_vars", "//runtime:unknown_attributes", + "//testing:cel_runtime_flavor", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/test:simple_java_proto", diff --git a/extensions/src/test/java/dev/cel/extensions/CelRegexExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelRegexExtensionsTest.java index 8a1bef014..924344b25 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelRegexExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelRegexExtensionsTest.java @@ -20,25 +20,38 @@ import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; +import dev.cel.bundle.Cel; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; -import dev.cel.compiler.CelCompiler; -import dev.cel.compiler.CelCompilerFactory; import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelRuntime; -import dev.cel.runtime.CelRuntimeFactory; +import dev.cel.testing.CelRuntimeFlavor; import java.util.Optional; +import org.junit.Assume; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) public final class CelRegexExtensionsTest { - private static final CelCompiler COMPILER = - CelCompilerFactory.standardCelCompilerBuilder().addLibraries(CelExtensions.regex()).build(); - private static final CelRuntime RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(CelExtensions.regex()).build(); + @TestParameter public CelRuntimeFlavor runtimeFlavor; + @TestParameter public boolean isParseOnly; + + private Cel cel; + + @Before + public void setUp() { + // Legacy runtime does not support parsed-only evaluation mode. + Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.LEGACY) && isParseOnly); + this.cel = + runtimeFlavor + .builder() + .addCompilerLibraries(CelExtensions.regex()) + .addRuntimeLibraries(CelExtensions.regex()) + .build(); + } + @Test public void library() { @@ -80,11 +93,7 @@ public void library() { public void replaceAll_success(String target, String regex, String replaceStr, String res) throws Exception { String expr = String.format("regex.replace('%s', '%s', '%s')", target, regex, replaceStr); - CelRuntime.Program program = RUNTIME.createProgram(COMPILER.compile(expr).getAst()); - - Object result = program.eval(); - - assertThat(result).isEqualTo(res); + assertThat(eval(expr)).isEqualTo(res); } @Test @@ -93,11 +102,7 @@ public void replace_nested_success() throws Exception { "regex.replace(" + " regex.replace('%(foo) %(bar) %2','%\\\\((\\\\w+)\\\\)','${\\\\1}')," + " '%(\\\\d+)', '$\\\\1')"; - CelRuntime.Program program = RUNTIME.createProgram(COMPILER.compile(expr).getAst()); - - Object result = program.eval(); - - assertThat(result).isEqualTo("${foo} ${bar} $2"); + assertThat(eval(expr)).isEqualTo("${foo} ${bar} $2"); } @Test @@ -118,11 +123,7 @@ public void replace_nested_success() throws Exception { public void replaceCount_success(String t, String re, String rep, long i, String res) throws Exception { String expr = String.format("regex.replace('%s', '%s', '%s', %d)", t, re, rep, i); - CelRuntime.Program program = RUNTIME.createProgram(COMPILER.compile(expr).getAst()); - - Object result = program.eval(); - - assertThat(result).isEqualTo(res); + assertThat(eval(expr)).isEqualTo(res); } @Test @@ -131,10 +132,8 @@ public void replaceCount_success(String t, String re, String rep, long i, String public void replace_invalidRegex_throwsException(String target, String regex, String replaceStr) throws Exception { String expr = String.format("regex.replace('%s', '%s', '%s')", target, regex, replaceStr); - CelAbstractSyntaxTree ast = COMPILER.compile(expr).getAst(); - CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> RUNTIME.createProgram(ast).eval()); + assertThrows(CelEvaluationException.class, () -> eval(expr)); assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(e).hasCauseThat().hasMessageThat().contains("Failed to compile regex: "); @@ -143,10 +142,8 @@ public void replace_invalidRegex_throwsException(String target, String regex, St @Test public void replace_invalidCaptureGroupReplaceStr_throwsException() throws Exception { String expr = "regex.replace('test', '(.)', '\\\\2')"; - CelAbstractSyntaxTree ast = COMPILER.compile(expr).getAst(); - CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> RUNTIME.createProgram(ast).eval()); + assertThrows(CelEvaluationException.class, () -> eval(expr)); assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(e) @@ -158,10 +155,8 @@ public void replace_invalidCaptureGroupReplaceStr_throwsException() throws Excep @Test public void replace_trailingBackslashReplaceStr_throwsException() throws Exception { String expr = "regex.replace('id=123', 'id=(?P\\\\d+)', '\\\\')"; - CelAbstractSyntaxTree ast = COMPILER.compile(expr).getAst(); - CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> RUNTIME.createProgram(ast).eval()); + assertThrows(CelEvaluationException.class, () -> eval(expr)); assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(e) @@ -173,10 +168,8 @@ public void replace_trailingBackslashReplaceStr_throwsException() throws Excepti @Test public void replace_invalidGroupReferenceReplaceStr_throwsException() throws Exception { String expr = "regex.replace('id=123', 'id=(?P\\\\d+)', '\\\\a')"; - CelAbstractSyntaxTree ast = COMPILER.compile(expr).getAst(); - CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> RUNTIME.createProgram(ast).eval()); + assertThrows(CelEvaluationException.class, () -> eval(expr)); assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(e) @@ -199,9 +192,7 @@ public void replace_invalidGroupReferenceReplaceStr_throwsException() throws Exc @TestParameters("{target: 'brand', regex: 'brand', expectedResult: 'brand'}") public void extract_success(String target, String regex, String expectedResult) throws Exception { String expr = String.format("regex.extract('%s', '%s')", target, regex); - CelRuntime.Program program = RUNTIME.createProgram(COMPILER.compile(expr).getAst()); - - Object result = program.eval(); + Object result = eval(expr); assertThat(result).isInstanceOf(Optional.class); assertThat((Optional) result).hasValue(expectedResult); @@ -213,9 +204,7 @@ public void extract_success(String target, String regex, String expectedResult) @TestParameters("{target: '', regex: '\\\\w+'}") public void extract_no_match(String target, String regex) throws Exception { String expr = String.format("regex.extract('%s', '%s')", target, regex); - CelRuntime.Program program = RUNTIME.createProgram(COMPILER.compile(expr).getAst()); - - Object result = program.eval(); + Object result = eval(expr); assertThat(result).isInstanceOf(Optional.class); assertThat((Optional) result).isEmpty(); @@ -227,10 +216,8 @@ public void extract_no_match(String target, String regex) throws Exception { public void extract_multipleCaptureGroups_throwsException(String target, String regex) throws Exception { String expr = String.format("regex.extract('%s', '%s')", target, regex); - CelAbstractSyntaxTree ast = COMPILER.compile(expr).getAst(); - CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> RUNTIME.createProgram(ast).eval()); + assertThrows(CelEvaluationException.class, () -> eval(expr)); assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(e) @@ -263,9 +250,7 @@ private enum ExtractAllTestCase { @Test public void extractAll_success(@TestParameter ExtractAllTestCase testCase) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(testCase.expr).getAst(); - - Object result = RUNTIME.createProgram(ast).eval(); + Object result = eval(testCase.expr); assertThat(result).isEqualTo(testCase.expectedResult); } @@ -281,10 +266,8 @@ public void extractAll_success(@TestParameter ExtractAllTestCase testCase) throw public void extractAll_multipleCaptureGroups_throwsException(String target, String regex) throws Exception { String expr = String.format("regex.extractAll('%s', '%s')", target, regex); - CelAbstractSyntaxTree ast = COMPILER.compile(expr).getAst(); - CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> RUNTIME.createProgram(ast).eval()); + assertThrows(CelEvaluationException.class, () -> eval(expr)); assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(e) @@ -292,4 +275,10 @@ public void extractAll_multipleCaptureGroups_throwsException(String target, Stri .hasMessageThat() .contains("Regular expression has more than one capturing group:"); } + + private Object eval(String expr) throws Exception { + CelAbstractSyntaxTree ast = + isParseOnly ? cel.parse(expr).getAst() : cel.compile(expr).getAst(); + return cel.createProgram(ast).eval(); + } } diff --git a/testing/src/main/java/dev/cel/testing/BUILD.bazel b/testing/src/main/java/dev/cel/testing/BUILD.bazel index 0d94bc8fc..b52026ec4 100644 --- a/testing/src/main/java/dev/cel/testing/BUILD.bazel +++ b/testing/src/main/java/dev/cel/testing/BUILD.bazel @@ -109,6 +109,8 @@ java_library( java_library( name = "cel_runtime_flavor", srcs = ["CelRuntimeFlavor.java"], + tags = [ + ], deps = [ "//bundle:cel", "//bundle:cel_experimental_factory", From 74ffbb3a269964c96006fab24e6cdc687adc7170 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 14 Apr 2026 15:08:15 -0700 Subject: [PATCH 046/204] Fix accu_init to be lazily initialized in folder. Add parsed-only evaluation test coverage to Bindings Extensions PiperOrigin-RevId: 899792272 --- .../extensions/CelBindingsExtensionsTest.java | 287 ++++++++++-------- .../java/dev/cel/runtime/planner/BUILD.bazel | 1 + .../dev/cel/runtime/planner/EvalFold.java | 52 +++- 3 files changed, 203 insertions(+), 137 deletions(-) diff --git a/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java index ff9e31432..b87967d0e 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java @@ -22,40 +22,51 @@ import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; +import dev.cel.bundle.Cel; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; import dev.cel.common.CelOverloadDecl; import dev.cel.common.CelValidationException; +import dev.cel.common.exceptions.CelDivideByZeroException; import dev.cel.common.types.SimpleType; import dev.cel.common.types.StructTypeReference; -import dev.cel.compiler.CelCompiler; -import dev.cel.compiler.CelCompilerFactory; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.parser.CelMacro; import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; -import dev.cel.runtime.CelRuntime; -import dev.cel.runtime.CelRuntimeFactory; +import dev.cel.testing.CelRuntimeFlavor; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Assume; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) public final class CelBindingsExtensionsTest { - private static final CelCompiler COMPILER = - CelCompilerFactory.standardCelCompilerBuilder() - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .addLibraries(CelOptionalLibrary.INSTANCE, CelExtensions.bindings()) - .build(); - - private static final CelRuntime RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder() - .addLibraries(CelOptionalLibrary.INSTANCE) - .build(); + @TestParameter public CelRuntimeFlavor runtimeFlavor; + @TestParameter public boolean isParseOnly; + + private Cel cel; + + @Before + public void setUp() { + // Legacy runtime does not support parsed-only evaluation mode. + Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.LEGACY) && isParseOnly); + cel = + runtimeFlavor + .builder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addCompilerLibraries(CelOptionalLibrary.INSTANCE, CelExtensions.bindings()) + .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) + .build(); + } @Test public void library() { @@ -93,9 +104,7 @@ private enum BindingTestCase { @Test public void binding_success(@TestParameter BindingTestCase testCase) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(testCase.source).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - boolean evaluatedResult = (boolean) program.eval(); + boolean evaluatedResult = (boolean) eval(testCase.source); assertThat(evaluatedResult).isTrue(); } @@ -103,9 +112,11 @@ public void binding_success(@TestParameter BindingTestCase testCase) throws Exce @Test @TestParameters("{expr: 'false.bind(false, false, false)'}") public void binding_nonCelNamespace_success(String expr) throws Exception { - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder() - .addLibraries(CelExtensions.bindings()) + Cel customCel = + runtimeFlavor + .builder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) + .addCompilerLibraries(CelExtensions.bindings()) .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( "bind", @@ -116,18 +127,16 @@ public void binding_nonCelNamespace_success(String expr) throws Exception { SimpleType.BOOL, SimpleType.BOOL, SimpleType.BOOL))) - .build(); - CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() .addFunctionBindings( - CelFunctionBinding.from( - "bool_bind_bool_bool_bool", - Arrays.asList(Boolean.class, Boolean.class, Boolean.class, Boolean.class), - (args) -> true)) + CelFunctionBinding.fromOverloads( + "bind", + CelFunctionBinding.from( + "bool_bind_bool_bool_bool", + Arrays.asList(Boolean.class, Boolean.class, Boolean.class, Boolean.class), + (args) -> true))) .build(); - CelAbstractSyntaxTree ast = celCompiler.compile(expr).getAst(); - boolean result = (boolean) celRuntime.createProgram(ast).eval(); + boolean result = (boolean) eval(customCel, expr); assertThat(result).isTrue(); } @@ -135,7 +144,7 @@ public void binding_nonCelNamespace_success(String expr) throws Exception { @TestParameters("{expr: 'cel.bind(bad.name, true, bad.name)'}") public void binding_throwsCompilationException(String expr) throws Exception { CelValidationException e = - assertThrows(CelValidationException.class, () -> COMPILER.compile(expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); assertThat(e).hasMessageThat().contains("cel.bind() variable name must be a simple identifier"); } @@ -143,70 +152,76 @@ public void binding_throwsCompilationException(String expr) throws Exception { @Test @SuppressWarnings("Immutable") // Test only public void lazyBinding_bindingVarNeverReferenced() throws Exception { - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder() + + AtomicInteger invocation = new AtomicInteger(); + Cel customCel = + runtimeFlavor + .builder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) .setStandardMacros(CelStandardMacro.HAS) .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) - .addLibraries(CelExtensions.bindings()) + .addCompilerLibraries(CelExtensions.bindings()) .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( "get_true", CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL))) - .build(); - AtomicInteger invocation = new AtomicInteger(); - CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() - .addMessageTypes(TestAllTypes.getDescriptor()) .addFunctionBindings( - CelFunctionBinding.from( - "get_true_overload", - ImmutableList.of(), - arg -> { - invocation.getAndIncrement(); - return true; - })) + CelFunctionBinding.fromOverloads( + "get_true", + CelFunctionBinding.from( + "get_true_overload", + ImmutableList.of(), + arg -> { + invocation.getAndIncrement(); + return true; + }))) .build(); - CelAbstractSyntaxTree ast = - celCompiler.compile("cel.bind(t, get_true(), has(msg.single_int64) ? t : false)").getAst(); - boolean result = (boolean) - celRuntime - .createProgram(ast) - .eval(ImmutableMap.of("msg", TestAllTypes.getDefaultInstance())); + eval( + customCel, + "cel.bind(t, get_true(), has(msg.single_int64) ? t : false)", + ImmutableMap.of("msg", TestAllTypes.getDefaultInstance())); assertThat(result).isFalse(); assertThat(invocation.get()).isEqualTo(0); } + @Test + public void lazyBinding_throwsEvaluationException() throws Exception { + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> eval(cel, "cel.bind(t, 1 / 0, t)")); + + assertThat(e).hasMessageThat().contains("/ by zero"); + assertThat(e).hasCauseThat().isInstanceOf(CelDivideByZeroException.class); + } + @Test @SuppressWarnings("Immutable") // Test only public void lazyBinding_accuInitEvaluatedOnce() throws Exception { - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder() - .addLibraries(CelExtensions.bindings()) + AtomicInteger invocation = new AtomicInteger(); + Cel customCel = + runtimeFlavor + .builder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) + .addCompilerLibraries(CelExtensions.bindings()) .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( "get_true", CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL))) - .build(); - AtomicInteger invocation = new AtomicInteger(); - CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() .addFunctionBindings( - CelFunctionBinding.from( - "get_true_overload", - ImmutableList.of(), - arg -> { - invocation.getAndIncrement(); - return true; - })) + CelFunctionBinding.fromOverloads( + "get_true", + CelFunctionBinding.from( + "get_true_overload", + ImmutableList.of(), + arg -> { + invocation.getAndIncrement(); + return true; + }))) .build(); - CelAbstractSyntaxTree ast = - celCompiler.compile("cel.bind(t, get_true(), t && t && t && t)").getAst(); - - boolean result = (boolean) celRuntime.createProgram(ast).eval(); + boolean result = (boolean) eval(customCel, "cel.bind(t, get_true(), t && t && t && t)"); assertThat(result).isTrue(); assertThat(invocation.get()).isEqualTo(1); @@ -215,32 +230,32 @@ public void lazyBinding_accuInitEvaluatedOnce() throws Exception { @Test @SuppressWarnings("Immutable") // Test only public void lazyBinding_withNestedBinds() throws Exception { - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder() - .addLibraries(CelExtensions.bindings()) + AtomicInteger invocation = new AtomicInteger(); + Cel customCel = + runtimeFlavor + .builder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) + .addCompilerLibraries(CelExtensions.bindings()) .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( "get_true", CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL))) - .build(); - AtomicInteger invocation = new AtomicInteger(); - CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() .addFunctionBindings( - CelFunctionBinding.from( - "get_true_overload", - ImmutableList.of(), - arg -> { - invocation.getAndIncrement(); - return true; - })) + CelFunctionBinding.fromOverloads( + "get_true", + CelFunctionBinding.from( + "get_true_overload", + ImmutableList.of(), + arg -> { + invocation.getAndIncrement(); + return true; + }))) .build(); - CelAbstractSyntaxTree ast = - celCompiler - .compile("cel.bind(t1, get_true(), cel.bind(t2, get_true(), t1 && t2 && t1 && t2))") - .getAst(); - - boolean result = (boolean) celRuntime.createProgram(ast).eval(); + boolean result = + (boolean) + eval( + customCel, + "cel.bind(t1, get_true(), cel.bind(t2, get_true(), t1 && t2 && t1 && t2))"); assertThat(result).isTrue(); assertThat(invocation.get()).isEqualTo(2); @@ -249,32 +264,31 @@ public void lazyBinding_withNestedBinds() throws Exception { @Test @SuppressWarnings({"Immutable", "unchecked"}) // Test only public void lazyBinding_boundAttributeInComprehension() throws Exception { - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder() + AtomicInteger invocation = new AtomicInteger(); + Cel customCel = + runtimeFlavor + .builder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) .setStandardMacros(CelStandardMacro.MAP) - .addLibraries(CelExtensions.bindings()) + .addCompilerLibraries(CelExtensions.bindings()) .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( "get_true", CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL))) - .build(); - AtomicInteger invocation = new AtomicInteger(); - CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() .addFunctionBindings( - CelFunctionBinding.from( - "get_true_overload", - ImmutableList.of(), - arg -> { - invocation.getAndIncrement(); - return true; - })) + CelFunctionBinding.fromOverloads( + "get_true", + CelFunctionBinding.from( + "get_true_overload", + ImmutableList.of(), + arg -> { + invocation.getAndIncrement(); + return true; + }))) .build(); - CelAbstractSyntaxTree ast = - celCompiler.compile("cel.bind(x, get_true(), [1,2,3].map(y, y < 0 || x))").getAst(); - - List result = (List) celRuntime.createProgram(ast).eval(); + List result = + (List) eval(customCel, "cel.bind(x, get_true(), [1,2,3].map(y, y < 0 || x))"); assertThat(result).containsExactly(true, true, true); assertThat(invocation.get()).isEqualTo(1); @@ -283,38 +297,55 @@ public void lazyBinding_boundAttributeInComprehension() throws Exception { @Test @SuppressWarnings({"Immutable"}) // Test only public void lazyBinding_boundAttributeInNestedComprehension() throws Exception { - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder() + AtomicInteger invocation = new AtomicInteger(); + Cel customCel = + runtimeFlavor + .builder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) .setStandardMacros(CelStandardMacro.EXISTS) - .addLibraries(CelExtensions.bindings()) + .addCompilerLibraries(CelExtensions.bindings()) .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( "get_true", CelOverloadDecl.newGlobalOverload("get_true_overload", SimpleType.BOOL))) - .build(); - AtomicInteger invocation = new AtomicInteger(); - CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() .addFunctionBindings( - CelFunctionBinding.from( - "get_true_overload", - ImmutableList.of(), - arg -> { - invocation.getAndIncrement(); - return true; - })) + CelFunctionBinding.fromOverloads( + "get_true", + CelFunctionBinding.from( + "get_true_overload", + ImmutableList.of(), + arg -> { + invocation.getAndIncrement(); + return true; + }))) .build(); - CelAbstractSyntaxTree ast = - celCompiler - .compile( + boolean result = + (boolean) + eval( + customCel, "cel.bind(x, get_true(), [1,2,3].exists(unused, x && " - + "['a','b','c'].exists(unused_2, x)))") - .getAst(); - - boolean result = (boolean) celRuntime.createProgram(ast).eval(); + + "['a','b','c'].exists(unused_2, x)))"); assertThat(result).isTrue(); assertThat(invocation.get()).isEqualTo(1); } + + private Object eval(Cel cel, String expression) throws Exception { + return eval(cel, expression, ImmutableMap.of()); + } + + private Object eval(Cel cel, String expression, Map variables) throws Exception { + CelAbstractSyntaxTree ast; + if (isParseOnly) { + ast = cel.parse(expression).getAst(); + } else { + ast = cel.compile(expression).getAst(); + } + return cel.createProgram(ast).eval(variables); + } + + private Object eval(String expression) throws Exception { + return eval(this.cel, expression, ImmutableMap.of()); + } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index cb2ad5a82..824c918d8 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -364,6 +364,7 @@ java_library( deps = [ ":activation_wrapper", ":planned_interpretable", + "//common/exceptions:runtime_exception", "//runtime:accumulated_unknowns", "//runtime:concatenated_list_view", "//runtime:evaluation_exception", diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java index 2631bf0b9..2eb30671e 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java @@ -16,6 +16,7 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.exceptions.CelRuntimeException; import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.ConcatenatedListView; @@ -77,8 +78,7 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval if (iterRangeRaw instanceof AccumulatedUnknowns) { return iterRangeRaw; } - Folder folder = new Folder(resolver, accuVar, iterVar, iterVar2); - folder.accuVal = maybeWrapAccumulator(accuInit.eval(folder, frame)); + Folder folder = new Folder(resolver, frame, accuInit, accuVar, iterVar, iterVar2); Object result; if (iterRangeRaw instanceof Map) { @@ -104,11 +104,14 @@ private Object evalMap(Map iterRange, Folder folder, ExecutionFrame frame) boolean cond = (boolean) condition.eval(folder, frame); if (!cond) { + folder.computeResult = true; return result.eval(folder, frame); } folder.accuVal = loopStep.eval(folder, frame); + folder.initialized = true; } + folder.computeResult = true; return result.eval(folder, frame); } @@ -127,12 +130,15 @@ private Object evalList(Collection iterRange, Folder folder, ExecutionFrame f boolean cond = (boolean) condition.eval(folder, frame); if (!cond) { + folder.computeResult = true; return result.eval(folder, frame); } folder.accuVal = loopStep.eval(folder, frame); + folder.initialized = true; index++; } + folder.computeResult = true; return result.eval(folder, frame); } @@ -155,6 +161,8 @@ private static Object maybeUnwrapAccumulator(Object val) { private static class Folder implements ActivationWrapper { private final GlobalResolver resolver; + private final ExecutionFrame frame; + private final PlannedInterpretable accuInit; private final String accuVar; private final String iterVar; private final String iterVar2; @@ -162,9 +170,19 @@ private static class Folder implements ActivationWrapper { private Object iterVarVal; private Object iterVar2Val; private Object accuVal; - - private Folder(GlobalResolver resolver, String accuVar, String iterVar, String iterVar2) { + private boolean initialized = false; + private boolean computeResult = false; + + private Folder( + GlobalResolver resolver, + ExecutionFrame frame, + PlannedInterpretable accuInit, + String accuVar, + String iterVar, + String iterVar2) { this.resolver = resolver; + this.frame = frame; + this.accuInit = accuInit; this.accuVar = accuVar; this.iterVar = iterVar; this.iterVar2 = iterVar2; @@ -183,18 +201,34 @@ public boolean isLocallyBound(String name) { @Override public @Nullable Object resolve(String name) { if (name.equals(accuVar)) { + if (!initialized) { + initialized = true; + try { + accuVal = maybeWrapAccumulator(accuInit.eval(resolver, frame)); + } catch (CelEvaluationException e) { + throw new LazyEvaluationRuntimeException(e); + } + } return accuVal; } - if (name.equals(iterVar)) { - return this.iterVarVal; - } + if (!computeResult) { + if (name.equals(iterVar)) { + return this.iterVarVal; + } - if (name.equals(iterVar2)) { - return this.iterVar2Val; + if (name.equals(iterVar2)) { + return this.iterVar2Val; + } } return resolver.resolve(name); } } + + private static class LazyEvaluationRuntimeException extends CelRuntimeException { + private LazyEvaluationRuntimeException(CelEvaluationException cause) { + super(cause, cause.getErrorCode()); + } + } } From 3075687e905227f5daa4da15df01b480397eb0c1 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 14 Apr 2026 18:04:42 -0700 Subject: [PATCH 047/204] Add parsed-only evaluation coverage to Proto Extensions PiperOrigin-RevId: 899863532 --- .../extensions/CelProtoExtensionsTest.java | 173 +++++++++--------- 1 file changed, 88 insertions(+), 85 deletions(-) diff --git a/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java index 15f6df5be..2e55619db 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java @@ -26,7 +26,6 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.bundle.Cel; -import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; @@ -35,8 +34,6 @@ import dev.cel.common.CelValidationException; import dev.cel.common.types.SimpleType; import dev.cel.common.types.StructTypeReference; -import dev.cel.compiler.CelCompiler; -import dev.cel.compiler.CelCompilerFactory; import dev.cel.expr.conformance.proto2.Proto2ExtensionScopedMessage; import dev.cel.expr.conformance.proto2.TestAllTypes; import dev.cel.expr.conformance.proto2.TestAllTypes.NestedEnum; @@ -44,27 +41,35 @@ import dev.cel.parser.CelMacro; import dev.cel.parser.CelStandardMacro; import dev.cel.runtime.CelFunctionBinding; -import dev.cel.runtime.CelRuntime; -import dev.cel.runtime.CelRuntimeFactory; +import dev.cel.testing.CelRuntimeFlavor; +import java.util.Map; +import org.junit.Assume; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) public final class CelProtoExtensionsTest { - private static final CelCompiler CEL_COMPILER = - CelCompilerFactory.standardCelCompilerBuilder() - .addLibraries(CelExtensions.protos()) - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .addFileTypes(TestAllTypesExtensions.getDescriptor()) - .addVar("msg", StructTypeReference.create("cel.expr.conformance.proto2.TestAllTypes")) - .setContainer(CelContainer.ofName("cel.expr.conformance.proto2")) - .build(); + @TestParameter public CelRuntimeFlavor runtimeFlavor; + @TestParameter public boolean isParseOnly; - private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder() - .addFileTypes(TestAllTypesExtensions.getDescriptor()) - .build(); + private Cel cel; + + @Before + public void setUp() { + // Legacy runtime does not support parsed-only evaluation mode. + Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.LEGACY) && isParseOnly); + this.cel = + runtimeFlavor + .builder() + .addCompilerLibraries(CelExtensions.protos()) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addFileTypes(TestAllTypesExtensions.getDescriptor()) + .addVar("msg", StructTypeReference.create("cel.expr.conformance.proto2.TestAllTypes")) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto2")) + .build(); + } private static final TestAllTypes PACKAGE_SCOPED_EXT_MSG = TestAllTypes.newBuilder() @@ -106,10 +111,7 @@ public void library() { "{expr: 'proto.hasExt(msg, cel.expr.conformance.proto2.repeated_test_all_types)'}") @TestParameters("{expr: '!proto.hasExt(msg, cel.expr.conformance.proto2.test_all_types_ext)'}") public void hasExt_packageScoped_success(String expr) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - boolean result = - (boolean) - CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", PACKAGE_SCOPED_EXT_MSG)); + boolean result = (boolean) eval(expr, ImmutableMap.of("msg", PACKAGE_SCOPED_EXT_MSG)); assertThat(result).isTrue(); } @@ -128,10 +130,7 @@ public void hasExt_packageScoped_success(String expr) throws Exception { "{expr: '!proto.hasExt(msg," + " cel.expr.conformance.proto2.Proto2ExtensionScopedMessage.nested_enum_ext)'}") public void hasExt_messageScoped_success(String expr) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - boolean result = - (boolean) - CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", MESSAGE_SCOPED_EXT_MSG)); + boolean result = (boolean) eval(expr, ImmutableMap.of("msg", MESSAGE_SCOPED_EXT_MSG)); assertThat(result).isTrue(); } @@ -142,9 +141,10 @@ public void hasExt_messageScoped_success(String expr) throws Exception { public void hasExt_nonProtoNamespace_success(String expr) throws Exception { StructTypeReference proto2MessageTypeReference = StructTypeReference.create("cel.expr.conformance.proto2.TestAllTypes"); - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder() - .addLibraries(CelExtensions.protos()) + Cel customCel = + runtimeFlavor + .builder() + .addCompilerLibraries(CelExtensions.protos()) .addVar("msg", proto2MessageTypeReference) .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( @@ -154,37 +154,35 @@ public void hasExt_nonProtoNamespace_success(String expr) throws Exception { SimpleType.BOOL, ImmutableList.of( proto2MessageTypeReference, SimpleType.STRING, SimpleType.INT)))) - .build(); - CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() .addFunctionBindings( - CelFunctionBinding.from( - "msg_hasExt", - ImmutableList.of(TestAllTypes.class, String.class, Long.class), - (arg) -> { - TestAllTypes msg = (TestAllTypes) arg[0]; - String extensionField = (String) arg[1]; - return msg.getAllFields().keySet().stream() - .anyMatch(fd -> fd.getFullName().equals(extensionField)); - })) + CelFunctionBinding.fromOverloads( + "hasExt", + CelFunctionBinding.from( + "msg_hasExt", + ImmutableList.of(TestAllTypes.class, String.class, Long.class), + (arg) -> { + TestAllTypes msg = (TestAllTypes) arg[0]; + String extensionField = (String) arg[1]; + return msg.getAllFields().keySet().stream() + .anyMatch(fd -> fd.getFullName().equals(extensionField)); + }))) .build(); - CelAbstractSyntaxTree ast = celCompiler.compile(expr).getAst(); boolean result = - (boolean) - celRuntime.createProgram(ast).eval(ImmutableMap.of("msg", PACKAGE_SCOPED_EXT_MSG)); + (boolean) eval(customCel, expr, ImmutableMap.of("msg", PACKAGE_SCOPED_EXT_MSG)); assertThat(result).isTrue(); } @Test public void hasExt_undefinedField_throwsException() { + // This is a type-checking failure + Assume.assumeFalse(isParseOnly); CelValidationException exception = assertThrows( CelValidationException.class, () -> - CEL_COMPILER - .compile("!proto.hasExt(msg, cel.expr.conformance.proto2.undefined_field)") + cel.compile("!proto.hasExt(msg, cel.expr.conformance.proto2.undefined_field)") .getAst()); assertThat(exception) @@ -204,10 +202,7 @@ public void hasExt_undefinedField_throwsException() { "{expr: 'proto.getExt(msg, cel.expr.conformance.proto2.repeated_test_all_types) ==" + " [TestAllTypes{single_string: ''A''}, TestAllTypes{single_string: ''B''}]'}") public void getExt_packageScoped_success(String expr) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - boolean result = - (boolean) - CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", PACKAGE_SCOPED_EXT_MSG)); + boolean result = (boolean) eval(expr, ImmutableMap.of("msg", PACKAGE_SCOPED_EXT_MSG)); assertThat(result).isTrue(); } @@ -221,22 +216,20 @@ public void getExt_packageScoped_success(String expr) throws Exception { "{expr: 'proto.getExt(msg," + " cel.expr.conformance.proto2.Proto2ExtensionScopedMessage.int64_ext) == 1'}") public void getExt_messageScopedSuccess(String expr) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - boolean result = - (boolean) - CEL_RUNTIME.createProgram(ast).eval(ImmutableMap.of("msg", MESSAGE_SCOPED_EXT_MSG)); + boolean result = (boolean) eval(expr, ImmutableMap.of("msg", MESSAGE_SCOPED_EXT_MSG)); assertThat(result).isTrue(); } @Test public void getExt_undefinedField_throwsException() { + // This is a type-checking failure + Assume.assumeFalse(isParseOnly); CelValidationException exception = assertThrows( CelValidationException.class, () -> - CEL_COMPILER - .compile("!proto.getExt(msg, cel.expr.conformance.proto2.undefined_field)") + cel.compile("!proto.getExt(msg, cel.expr.conformance.proto2.undefined_field)") .getAst()); assertThat(exception) @@ -250,9 +243,10 @@ public void getExt_undefinedField_throwsException() { public void getExt_nonProtoNamespace_success(String expr) throws Exception { StructTypeReference proto2MessageTypeReference = StructTypeReference.create("cel.expr.conformance.proto2.TestAllTypes"); - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder() - .addLibraries(CelExtensions.protos()) + Cel customCel = + runtimeFlavor + .builder() + .addCompilerLibraries(CelExtensions.protos()) .addVar("msg", proto2MessageTypeReference) .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( @@ -262,29 +256,26 @@ public void getExt_nonProtoNamespace_success(String expr) throws Exception { SimpleType.DYN, ImmutableList.of( proto2MessageTypeReference, SimpleType.STRING, SimpleType.INT)))) - .build(); - CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() .addFunctionBindings( - CelFunctionBinding.from( - "msg_getExt", - ImmutableList.of(TestAllTypes.class, String.class, Long.class), - (arg) -> { - TestAllTypes msg = (TestAllTypes) arg[0]; - String extensionField = (String) arg[1]; - FieldDescriptor extensionDescriptor = - msg.getAllFields().keySet().stream() - .filter(fd -> fd.getFullName().equals(extensionField)) - .findAny() - .get(); - return msg.getField(extensionDescriptor); - })) + CelFunctionBinding.fromOverloads( + "getExt", + CelFunctionBinding.from( + "msg_getExt", + ImmutableList.of(TestAllTypes.class, String.class, Long.class), + (arg) -> { + TestAllTypes msg = (TestAllTypes) arg[0]; + String extensionField = (String) arg[1]; + FieldDescriptor extensionDescriptor = + msg.getAllFields().keySet().stream() + .filter(fd -> fd.getFullName().equals(extensionField)) + .findAny() + .get(); + return msg.getField(extensionDescriptor); + }))) .build(); - CelAbstractSyntaxTree ast = celCompiler.compile(expr).getAst(); boolean result = - (boolean) - celRuntime.createProgram(ast).eval(ImmutableMap.of("msg", PACKAGE_SCOPED_EXT_MSG)); + (boolean) eval(customCel, expr, ImmutableMap.of("msg", PACKAGE_SCOPED_EXT_MSG)); assertThat(result).isTrue(); } @@ -293,21 +284,24 @@ public void getExt_nonProtoNamespace_success(String expr) throws Exception { public void getExt_onAnyPackedExtensionField_success() throws Exception { ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); TestAllTypesExtensions.registerAllExtensions(extensionRegistry); - Cel cel = - CelFactory.standardCelBuilder() + Cel customCel = + runtimeFlavor + .builder() // CEL-Internal-2 .addCompilerLibraries(CelExtensions.protos()) .addFileTypes(TestAllTypesExtensions.getDescriptor()) .setExtensionRegistry(extensionRegistry) .addVar("msg", StructTypeReference.create("cel.expr.conformance.proto2.TestAllTypes")) .build(); - CelAbstractSyntaxTree ast = - cel.compile("proto.getExt(msg, cel.expr.conformance.proto2.int32_ext)").getAst(); Any anyMsg = Any.pack( TestAllTypes.newBuilder().setExtension(TestAllTypesExtensions.int32Ext, 1).build()); - - Long result = (Long) cel.createProgram(ast).eval(ImmutableMap.of("msg", anyMsg)); + Long result = + (Long) + eval( + customCel, + "proto.getExt(msg, cel.expr.conformance.proto2.int32_ext)", + ImmutableMap.of("msg", anyMsg)); assertThat(result).isEqualTo(1); } @@ -343,9 +337,18 @@ private enum ParseErrorTestCase { @Test public void parseErrors(@TestParameter ParseErrorTestCase testcase) { CelValidationException e = - assertThrows( - CelValidationException.class, () -> CEL_COMPILER.compile(testcase.expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.parse(testcase.expr).getAst()); assertThat(e).hasMessageThat().isEqualTo(testcase.error); } + + private Object eval(String expression, Map variables) throws Exception { + return eval(this.cel, expression, variables); + } + + private Object eval(Cel cel, String expression, Map variables) throws Exception { + CelAbstractSyntaxTree ast = + this.isParseOnly ? cel.parse(expression).getAst() : cel.compile(expression).getAst(); + return cel.createProgram(ast).eval(variables); + } } From 2203ac83b0fb19ad56fe7b9bfb4a963034886bd3 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 14 Apr 2026 18:59:59 -0700 Subject: [PATCH 048/204] Support parsed-only evaluation for lists extensions, remove check for heterogeneous numeric comparisons for sorting PiperOrigin-RevId: 899880149 --- .../cel/extensions/CelListsExtensions.java | 66 +++----- .../extensions/CelListsExtensionsTest.java | 146 +++++++++--------- 2 files changed, 99 insertions(+), 113 deletions(-) diff --git a/extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java index a91edd822..79539b008 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java @@ -128,7 +128,8 @@ public enum Function { "list_sort", "Sorts a list with comparable elements.", ListType.create(TypeParamType.create("T")), - ListType.create(TypeParamType.create("T"))))), + ListType.create(TypeParamType.create("T")))), + CelFunctionBinding.from("list_sort", Collection.class, CelListsExtensions::sort)), SORT_BY( CelFunctionDecl.newFunctionDeclaration( "lists.@sortByAssociatedKeys", @@ -136,7 +137,11 @@ public enum Function { "list_sortByAssociatedKeys", "Sorts a list by a key value. Used by the 'sortBy' macro", ListType.create(TypeParamType.create("T")), - ListType.create(TypeParamType.create("T"))))); + ListType.create(TypeParamType.create("T")))), + CelFunctionBinding.from( + "list_sortByAssociatedKeys", + Collection.class, + CelListsExtensions::sortByAssociatedKeys)); private final CelFunctionDecl functionDecl; private final ImmutableSet functionBindings; @@ -147,7 +152,10 @@ String getFunction() { Function(CelFunctionDecl functionDecl, CelFunctionBinding... functionBindings) { this.functionDecl = functionDecl; - this.functionBindings = ImmutableSet.copyOf(functionBindings); + this.functionBindings = + functionBindings.length > 0 + ? CelFunctionBinding.fromOverloads(functionDecl.name(), functionBindings) + : ImmutableSet.of(); } } @@ -240,32 +248,13 @@ public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) { @Override public void setRuntimeOptions( CelRuntimeBuilder runtimeBuilder, RuntimeEquality runtimeEquality, CelOptions celOptions) { - for (Function function : functions) { - runtimeBuilder.addFunctionBindings(function.functionBindings); - for (CelOverloadDecl overload : function.functionDecl.overloads()) { - switch (overload.overloadId()) { - case "list_distinct": - runtimeBuilder.addFunctionBindings( - CelFunctionBinding.from( - "list_distinct", Collection.class, (list) -> distinct(list, runtimeEquality))); - break; - case "list_sort": - runtimeBuilder.addFunctionBindings( - CelFunctionBinding.from( - "list_sort", Collection.class, (list) -> sort(list, celOptions))); - break; - case "list_sortByAssociatedKeys": - runtimeBuilder.addFunctionBindings( - CelFunctionBinding.from( - "list_sortByAssociatedKeys", - Collection.class, - (list) -> sortByAssociatedKeys(list, celOptions))); - break; - default: - // Nothing to add - } - } - } + functions.forEach(function -> runtimeBuilder.addFunctionBindings(function.functionBindings)); + + runtimeBuilder.addFunctionBindings( + CelFunctionBinding.fromOverloads( + "distinct", + CelFunctionBinding.from( + "list_distinct", Collection.class, (list) -> distinct(list, runtimeEquality)))); } private static ImmutableList slice(Collection list, long from, long to) { @@ -369,22 +358,18 @@ private static List reverse(Collection list) { } } - private static ImmutableList sort(Collection objects, CelOptions options) { - return ImmutableList.sortedCopyOf( - new CelObjectComparator(options.enableHeterogeneousNumericComparisons()), objects); + private static ImmutableList sort(Collection objects) { + return ImmutableList.sortedCopyOf(new CelObjectComparator(), objects); } private static class CelObjectComparator implements Comparator { - private final boolean enableHeterogeneousNumericComparisons; - CelObjectComparator(boolean enableHeterogeneousNumericComparisons) { - this.enableHeterogeneousNumericComparisons = enableHeterogeneousNumericComparisons; - } + CelObjectComparator() {} @SuppressWarnings({"unchecked"}) @Override public int compare(Object o1, Object o2) { - if (o1 instanceof Number && o2 instanceof Number && enableHeterogeneousNumericComparisons) { + if (o1 instanceof Number && o2 instanceof Number) { return ComparisonFunctions.numericCompare((Number) o1, (Number) o2); } @@ -444,12 +429,9 @@ private static Optional sortByMacro( @SuppressWarnings({"unchecked", "rawtypes"}) private static ImmutableList sortByAssociatedKeys( - Collection> keyValuePairs, CelOptions options) { + Collection> keyValuePairs) { List[] array = keyValuePairs.toArray(new List[0]); - Arrays.sort( - array, - new CelObjectByKeyComparator( - new CelObjectComparator(options.enableHeterogeneousNumericComparisons()))); + Arrays.sort(array, new CelObjectByKeyComparator(new CelObjectComparator())); ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(array.length); for (List pair : array) { builder.add(pair.get(1)); diff --git a/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java index c4739b18b..2083ccc42 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java @@ -19,41 +19,38 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedMultiset; import com.google.common.collect.ImmutableSortedSet; +import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.bundle.Cel; -import dev.cel.bundle.CelFactory; +import dev.cel.bundle.CelBuilder; +import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; -import dev.cel.common.CelOptions; import dev.cel.common.CelValidationException; import dev.cel.common.types.SimpleType; import dev.cel.expr.conformance.test.SimpleTest; import dev.cel.parser.CelStandardMacro; import dev.cel.runtime.CelEvaluationException; +import dev.cel.testing.CelRuntimeFlavor; +import java.util.Map; +import org.junit.Assume; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) public class CelListsExtensionsTest { - private static final Cel CEL = - CelFactory.standardCelBuilder() - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .addCompilerLibraries(CelExtensions.lists()) - .addRuntimeLibraries(CelExtensions.lists()) - .setContainer(CelContainer.ofName("cel.expr.conformance.test")) - .addMessageTypes(SimpleTest.getDescriptor()) - .addVar("non_list", SimpleType.DYN) - .build(); - - private static final Cel CEL_WITH_HETEROGENEOUS_NUMERIC_COMPARISONS = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .addCompilerLibraries(CelExtensions.lists()) - .addRuntimeLibraries(CelExtensions.lists()) - .setContainer(CelContainer.ofName("cel.expr.conformance.test")) - .addMessageTypes(SimpleTest.getDescriptor()) - .build(); + @TestParameter public CelRuntimeFlavor runtimeFlavor; + @TestParameter public boolean isParseOnly; + + private Cel cel; + + @Before + public void setUp() { + // Legacy runtime does not support parsed-only evaluation mode. + Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.LEGACY) && isParseOnly); + this.cel = setupEnv(runtimeFlavor.builder()); + } @Test public void functionList_byVersion() { @@ -89,10 +86,9 @@ public void macroList_byVersion() { @TestParameters("{expression: 'non_list.slice(1, 3)', expected: '[2, 3]'}") public void slice_success(String expression, String expected) throws Exception { Object result = - CEL.createProgram(CEL.compile(expression).getAst()) - .eval(ImmutableMap.of("non_list", ImmutableSortedSet.of(4L, 1L, 3L, 2L))); + eval(cel, expression, ImmutableMap.of("non_list", ImmutableSortedSet.of(4L, 1L, 3L, 2L))); - assertThat(result).isEqualTo(expectedResult(expected)); + assertThat(result).isEqualTo(eval(cel, expected)); } @Test @@ -107,10 +103,7 @@ public void slice_success(String expression, String expected) throws Exception { "{expression: '[1,2,3,4].slice(-5, -3)', " + "expectedError: 'Negative indexes not supported'}") public void slice_throws(String expression, String expectedError) throws Exception { - assertThat( - assertThrows( - CelEvaluationException.class, - () -> CEL.createProgram(CEL.compile(expression).getAst()).eval())) + assertThat(assertThrows(CelEvaluationException.class, () -> eval(cel, expression))) .hasCauseThat() .hasMessageThat() .contains(expectedError); @@ -127,7 +120,7 @@ public void slice_throws(String expression, String expectedError) throws Excepti @TestParameters("{expression: 'dyn([{1: 2}]).flatten() == [{1: 2}]'}") @TestParameters("{expression: 'dyn([1,2,3,4]).flatten() == [1,2,3,4]'}") public void flattenSingleLevel_success(String expression) throws Exception { - boolean result = (boolean) CEL.createProgram(CEL.compile(expression).getAst()).eval(); + boolean result = (boolean) eval(cel, expression); assertThat(result).isTrue(); } @@ -143,7 +136,7 @@ public void flattenSingleLevel_success(String expression) throws Exception { // The overload with the depth accepts and returns a List(dyn), so the following is permitted. @TestParameters("{expression: '[1].flatten(1) == [1]'}") public void flatten_withDepthValue_success(String expression) throws Exception { - boolean result = (boolean) CEL.createProgram(CEL.compile(expression).getAst()).eval(); + boolean result = (boolean) eval(cel, expression); assertThat(result).isTrue(); } @@ -151,13 +144,17 @@ public void flatten_withDepthValue_success(String expression) throws Exception { @Test public void flatten_negativeDepth_throws() { CelEvaluationException e = - assertThrows( - CelEvaluationException.class, - () -> CEL.createProgram(CEL.compile("[1,2,3,4].flatten(-1)").getAst()).eval()); - - assertThat(e) - .hasMessageThat() - .contains("evaluation error at :17: Function 'list_flatten_list_int' failed"); + assertThrows(CelEvaluationException.class, () -> eval(cel, "[1,2,3,4].flatten(-1)")); + + if (isParseOnly) { + assertThat(e) + .hasMessageThat() + .contains("evaluation error at :17: Function 'flatten' failed"); + } else { + assertThat(e) + .hasMessageThat() + .contains("evaluation error at :17: Function 'list_flatten_list_int' failed"); + } assertThat(e).hasCauseThat().hasMessageThat().isEqualTo("Level must be non-negative"); } @@ -166,9 +163,11 @@ public void flatten_negativeDepth_throws() { @TestParameters("{expression: '[{1: 2}].flatten()'}") @TestParameters("{expression: '[1,2,3,4].flatten()'}") public void flattenSingleLevel_listIsSingleLevel_throws(String expression) { + // This is a type-checking failure. + Assume.assumeFalse(isParseOnly); // Note: Java lacks the capability of conditionally disabling type guards // due to the lack of full-fledged dynamic dispatch. - assertThrows(CelValidationException.class, () -> CEL.compile(expression).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expression).getAst()); } @Test @@ -176,7 +175,7 @@ public void flattenSingleLevel_listIsSingleLevel_throws(String expression) { @TestParameters("{expression: 'lists.range(0) == []'}") @TestParameters("{expression: 'lists.range(-1) == []'}") public void range_success(String expression) throws Exception { - boolean result = (boolean) CEL.createProgram(CEL.compile(expression).getAst()).eval(); + boolean result = (boolean) eval(cel, expression); assertThat(result).isTrue(); } @@ -204,12 +203,13 @@ public void range_success(String expression) throws Exception { @TestParameters("{expression: 'non_list.distinct()', expected: '[1, 2, 3, 4]'}") public void distinct_success(String expression, String expected) throws Exception { Object result = - CEL.createProgram(CEL.compile(expression).getAst()) - .eval( - ImmutableMap.of( - "non_list", ImmutableSortedMultiset.of(1L, 2L, 3L, 4L, 4L, 1L, 3L, 2L))); + eval( + cel, + expression, + ImmutableMap.of( + "non_list", ImmutableSortedMultiset.of(1L, 2L, 3L, 4L, 4L, 1L, 3L, 2L))); - assertThat(result).isEqualTo(expectedResult(expected)); + assertThat(result).isEqualTo(eval(cel, expected)); } @Test @@ -224,10 +224,9 @@ public void distinct_success(String expression, String expected) throws Exceptio @TestParameters("{expression: 'non_list.reverse()', expected: '[4, 3, 2, 1]'}") public void reverse_success(String expression, String expected) throws Exception { Object result = - CEL.createProgram(CEL.compile(expression).getAst()) - .eval(ImmutableMap.of("non_list", ImmutableSortedSet.of(4L, 1L, 3L, 2L))); + eval(cel, expression, ImmutableMap.of("non_list", ImmutableSortedSet.of(4L, 1L, 3L, 2L))); - assertThat(result).isEqualTo(expectedResult(expected)); + assertThat(result).isEqualTo(eval(cel, expected)); } @Test @@ -238,9 +237,9 @@ public void reverse_success(String expression, String expected) throws Exception "{expression: '[\"d\", \"a\", \"b\", \"c\"].sort()', " + "expected: '[\"a\", \"b\", \"c\", \"d\"]'}") public void sort_success(String expression, String expected) throws Exception { - Object result = CEL.createProgram(CEL.compile(expression).getAst()).eval(); + Object result = eval(cel, expression); - assertThat(result).isEqualTo(expectedResult(expected)); + assertThat(result).isEqualTo(eval(cel, expected)); } @Test @@ -248,29 +247,20 @@ public void sort_success(String expression, String expected) throws Exception { @TestParameters("{expression: '[4, 3, 2, 1].sort()', expected: '[1, 2, 3, 4]'}") public void sort_success_heterogeneousNumbers(String expression, String expected) throws Exception { - Object result = - CEL_WITH_HETEROGENEOUS_NUMERIC_COMPARISONS - .createProgram(CEL_WITH_HETEROGENEOUS_NUMERIC_COMPARISONS.compile(expression).getAst()) - .eval(); + Object result = eval(cel, expression); - assertThat(result).isEqualTo(expectedResult(expected)); + assertThat(result).isEqualTo(eval(cel, expected)); } @Test @TestParameters( "{expression: '[\"d\", 3, 2, \"c\"].sort()', " + "expectedError: 'List elements must have the same type'}") - @TestParameters( - "{expression: '[3.0, 2, 1u].sort()', " - + "expectedError: 'List elements must have the same type'}") @TestParameters( "{expression: '[SimpleTest{name: \"a\"}, SimpleTest{name: \"b\"}].sort()', " + "expectedError: 'List elements must be comparable'}") public void sort_throws(String expression, String expectedError) throws Exception { - assertThat( - assertThrows( - CelEvaluationException.class, - () -> CEL.createProgram(CEL.compile(expression).getAst()).eval())) + assertThat(assertThrows(CelEvaluationException.class, () -> eval(cel, expression))) .hasCauseThat() .hasMessageThat() .contains(expectedError); @@ -296,9 +286,9 @@ public void sort_throws(String expression, String expectedError) throws Exceptio + " SimpleTest{name: \"baz\"}," + " SimpleTest{name: \"foo\"}]'}") public void sortBy_success(String expression, String expected) throws Exception { - Object result = CEL.createProgram(CEL.compile(expression).getAst()).eval(); + Object result = eval(cel, expression); - assertThat(result).isEqualTo(expectedResult(expected)); + assertThat(result).isEqualTo(eval(cel, expected)); } @Test @@ -313,7 +303,7 @@ public void sortBy_throws_validationException(String expression, String expected assertThat( assertThrows( CelValidationException.class, - () -> CEL.createProgram(CEL.compile(expression).getAst()).eval())) + () -> cel.createProgram(cel.compile(expression).getAst()).eval())) .hasMessageThat() .contains(expectedError); } @@ -327,17 +317,31 @@ public void sortBy_throws_validationException(String expression, String expected + "expectedError: 'List elements must be comparable'}") public void sortBy_throws_evaluationException(String expression, String expectedError) throws Exception { - assertThat( - assertThrows( - CelEvaluationException.class, - () -> CEL.createProgram(CEL.compile(expression).getAst()).eval())) + assertThat(assertThrows(CelEvaluationException.class, () -> eval(cel, expression))) .hasCauseThat() .hasMessageThat() .contains(expectedError); } - private static Object expectedResult(String expression) - throws CelEvaluationException, CelValidationException { - return CEL.createProgram(CEL.compile(expression).getAst()).eval(); + private static Cel setupEnv(CelBuilder celBuilder) { + return celBuilder + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addCompilerLibraries(CelExtensions.lists()) + .addRuntimeLibraries(CelExtensions.lists()) + .setContainer(CelContainer.ofName("cel.expr.conformance.test")) + .addMessageTypes(SimpleTest.getDescriptor()) + .addVar("non_list", SimpleType.DYN) + .build(); + } + + + + private Object eval(Cel cel, String expr) throws Exception { + return eval(cel, expr, ImmutableMap.of()); + } + + private Object eval(Cel cel, String expr, Map vars) throws Exception { + CelAbstractSyntaxTree ast = isParseOnly ? cel.parse(expr).getAst() : cel.compile(expr).getAst(); + return cel.createProgram(ast).eval(vars); } } From 4f9a3a8987fdb24297216b5817e4fcef7cb11b3a Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 15 Apr 2026 11:26:41 -0700 Subject: [PATCH 049/204] Add parsed-only evaluation coverage to Comprehensions Extensions Includes a fix to preserve first encountered error message for comprehensions PiperOrigin-RevId: 900264092 --- .../CelComprehensionsExtensionsTest.java | 150 +++++++++--------- .../extensions/CelListsExtensionsTest.java | 2 - .../java/dev/cel/runtime/planner/EvalAnd.java | 5 +- .../java/dev/cel/runtime/planner/EvalOr.java | 5 +- .../planner_unknownResultSet_errors.baseline | 4 +- 5 files changed, 89 insertions(+), 77 deletions(-) diff --git a/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java index 34696b688..fbe160cd3 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java @@ -15,11 +15,15 @@ package dev.cel.extensions; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertThrows; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; +import dev.cel.bundle.Cel; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; @@ -28,15 +32,15 @@ import dev.cel.common.exceptions.CelIndexOutOfBoundsException; import dev.cel.common.types.SimpleType; import dev.cel.common.types.TypeParamType; -import dev.cel.compiler.CelCompiler; -import dev.cel.compiler.CelCompilerFactory; import dev.cel.parser.CelMacro; import dev.cel.parser.CelStandardMacro; import dev.cel.parser.CelUnparser; import dev.cel.parser.CelUnparserFactory; import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelRuntime; -import dev.cel.runtime.CelRuntimeFactory; +import dev.cel.testing.CelRuntimeFlavor; +import java.util.Map; +import org.junit.Assume; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -46,26 +50,35 @@ public class CelComprehensionsExtensionsTest { private static final CelOptions CEL_OPTIONS = CelOptions.current() + .enableHeterogeneousNumericComparisons(true) // Enable macro call population for unparsing .populateMacroCalls(true) .build(); - private static final CelCompiler CEL_COMPILER = - CelCompilerFactory.standardCelCompilerBuilder() - .setOptions(CEL_OPTIONS) - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .addLibraries(CelExtensions.comprehensions()) - .addLibraries(CelExtensions.lists()) - .addLibraries(CelExtensions.strings()) - .addLibraries(CelOptionalLibrary.INSTANCE, CelExtensions.bindings()) - .build(); - private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder() - .addLibraries(CelOptionalLibrary.INSTANCE) - .addLibraries(CelExtensions.lists()) - .addLibraries(CelExtensions.strings()) - .addLibraries(CelExtensions.comprehensions()) - .build(); + @TestParameter public CelRuntimeFlavor runtimeFlavor; + @TestParameter public boolean isParseOnly; + + private Cel cel; + + @Before + public void setUp() { + // Legacy runtime does not support parsed-only evaluation mode. + Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.LEGACY) && isParseOnly); + this.cel = + runtimeFlavor + .builder() + .setOptions(CEL_OPTIONS) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addCompilerLibraries(CelExtensions.comprehensions()) + .addCompilerLibraries(CelExtensions.lists()) + .addCompilerLibraries(CelExtensions.strings()) + .addCompilerLibraries(CelOptionalLibrary.INSTANCE, CelExtensions.bindings()) + .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) + .addRuntimeLibraries(CelExtensions.lists()) + .addRuntimeLibraries(CelExtensions.strings()) + .addRuntimeLibraries(CelExtensions.comprehensions()) + .build(); + } private static final CelUnparser UNPARSER = CelUnparserFactory.newUnparser(); @@ -101,11 +114,7 @@ public void allMacro_twoVarComprehension_success( }) String expr) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - - Object result = CEL_RUNTIME.createProgram(ast).eval(); - - assertThat(result).isEqualTo(true); + assertThat(eval(expr)).isEqualTo(true); } @Test @@ -127,11 +136,7 @@ public void existsMacro_twoVarComprehension_success( }) String expr) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - - Object result = CEL_RUNTIME.createProgram(ast).eval(); - - assertThat(result).isEqualTo(true); + assertThat(eval(expr)).isEqualTo(true); } @Test @@ -156,11 +161,7 @@ public void exists_oneMacro_twoVarComprehension_success( }) String expr) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - - Object result = CEL_RUNTIME.createProgram(ast).eval(); - - assertThat(result).isEqualTo(true); + assertThat(eval(expr)).isEqualTo(true); } @Test @@ -182,11 +183,7 @@ public void transformListMacro_twoVarComprehension_success( }) String expr) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - - Object result = CEL_RUNTIME.createProgram(ast).eval(); - - assertThat(result).isEqualTo(true); + assertThat(eval(expr)).isEqualTo(true); } @Test @@ -210,11 +207,7 @@ public void transformMapMacro_twoVarComprehension_success( }) String expr) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - - Object result = CEL_RUNTIME.createProgram(ast).eval(); - - assertThat(result).isEqualTo(true); + assertThat(eval(expr)).isEqualTo(true); } @Test @@ -238,24 +231,22 @@ public void transformMapEntryMacro_twoVarComprehension_success( }) String expr) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - - Object result = CEL_RUNTIME.createProgram(ast).eval(); - - assertThat(result).isEqualTo(true); + assertThat(eval(expr)).isEqualTo(true); } @Test public void comprehension_onTypeParam_success() throws Exception { - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder() + Assume.assumeFalse(isParseOnly); + Cel customCel = + runtimeFlavor + .builder() .setOptions(CEL_OPTIONS) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .addLibraries(CelExtensions.comprehensions()) + .addCompilerLibraries(CelExtensions.comprehensions()) .addVar("items", TypeParamType.create("T")) .build(); - CelAbstractSyntaxTree ast = celCompiler.compile("items.all(i, v, v > 0)").getAst(); + CelAbstractSyntaxTree ast = customCel.compile("items.all(i, v, v > 0)").getAst(); assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); } @@ -275,7 +266,7 @@ public void unparseAST_twoVarComprehension( }) String expr) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = isParseOnly ? cel.parse(expr).getAst() : cel.compile(expr).getAst(); String unparsed = UNPARSER.unparse(ast); assertThat(unparsed).isEqualTo(expr); } @@ -318,8 +309,9 @@ public void unparseAST_twoVarComprehension( "{expr: \"{'hello': 'world', 'greetings': 'tacocat'}.transformMapEntry(k, v, []) == {}\"," + " err: 'no matching overload'}") public void twoVarComprehension_compilerErrors(String expr, String err) throws Exception { + Assume.assumeFalse(isParseOnly); CelValidationException e = - assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); assertThat(e).hasMessageThat().contains(err); } @@ -339,34 +331,50 @@ public void twoVarComprehension_compilerErrors(String expr, String err) throws E + " '2.0' already exists\"}") public void twoVarComprehension_keyCollision_runtimeError(String expr, String err) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - - CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> CEL_RUNTIME.createProgram(ast).eval()); - - assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); - assertThat(e).hasCauseThat().hasMessageThat().contains(err); + // Planner does not allow decimals for map keys + Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.PLANNER) && expr.contains("2.0")); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> eval(expr)); + Throwable cause = + Throwables.getCausalChain(e).stream() + .filter(IllegalArgumentException.class::isInstance) + .filter(t -> t.getMessage() != null && t.getMessage().contains(err)) + .findFirst() + .orElse(null); + + assertWithMessage( + "Expected IllegalArgumentException with message containing '%s' in cause chain", err) + .that(cause) + .isNotNull(); } @Test public void twoVarComprehension_arithmeticException_runtimeError() throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile("[0].all(i, k, i/k < k)").getAst(); - CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> CEL_RUNTIME.createProgram(ast).eval()); - + assertThrows(CelEvaluationException.class, () -> eval("[0].all(i, k, i/k < k)")); assertThat(e).hasCauseThat().isInstanceOf(CelDivideByZeroException.class); assertThat(e).hasCauseThat().hasMessageThat().contains("/ by zero"); } @Test public void twoVarComprehension_outOfBounds_runtimeError() throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile("[1, 2].exists(i, v, [0][v] > 0)").getAst(); - CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> CEL_RUNTIME.createProgram(ast).eval()); - + assertThrows(CelEvaluationException.class, () -> eval("[1, 2].exists(i, v, [0][v] > 0)")); assertThat(e).hasCauseThat().isInstanceOf(CelIndexOutOfBoundsException.class); assertThat(e).hasCauseThat().hasMessageThat().contains("Index out of bounds: 1"); } + + private Object eval(String expression) throws Exception { + return eval(this.cel, expression, ImmutableMap.of()); + } + + private Object eval(Cel cel, String expression, Map variables) throws Exception { + CelAbstractSyntaxTree ast; + if (isParseOnly) { + ast = cel.parse(expression).getAst(); + } else { + ast = cel.compile(expression).getAst(); + } + return cel.createProgram(ast).eval(variables); + } } diff --git a/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java index 2083ccc42..b36e0e92e 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java @@ -334,8 +334,6 @@ private static Cel setupEnv(CelBuilder celBuilder) { .build(); } - - private Object eval(Cel cel, String expr) throws Exception { return eval(cel, expr, ImmutableMap.of()); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java index eb7406071..91f5b2ff4 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java @@ -38,7 +38,10 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { return false; } } else if (argVal instanceof ErrorValue) { - errorValue = (ErrorValue) argVal; + // Preserve the first encountered error instead of overwriting it with subsequent errors. + if (errorValue == null) { + errorValue = (ErrorValue) argVal; + } } else if (argVal instanceof AccumulatedUnknowns) { unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVal); } else { diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java index bc19ed81a..62e617d9d 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java @@ -38,7 +38,10 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { return true; } } else if (argVal instanceof ErrorValue) { - errorValue = (ErrorValue) argVal; + // Preserve the first encountered error instead of overwriting it with subsequent errors. + if (errorValue == null) { + errorValue = (ErrorValue) argVal; + } } else if (argVal instanceof AccumulatedUnknowns) { unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVal); } else { diff --git a/runtime/src/test/resources/planner_unknownResultSet_errors.baseline b/runtime/src/test/resources/planner_unknownResultSet_errors.baseline index 812067ddf..7885e9da1 100644 --- a/runtime/src/test/resources/planner_unknownResultSet_errors.baseline +++ b/runtime/src/test/resources/planner_unknownResultSet_errors.baseline @@ -32,7 +32,7 @@ single_timestamp { seconds: 15 } , unknown_attributes=[x.single_int32]} -error: evaluation error at test_location:89: Text 'another bad timestamp string' could not be parsed at index 0 +error: evaluation error at test_location:31: Text 'bad timestamp string' could not be parsed at index 0 error_code: BAD_FORMAT Source: x.single_int32 == 1 || x.single_timestamp <= timestamp("bad timestamp string") @@ -69,7 +69,7 @@ single_timestamp { seconds: 15 } , unknown_attributes=[x.single_int32]} -error: evaluation error at test_location:89: Text 'another bad timestamp string' could not be parsed at index 0 +error: evaluation error at test_location:31: Text 'bad timestamp string' could not be parsed at index 0 error_code: BAD_FORMAT Source: x From 5667bd825ed42bacaee4193c42f58b71786cc435 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 17 Apr 2026 13:04:41 -0700 Subject: [PATCH 050/204] Implement mutable map support for planner PiperOrigin-RevId: 901433988 --- .../java/dev/cel/common/values/BUILD.bazel | 32 ++++ .../cel/common/values/MutableMapValue.java | 146 ++++++++++++++++++ common/values/BUILD.bazel | 12 ++ .../main/java/dev/cel/extensions/BUILD.bazel | 1 + .../CelComprehensionsExtensions.java | 45 +++--- .../test/java/dev/cel/extensions/BUILD.bazel | 1 + .../CelComprehensionsExtensionsTest.java | 11 ++ .../java/dev/cel/runtime/planner/BUILD.bazel | 1 + .../dev/cel/runtime/planner/EvalFold.java | 15 +- 9 files changed, 241 insertions(+), 23 deletions(-) create mode 100644 common/src/main/java/dev/cel/common/values/MutableMapValue.java diff --git a/common/src/main/java/dev/cel/common/values/BUILD.bazel b/common/src/main/java/dev/cel/common/values/BUILD.bazel index 53ffdda3d..0d1d5431f 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -118,6 +118,38 @@ java_library( ], ) +java_library( + name = "mutable_map_value", + srcs = ["MutableMapValue.java"], + tags = [ + ], + deps = [ + "//common/annotations", + "//common/exceptions:attribute_not_found", + "//common/types", + "//common/types:type_providers", + "//common/values", + "//common/values:cel_value", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "mutable_map_value_android", + srcs = ["MutableMapValue.java"], + tags = [ + ], + deps = [ + ":cel_value_android", + "//common/annotations", + "//common/exceptions:attribute_not_found", + "//common/types:type_providers_android", + "//common/types:types_android", + "//common/values:values_android", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + cel_android_library( name = "values_android", srcs = CEL_VALUES_SOURCES, diff --git a/common/src/main/java/dev/cel/common/values/MutableMapValue.java b/common/src/main/java/dev/cel/common/values/MutableMapValue.java new file mode 100644 index 000000000..706436b2e --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/MutableMapValue.java @@ -0,0 +1,146 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.values; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.annotations.Internal; +import dev.cel.common.exceptions.CelAttributeNotFoundException; +import dev.cel.common.types.CelType; +import dev.cel.common.types.MapType; +import dev.cel.common.types.SimpleType; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * A custom CelValue implementation that allows O(1) insertions for maps during comprehension. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +@Immutable +@SuppressWarnings("Immutable") // Intentionally mutable for performance reasons +public final class MutableMapValue extends CelValue + implements SelectableValue, Map { + private final Map internalMap; + private final CelType celType; + + public static MutableMapValue create(Map map) { + return new MutableMapValue(map); + } + + @Override + public int size() { + return internalMap.size(); + } + + @Override + public boolean isEmpty() { + return internalMap.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return internalMap.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return internalMap.containsValue(value); + } + + @Override + public Object get(Object key) { + return internalMap.get(key); + } + + @Override + public Object put(Object key, Object value) { + return internalMap.put(key, value); + } + + @Override + public Object remove(Object key) { + return internalMap.remove(key); + } + + @Override + public void putAll(Map m) { + internalMap.putAll(m); + } + + @Override + public void clear() { + internalMap.clear(); + } + + @Override + public Set keySet() { + return internalMap.keySet(); + } + + @Override + public Collection values() { + return internalMap.values(); + } + + @Override + public Set> entrySet() { + return internalMap.entrySet(); + } + + @Override + public Object select(Object field) { + Object val = internalMap.get(field); + if (val != null) { + return val; + } + if (!internalMap.containsKey(field)) { + throw CelAttributeNotFoundException.forMissingMapKey(field.toString()); + } + throw CelAttributeNotFoundException.of( + String.format("Map value cannot be null for key: %s", field)); + } + + @Override + public Optional find(Object field) { + if (internalMap.containsKey(field)) { + return Optional.ofNullable(internalMap.get(field)); + } + return Optional.empty(); + } + + @Override + public Object value() { + return this; + } + + @Override + public boolean isZeroValue() { + return internalMap.isEmpty(); + } + + @Override + public CelType celType() { + return celType; + } + + private MutableMapValue(Map map) { + this.internalMap = new LinkedHashMap<>(map); + this.celType = MapType.create(SimpleType.DYN, SimpleType.DYN); + } +} diff --git a/common/values/BUILD.bazel b/common/values/BUILD.bazel index f1fa107b6..74bfa9e0f 100644 --- a/common/values/BUILD.bazel +++ b/common/values/BUILD.bazel @@ -47,6 +47,18 @@ cel_android_library( exports = ["//common/src/main/java/dev/cel/common/values:values_android"], ) +java_library( + name = "mutable_map_value", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/values:mutable_map_value"], +) + +cel_android_library( + name = "mutable_map_value_android", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/values:mutable_map_value_android"], +) + java_library( name = "base_proto_cel_value_converter", exports = ["//common/src/main/java/dev/cel/common/values:base_proto_cel_value_converter"], diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel index 77663f2fa..2eb26846f 100644 --- a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel @@ -307,6 +307,7 @@ java_library( "//common:options", "//common/ast", "//common/types", + "//common/values:mutable_map_value", "//compiler:compiler_builder", "//extensions:extension_library", "//parser:macro", diff --git a/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java index 7c298a773..3bf47c4a6 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java @@ -29,6 +29,7 @@ import dev.cel.common.ast.CelExpr; import dev.cel.common.types.MapType; import dev.cel.common.types.TypeParamType; +import dev.cel.common.values.MutableMapValue; import dev.cel.compiler.CelCompilerLibrary; import dev.cel.parser.CelMacro; import dev.cel.parser.CelMacroExprFactory; @@ -171,38 +172,46 @@ public void setParserOptions(CelParserBuilder parserBuilder) { parserBuilder.addMacros(macros()); } - // TODO: Implement a more efficient map insertion based on mutability once mutable - // maps are supported in Java stack. - private static ImmutableMap mapInsertMap( + private static Map mapInsertMap( Map targetMap, Map mapToMerge, RuntimeEquality equality) { - ImmutableMap.Builder resultBuilder = - ImmutableMap.builderWithExpectedSize(targetMap.size() + mapToMerge.size()); - - for (Map.Entry entry : mapToMerge.entrySet()) { - if (equality.findInMap(targetMap, entry.getKey()).isPresent()) { + for (Object key : mapToMerge.keySet()) { + if (equality.findInMap(targetMap, key).isPresent()) { throw new IllegalArgumentException( - String.format("insert failed: key '%s' already exists", entry.getKey())); - } else { - resultBuilder.put(entry.getKey(), entry.getValue()); + String.format("insert failed: key '%s' already exists", key)); } } - return resultBuilder.putAll(targetMap).buildOrThrow(); + + if (targetMap instanceof MutableMapValue) { + MutableMapValue wrapper = (MutableMapValue) targetMap; + wrapper.putAll(mapToMerge); + return wrapper; + } + + return ImmutableMap.builderWithExpectedSize(targetMap.size() + mapToMerge.size()) + .putAll(targetMap) + .putAll(mapToMerge) + .buildOrThrow(); } - private static ImmutableMap mapInsertKeyValue( - Object[] args, RuntimeEquality equality) { - Map map = (Map) args[0]; + private static Map mapInsertKeyValue(Object[] args, RuntimeEquality equality) { + Map mapArg = (Map) args[0]; Object key = args[1]; Object value = args[2]; - if (equality.findInMap(map, key).isPresent()) { + if (equality.findInMap(mapArg, key).isPresent()) { throw new IllegalArgumentException( String.format("insert failed: key '%s' already exists", key)); } + if (mapArg instanceof MutableMapValue) { + MutableMapValue mutableMap = (MutableMapValue) mapArg; + mutableMap.put(key, value); + return mutableMap; + } + ImmutableMap.Builder builder = - ImmutableMap.builderWithExpectedSize(map.size() + 1); - return builder.put(key, value).putAll(map).buildOrThrow(); + ImmutableMap.builderWithExpectedSize(mapArg.size() + 1); + return builder.put(key, value).putAll(mapArg).buildOrThrow(); } private static Optional expandAllMacro( diff --git a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel index 0b6502410..19fd3657e 100644 --- a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel @@ -15,6 +15,7 @@ java_library( "//common:compiler_common", "//common:container", "//common:options", + "//common/exceptions:attribute_not_found", "//common/exceptions:divide_by_zero", "//common/exceptions:index_out_of_bounds", "//common/types", diff --git a/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java index fbe160cd3..374178540 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java @@ -28,6 +28,7 @@ import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; import dev.cel.common.CelValidationException; +import dev.cel.common.exceptions.CelAttributeNotFoundException; import dev.cel.common.exceptions.CelDivideByZeroException; import dev.cel.common.exceptions.CelIndexOutOfBoundsException; import dev.cel.common.types.SimpleType; @@ -222,6 +223,7 @@ public void transformMapEntryMacro_twoVarComprehension_success( + " 'key2': 'value2'}", // map.transformMapEntry() "{'hello': 'world', 'greetings': 'tacocat'}.transformMapEntry(k, v, {}) == {}", + "{'a': 1, 'b': 2}.transformMapEntry(k, v, {k: v}) == {'a': 1, 'b': 2}", "{'a': 1, 'b': 2}.transformMapEntry(k, v, {k + '_new': v * 2}) == {'a_new': 2," + " 'b_new': 4}", "{'a': 1, 'b': 2, 'c': 3}.transformMapEntry(k, v, v % 2 == 1, {k: v * 10}) == {'a': 10," @@ -364,6 +366,15 @@ public void twoVarComprehension_outOfBounds_runtimeError() throws Exception { assertThat(e).hasCauseThat().hasMessageThat().contains("Index out of bounds: 1"); } + @Test + public void mutableMapValue_select_missingKeyException() throws Exception { + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, () -> eval("cel.bind(my_map, {'a': 1}, my_map.b)")); + assertThat(e).hasCauseThat().isInstanceOf(CelAttributeNotFoundException.class); + assertThat(e).hasCauseThat().hasMessageThat().contains("key 'b' is not present in map."); + } + private Object eval(String expression) throws Exception { return eval(this.cel, expression, ImmutableMap.of()); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index 824c918d8..96382b9a9 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -365,6 +365,7 @@ java_library( ":activation_wrapper", ":planned_interpretable", "//common/exceptions:runtime_exception", + "//common/values:mutable_map_value", "//runtime:accumulated_unknowns", "//runtime:concatenated_list_view", "//runtime:evaluation_exception", diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java index 2eb30671e..090a8bfae 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java @@ -15,8 +15,10 @@ package dev.cel.runtime.planner; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.errorprone.annotations.Immutable; import dev.cel.common.exceptions.CelRuntimeException; +import dev.cel.common.values.MutableMapValue; import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.ConcatenatedListView; @@ -131,7 +133,7 @@ private Object evalList(Collection iterRange, Folder folder, ExecutionFrame f boolean cond = (boolean) condition.eval(folder, frame); if (!cond) { folder.computeResult = true; - return result.eval(folder, frame); + return maybeUnwrapAccumulator(result.eval(folder, frame)); } folder.accuVal = loopStep.eval(folder, frame); @@ -139,14 +141,16 @@ private Object evalList(Collection iterRange, Folder folder, ExecutionFrame f index++; } folder.computeResult = true; - return result.eval(folder, frame); + return maybeUnwrapAccumulator(result.eval(folder, frame)); } private static Object maybeWrapAccumulator(Object val) { if (val instanceof Collection) { return new ConcatenatedListView<>((Collection) val); } - // TODO: Introduce mutable map support (for comp v2) + if (val instanceof Map) { + return MutableMapValue.create((Map) val); + } return val; } @@ -154,8 +158,9 @@ private static Object maybeUnwrapAccumulator(Object val) { if (val instanceof ConcatenatedListView) { return ImmutableList.copyOf((ConcatenatedListView) val); } - - // TODO: Introduce mutable map support (for comp v2) + if (val instanceof MutableMapValue) { + return ImmutableMap.copyOf((MutableMapValue) val); + } return val; } From 64bd51af154a5187d581e741958183cad4fe79b0 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 17 Apr 2026 16:07:22 -0700 Subject: [PATCH 051/204] Change string.split to return an immutable list. Add parsed-only evaluation coverage to CelStringExtensions PiperOrigin-RevId: 901514168 --- .../cel/extensions/CelStringExtensions.java | 28 +- .../main/java/dev/cel/extensions/README.md | 4 +- .../CelComprehensionsExtensionsTest.java | 5 +- .../extensions/CelListsExtensionsTest.java | 7 +- .../extensions/CelStringExtensionsTest.java | 509 ++++++++---------- 5 files changed, 238 insertions(+), 315 deletions(-) diff --git a/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java index 37c8270cc..2bb477b82 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java @@ -23,7 +23,6 @@ import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Lists; import com.google.errorprone.annotations.Immutable; import dev.cel.checker.CelCheckerBuilder; import dev.cel.common.CelFunctionDecl; @@ -37,7 +36,6 @@ import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelRuntimeBuilder; import dev.cel.runtime.CelRuntimeLibrary; -import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -475,7 +473,7 @@ private static String quote(String s) { sb.append('"'); for (int i = 0; i < s.length(); ) { int codePoint = s.codePointAt(i); - if (isMalformedUtf16(s, i, codePoint)) { + if (isMalformedUtf16(s, i)) { sb.append('\uFFFD'); i++; continue; @@ -518,7 +516,7 @@ private static String quote(String s) { return sb.toString(); } - private static boolean isMalformedUtf16(String s, int index, int codePoint) { + private static boolean isMalformedUtf16(String s, int index) { char currentChar = s.charAt(index); if (Character.isLowSurrogate(currentChar)) { return true; @@ -587,14 +585,14 @@ private static String reverse(String s) { return new StringBuilder(s).reverse().toString(); } - private static List split(String str, String separator) { + private static ImmutableList split(String str, String separator) { return split(str, separator, Integer.MAX_VALUE); } /** * @param args Object array with indices of: [0: string], [1: separator], [2: limit] */ - private static List split(Object[] args) throws CelEvaluationException { + private static ImmutableList split(Object[] args) throws CelEvaluationException { long limitInLong = (Long) args[2]; int limit; try { @@ -609,16 +607,14 @@ private static List split(Object[] args) throws CelEvaluationException { return split((String) args[0], (String) args[1], limit); } - /** Returns a **mutable** list of strings split on the separator */ - private static List split(String str, String separator, int limit) { + /** Returns an immutable list of strings split on the separator */ + private static ImmutableList split(String str, String separator, int limit) { if (limit == 0) { - return new ArrayList<>(); + return ImmutableList.of(); } if (limit == 1) { - List singleElementList = new ArrayList<>(); - singleElementList.add(str); - return singleElementList; + return ImmutableList.of(str); } if (limit < 0) { @@ -630,7 +626,7 @@ private static List split(String str, String separator, int limit) { } Iterable splitString = Splitter.on(separator).limit(limit).split(str); - return Lists.newArrayList(splitString); + return ImmutableList.copyOf(splitString); } /** @@ -643,8 +639,8 @@ private static List split(String str, String separator, int limit) { *

This exists because neither the built-in String.split nor Guava's splitter is able to deal * with separating single printable characters. */ - private static List explode(String str, int limit) { - List exploded = new ArrayList<>(); + private static ImmutableList explode(String str, int limit) { + ImmutableList.Builder exploded = ImmutableList.builder(); CelCodePointArray codePointArray = CelCodePointArray.fromString(str); if (limit > 0) { limit -= 1; @@ -656,7 +652,7 @@ private static List explode(String str, int limit) { if (codePointArray.length() > limit) { exploded.add(codePointArray.slice(limit, codePointArray.length()).toString()); } - return exploded; + return exploded.build(); } private static Object substring(String s, long i) throws CelEvaluationException { diff --git a/extensions/src/main/java/dev/cel/extensions/README.md b/extensions/src/main/java/dev/cel/extensions/README.md index c3fbf8c54..b1d3611b4 100644 --- a/extensions/src/main/java/dev/cel/extensions/README.md +++ b/extensions/src/main/java/dev/cel/extensions/README.md @@ -522,7 +522,7 @@ Examples: ### Split -Returns a mutable list of strings split from the input by the given separator. The +Returns a list of strings split from the input by the given separator. The function accepts an optional argument specifying a limit on the number of substrings produced by the split. @@ -1069,4 +1069,4 @@ Examples: {valueVar: indexVar}) // returns {1:0, 2:1, 3:2} {'greeting': 'aloha', 'farewell': 'aloha'} - .transformMapEntry(k, v, {v: k}) // error, duplicate key \ No newline at end of file + .transformMapEntry(k, v, {v: k}) // error, duplicate key diff --git a/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java index 374178540..207178cfe 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java @@ -28,6 +28,7 @@ import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; import dev.cel.common.exceptions.CelAttributeNotFoundException; import dev.cel.common.exceptions.CelDivideByZeroException; import dev.cel.common.exceptions.CelIndexOutOfBoundsException; @@ -312,8 +313,8 @@ public void unparseAST_twoVarComprehension( + " err: 'no matching overload'}") public void twoVarComprehension_compilerErrors(String expr, String err) throws Exception { Assume.assumeFalse(isParseOnly); - CelValidationException e = - assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); + CelValidationResult result = cel.compile(expr); + CelValidationException e = assertThrows(CelValidationException.class, () -> result.getAst()); assertThat(e).hasMessageThat().contains(err); } diff --git a/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java index b36e0e92e..f36d90e2d 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java @@ -27,6 +27,7 @@ import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; import dev.cel.common.types.SimpleType; import dev.cel.expr.conformance.test.SimpleTest; import dev.cel.parser.CelStandardMacro; @@ -300,10 +301,8 @@ public void sortBy_success(String expression, String expected) throws Exception + "expectedError: 'variable name must be a simple identifier'}") public void sortBy_throws_validationException(String expression, String expectedError) throws Exception { - assertThat( - assertThrows( - CelValidationException.class, - () -> cel.createProgram(cel.compile(expression).getAst()).eval())) + CelValidationResult result = cel.compile(expression); + assertThat(assertThrows(CelValidationException.class, () -> result.getAst())) .hasMessageThat() .contains(expectedError); } diff --git a/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java index 3624e0902..e7542b7b7 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java @@ -18,43 +18,56 @@ import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; +import dev.cel.bundle.Cel; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; import dev.cel.common.types.SimpleType; import dev.cel.compiler.CelCompiler; import dev.cel.compiler.CelCompilerFactory; import dev.cel.extensions.CelStringExtensions.Function; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelRuntime; -import dev.cel.runtime.CelRuntimeFactory; +import dev.cel.testing.CelRuntimeFlavor; import java.util.List; +import java.util.Map; +import org.junit.Assume; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) public final class CelStringExtensionsTest { - private static final CelCompiler COMPILER = - CelCompilerFactory.standardCelCompilerBuilder() - .addLibraries(CelExtensions.strings()) - .addVar("s", SimpleType.STRING) - .addVar("separator", SimpleType.STRING) - .addVar("index", SimpleType.INT) - .addVar("offset", SimpleType.INT) - .addVar("indexOfParam", SimpleType.STRING) - .addVar("beginIndex", SimpleType.INT) - .addVar("endIndex", SimpleType.INT) - .addVar("limit", SimpleType.INT) - .build(); - - private static final CelRuntime RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(CelExtensions.strings()).build(); + @TestParameter public CelRuntimeFlavor runtimeFlavor; + @TestParameter public boolean isParseOnly; + + private Cel cel; + + @Before + public void setUp() { + // Legacy runtime does not support parsed-only evaluation mode. + Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.LEGACY) && isParseOnly); + this.cel = + runtimeFlavor + .builder() + .addCompilerLibraries(CelExtensions.strings()) + .addRuntimeLibraries(CelExtensions.strings()) + .addVar("s", SimpleType.STRING) + .addVar("separator", SimpleType.STRING) + .addVar("index", SimpleType.INT) + .addVar("offset", SimpleType.INT) + .addVar("indexOfParam", SimpleType.STRING) + .addVar("beginIndex", SimpleType.INT) + .addVar("endIndex", SimpleType.INT) + .addVar("limit", SimpleType.INT) + .build(); + } @Test public void library() { @@ -92,10 +105,8 @@ public void library() { @TestParameters("{string: '😁😑😦', beginIndex: 3, expectedResult: ''}") public void substring_beginIndex_success(String string, int beginIndex, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.substring(beginIndex)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(ImmutableMap.of("s", string, "beginIndex", beginIndex)); + Object evaluatedResult = + eval("s.substring(beginIndex)", ImmutableMap.of("s", string, "beginIndex", beginIndex)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -108,10 +119,7 @@ public void substring_beginIndex_success(String string, int beginIndex, String e @TestParameters( "{string: 'A!@#$%^&*()-_+=?/<>.,;:''\"\\', expectedResult: 'a!@#$%^&*()-_+=?/<>.,;:''\"\\'}") public void lowerAscii_success(String string, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.lowerAscii()").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(ImmutableMap.of("s", string)); + Object evaluatedResult = eval("s.lowerAscii()", ImmutableMap.of("s", string)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -127,10 +135,7 @@ public void lowerAscii_success(String string, String expectedResult) throws Exce @TestParameters("{string: 'A😁B 😑C가😦D', expectedResult: 'a😁b 😑c가😦d'}") public void lowerAscii_outsideAscii_success(String string, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.lowerAscii()").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(ImmutableMap.of("s", string)); + Object evaluatedResult = eval("s.lowerAscii()", ImmutableMap.of("s", string)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -161,10 +166,8 @@ public void lowerAscii_outsideAscii_success(String string, String expectedResult + " ['The quick brown ', ' jumps over the lazy dog']}") public void split_ascii_success(String string, String separator, List expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.split(separator)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(ImmutableMap.of("s", string, "separator", separator)); + Object evaluatedResult = + eval("s.split(separator)", ImmutableMap.of("s", string, "separator", separator)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -182,34 +185,30 @@ public void split_ascii_success(String string, String separator, List ex @TestParameters("{string: '😁a😦나😑 😦', separator: '😁a😦나😑 😦', expectedResult: ['','']}") public void split_unicode_success(String string, String separator, List expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.split(separator)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(ImmutableMap.of("s", string, "separator", separator)); + Object evaluatedResult = + eval("s.split(separator)", ImmutableMap.of("s", string, "separator", separator)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @Test @SuppressWarnings("unchecked") // Test only, need List cast to test mutability - public void split_collectionIsMutable() throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("'test'.split('')").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); + public void split_collectionIsImmutable() throws Exception { + CelAbstractSyntaxTree ast = cel.compile("'test'.split('')").getAst(); + CelRuntime.Program program = cel.createProgram(ast); List evaluatedResult = (List) program.eval(); - evaluatedResult.add("a"); - evaluatedResult.add("b"); - evaluatedResult.add("c"); - evaluatedResult.remove("c"); - assertThat(evaluatedResult).containsExactly("t", "e", "s", "t", "a", "b").inOrder(); + assertThrows(UnsupportedOperationException.class, () -> evaluatedResult.add("a")); } @Test public void split_separatorIsNonString_throwsException() { + // This is a type-check failure. + Assume.assumeFalse(isParseOnly); + CelValidationResult result = cel.compile("'12'.split(2)"); CelValidationException exception = - assertThrows( - CelValidationException.class, () -> COMPILER.compile("'12'.split(2)").getAst()); + assertThrows(CelValidationException.class, () -> result.getAst()); assertThat(exception).hasMessageThat().contains("found no matching overload for 'split'"); } @@ -295,11 +294,10 @@ public void split_separatorIsNonString_throwsException() { + " expectedResult: ['The quick brown ', ' jumps over the lazy dog']}") public void split_asciiWithLimit_success( String string, String separator, int limit, List expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.split(separator, limit)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - Object evaluatedResult = - program.eval(ImmutableMap.of("s", string, "separator", separator, "limit", limit)); + eval( + "s.split(separator, limit)", + ImmutableMap.of("s", string, "separator", separator, "limit", limit)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -351,11 +349,10 @@ public void split_asciiWithLimit_success( "{string: '😁a😦나😑 😦', separator: '😁a😦나😑 😦', limit: -1, expectedResult: ['','']}") public void split_unicodeWithLimit_success( String string, String separator, int limit, List expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.split(separator, limit)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - Object evaluatedResult = - program.eval(ImmutableMap.of("s", string, "separator", separator, "limit", limit)); + eval( + "s.split(separator, limit)", + ImmutableMap.of("s", string, "separator", separator, "limit", limit)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -368,35 +365,36 @@ public void split_unicodeWithLimit_success( @TestParameters("{separator: 'te', limit: 1}") @TestParameters("{separator: 'te', limit: 2}") @SuppressWarnings("unchecked") // Test only, need List cast to test mutability - public void split_withLimit_collectionIsMutable(String separator, int limit) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("'test'.split(separator, limit)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - + public void split_withLimit_collectionIsImmutable(String separator, int limit) throws Exception { List evaluatedResult = - (List) program.eval(ImmutableMap.of("separator", separator, "limit", limit)); - evaluatedResult.add("a"); + (List) + eval( + "'test'.split(separator, limit)", + ImmutableMap.of("separator", separator, "limit", limit)); - assertThat(Iterables.getLast(evaluatedResult)).isEqualTo("a"); + assertThrows(UnsupportedOperationException.class, () -> evaluatedResult.add("a")); } @Test public void split_withLimit_separatorIsNonString_throwsException() { + // This is a type-check failure. + Assume.assumeFalse(isParseOnly); + CelValidationResult result = cel.compile("'12'.split(2, 3)"); CelValidationException exception = - assertThrows( - CelValidationException.class, () -> COMPILER.compile("'12'.split(2, 3)").getAst()); + assertThrows(CelValidationException.class, () -> result.getAst()); assertThat(exception).hasMessageThat().contains("found no matching overload for 'split'"); } @Test public void split_withLimitOverflow_throwsException() throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("'test'.split('', limit)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - CelEvaluationException exception = assertThrows( CelEvaluationException.class, - () -> program.eval(ImmutableMap.of("limit", 2147483648L))); // INT_MAX + 1 + () -> + eval( + "'test'.split('', limit)", + ImmutableMap.of("limit", 2147483648L))); // INT_MAX + 1 assertThat(exception) .hasMessageThat() @@ -416,11 +414,10 @@ public void split_withLimitOverflow_throwsException() throws Exception { @TestParameters("{string: '', beginIndex: 0, endIndex: 0, expectedResult: ''}") public void substring_beginAndEndIndex_ascii_success( String string, int beginIndex, int endIndex, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.substring(beginIndex, endIndex)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - Object evaluatedResult = - program.eval(ImmutableMap.of("s", string, "beginIndex", beginIndex, "endIndex", endIndex)); + eval( + "s.substring(beginIndex, endIndex)", + ImmutableMap.of("s", string, "beginIndex", beginIndex, "endIndex", endIndex)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -444,11 +441,10 @@ public void substring_beginAndEndIndex_ascii_success( @TestParameters("{string: 'a😁나', beginIndex: 3, endIndex: 3, expectedResult: ''}") public void substring_beginAndEndIndex_unicode_success( String string, int beginIndex, int endIndex, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.substring(beginIndex, endIndex)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - Object evaluatedResult = - program.eval(ImmutableMap.of("s", string, "beginIndex", beginIndex, "endIndex", endIndex)); + eval( + "s.substring(beginIndex, endIndex)", + ImmutableMap.of("s", string, "beginIndex", beginIndex, "endIndex", endIndex)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -458,13 +454,13 @@ public void substring_beginAndEndIndex_unicode_success( @TestParameters("{string: '', beginIndex: 2}") public void substring_beginIndexOutOfRange_ascii_throwsException(String string, int beginIndex) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.substring(beginIndex)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - CelEvaluationException exception = assertThrows( CelEvaluationException.class, - () -> program.eval(ImmutableMap.of("s", string, "beginIndex", beginIndex))); + () -> + eval( + "s.substring(beginIndex)", + ImmutableMap.of("s", string, "beginIndex", beginIndex))); String exceptionMessage = String.format( @@ -482,13 +478,13 @@ public void substring_beginIndexOutOfRange_ascii_throwsException(String string, @TestParameters("{string: '😁가나', beginIndex: 4, uniqueCharCount: 3}") public void substring_beginIndexOutOfRange_unicode_throwsException( String string, int beginIndex, int uniqueCharCount) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.substring(beginIndex)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - CelEvaluationException exception = assertThrows( CelEvaluationException.class, - () -> program.eval(ImmutableMap.of("s", string, "beginIndex", beginIndex))); + () -> + eval( + "s.substring(beginIndex)", + ImmutableMap.of("s", string, "beginIndex", beginIndex))); String exceptionMessage = String.format( @@ -505,14 +501,12 @@ public void substring_beginIndexOutOfRange_unicode_throwsException( @TestParameters("{string: '😁😑😦', beginIndex: 2, endIndex: 1}") public void substring_beginAndEndIndexOutOfRange_throwsException( String string, int beginIndex, int endIndex) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.substring(beginIndex, endIndex)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - CelEvaluationException exception = assertThrows( CelEvaluationException.class, () -> - program.eval( + eval( + "s.substring(beginIndex, endIndex)", ImmutableMap.of("s", string, "beginIndex", beginIndex, "endIndex", endIndex))); String exceptionMessage = @@ -522,13 +516,13 @@ public void substring_beginAndEndIndexOutOfRange_throwsException( @Test public void substring_beginIndexOverflow_throwsException() throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("'abcd'.substring(beginIndex)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - CelEvaluationException exception = assertThrows( CelEvaluationException.class, - () -> program.eval(ImmutableMap.of("beginIndex", 2147483648L))); // INT_MAX + 1 + () -> + eval( + "'abcd'.substring(beginIndex)", + ImmutableMap.of("beginIndex", 2147483648L))); // INT_MAX + 1 assertThat(exception) .hasMessageThat() @@ -540,13 +534,13 @@ public void substring_beginIndexOverflow_throwsException() throws Exception { @TestParameters("{beginIndex: 2147483648, endIndex: 2147483648}") public void substring_beginOrEndIndexOverflow_throwsException(long beginIndex, long endIndex) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("'abcd'.substring(beginIndex, endIndex)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - CelEvaluationException exception = assertThrows( CelEvaluationException.class, - () -> program.eval(ImmutableMap.of("beginIndex", beginIndex, "endIndex", endIndex))); + () -> + eval( + "'abcd'.substring(beginIndex, endIndex)", + ImmutableMap.of("beginIndex", beginIndex, "endIndex", endIndex))); assertThat(exception) .hasMessageThat() @@ -563,10 +557,7 @@ public void substring_beginOrEndIndexOverflow_throwsException(long beginIndex, l @TestParameters("{string: 'world', index: 5, expectedResult: ''}") public void charAt_ascii_success(String string, long index, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.charAt(index)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(ImmutableMap.of("s", string, "index", index)); + Object evaluatedResult = eval("s.charAt(index)", ImmutableMap.of("s", string, "index", index)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -588,10 +579,7 @@ public void charAt_ascii_success(String string, long index, String expectedResul @TestParameters("{string: 'a😁나', index: 3, expectedResult: ''}") public void charAt_unicode_success(String string, long index, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.charAt(index)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(ImmutableMap.of("s", string, "index", index)); + Object evaluatedResult = eval("s.charAt(index)", ImmutableMap.of("s", string, "index", index)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -602,26 +590,21 @@ public void charAt_unicode_success(String string, long index, String expectedRes @TestParameters("{string: '😁😑😦', index: -1}") @TestParameters("{string: '😁😑😦', index: 4}") public void charAt_outOfBounds_throwsException(String string, long index) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.charAt(index)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - CelEvaluationException exception = assertThrows( CelEvaluationException.class, - () -> program.eval(ImmutableMap.of("s", string, "index", index))); + () -> eval("s.charAt(index)", ImmutableMap.of("s", string, "index", index))); assertThat(exception).hasMessageThat().contains("charAt failure: Index out of range"); } @Test public void charAt_indexOverflow_throwsException() throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("'test'.charAt(index)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - CelEvaluationException exception = assertThrows( CelEvaluationException.class, - () -> program.eval(ImmutableMap.of("index", 2147483648L))); // INT_MAX + 1 + () -> + eval("'test'.charAt(index)", ImmutableMap.of("index", 2147483648L))); // INT_MAX + 1 assertThat(exception) .hasMessageThat() @@ -650,10 +633,8 @@ public void charAt_indexOverflow_throwsException() throws Exception { @TestParameters("{string: 'hello mellow', indexOf: ' ', expectedResult: -1}") public void indexOf_ascii_success(String string, String indexOf, int expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.indexOf(indexOfParam)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(ImmutableMap.of("s", string, "indexOfParam", indexOf)); + Object evaluatedResult = + eval("s.indexOf(indexOfParam)", ImmutableMap.of("s", string, "indexOfParam", indexOf)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -682,10 +663,8 @@ public void indexOf_ascii_success(String string, String indexOf, int expectedRes @TestParameters("{string: 'a😁😑 나😦😁😑다', indexOf: 'a😁😑 나😦😁😑다😁', expectedResult: -1}") public void indexOf_unicode_success(String string, String indexOf, int expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.indexOf(indexOfParam)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(ImmutableMap.of("s", string, "indexOfParam", indexOf)); + Object evaluatedResult = + eval("s.indexOf(indexOfParam)", ImmutableMap.of("s", string, "indexOfParam", indexOf)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -697,13 +676,10 @@ public void indexOf_unicode_success(String string, String indexOf, int expectedR @TestParameters("{indexOf: '나'}") @TestParameters("{indexOf: '😁'}") public void indexOf_onEmptyString_throwsException(String indexOf) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("''.indexOf(indexOfParam)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - CelEvaluationException exception = assertThrows( CelEvaluationException.class, - () -> program.eval(ImmutableMap.of("indexOfParam", indexOf))); + () -> eval("''.indexOf(indexOfParam)", ImmutableMap.of("indexOfParam", indexOf))); assertThat(exception).hasMessageThat().contains("indexOf failure: Offset out of range"); } @@ -728,11 +704,10 @@ public void indexOf_onEmptyString_throwsException(String indexOf) throws Excepti @TestParameters("{string: 'hello mellow', indexOf: 'l', offset: 10, expectedResult: -1}") public void indexOf_asciiWithOffset_success( String string, String indexOf, int offset, int expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.indexOf(indexOfParam, offset)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - Object evaluatedResult = - program.eval(ImmutableMap.of("s", string, "indexOfParam", indexOf, "offset", offset)); + eval( + "s.indexOf(indexOfParam, offset)", + ImmutableMap.of("s", string, "indexOfParam", indexOf, "offset", offset)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -779,11 +754,10 @@ public void indexOf_asciiWithOffset_success( "{string: 'a😁😑 나😦😁😑다', indexOf: 'a😁😑 나😦😁😑다😁', offset: 0, expectedResult: -1}") public void indexOf_unicodeWithOffset_success( String string, String indexOf, int offset, int expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.indexOf(indexOfParam, offset)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - Object evaluatedResult = - program.eval(ImmutableMap.of("s", string, "indexOfParam", indexOf, "offset", offset)); + eval( + "s.indexOf(indexOfParam, offset)", + ImmutableMap.of("s", string, "indexOfParam", indexOf, "offset", offset)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -797,14 +771,12 @@ public void indexOf_unicodeWithOffset_success( @TestParameters("{string: '😁😑 😦', indexOf: '😦', offset: 4}") public void indexOf_withOffsetOutOfBounds_throwsException( String string, String indexOf, int offset) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.indexOf(indexOfParam, offset)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - CelEvaluationException exception = assertThrows( CelEvaluationException.class, () -> - program.eval( + eval( + "s.indexOf(indexOfParam, offset)", ImmutableMap.of("s", string, "indexOfParam", indexOf, "offset", offset))); assertThat(exception).hasMessageThat().contains("indexOf failure: Offset out of range"); @@ -812,13 +784,13 @@ public void indexOf_withOffsetOutOfBounds_throwsException( @Test public void indexOf_offsetOverflow_throwsException() throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("'test'.indexOf('t', offset)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - CelEvaluationException exception = assertThrows( CelEvaluationException.class, - () -> program.eval(ImmutableMap.of("offset", 2147483648L))); // INT_MAX + 1 + () -> + eval( + "'test'.indexOf('t', offset)", + ImmutableMap.of("offset", 2147483648L))); // INT_MAX + 1 assertThat(exception) .hasMessageThat() @@ -835,10 +807,7 @@ public void indexOf_offsetOverflow_throwsException() throws Exception { @TestParameters("{list: '[''x'', '' '', '' y '', ''z '']', expectedResult: 'x y z '}") @TestParameters("{list: '[''hello '', ''world'']', expectedResult: 'hello world'}") public void join_ascii_success(String list, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(String.format("%s.join()", list)).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - String result = (String) program.eval(); + String result = (String) eval(String.format("%s.join()", list)); assertThat(result).isEqualTo(expectedResult); } @@ -847,10 +816,7 @@ public void join_ascii_success(String list, String expectedResult) throws Except @TestParameters("{list: '[''가'', ''😁'']', expectedResult: '가😁'}") @TestParameters("{list: '[''😁😦😑 😦'', ''나'']', expectedResult: '😁😦😑 😦나'}") public void join_unicode_success(String list, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(String.format("%s.join()", list)).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - String result = (String) program.eval(); + String result = (String) eval(String.format("%s.join()", list)); assertThat(result).isEqualTo(expectedResult); } @@ -874,11 +840,7 @@ public void join_unicode_success(String list, String expectedResult) throws Exce "{list: '[''hello '', ''world'']', separator: '/', expectedResult: 'hello /world'}") public void join_asciiWithSeparator_success(String list, String separator, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = - COMPILER.compile(String.format("%s.join('%s')", list, separator)).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - String result = (String) program.eval(); + String result = (String) eval(String.format("%s.join('%s')", list, separator)); assertThat(result).isEqualTo(expectedResult); } @@ -893,20 +855,17 @@ public void join_asciiWithSeparator_success(String list, String separator, Strin + " -😑-나'}") public void join_unicodeWithSeparator_success( String list, String separator, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = - COMPILER.compile(String.format("%s.join('%s')", list, separator)).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - String result = (String) program.eval(); + String result = (String) eval(String.format("%s.join('%s')", list, separator)); assertThat(result).isEqualTo(expectedResult); } @Test public void join_separatorIsNonString_throwsException() { + // This is a type-check failure. + Assume.assumeFalse(isParseOnly); CelValidationException exception = - assertThrows( - CelValidationException.class, () -> COMPILER.compile("['x','y'].join(2)").getAst()); + assertThrows(CelValidationException.class, () -> cel.compile("['x','y'].join(2)").getAst()); assertThat(exception).hasMessageThat().contains("found no matching overload for 'join'"); } @@ -935,11 +894,10 @@ public void join_separatorIsNonString_throwsException() { @TestParameters("{string: 'hello mellow', lastIndexOf: ' ', expectedResult: -1}") public void lastIndexOf_ascii_success(String string, String lastIndexOf, int expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.lastIndexOf(indexOfParam)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - Object evaluatedResult = - program.eval(ImmutableMap.of("s", string, "indexOfParam", lastIndexOf)); + eval( + "s.lastIndexOf(indexOfParam)", + ImmutableMap.of("s", string, "indexOfParam", lastIndexOf)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -969,11 +927,10 @@ public void lastIndexOf_ascii_success(String string, String lastIndexOf, int exp @TestParameters("{string: 'a😁😑 나😦😁😑다', lastIndexOf: 'a😁😑 나😦😁😑다😁', expectedResult: -1}") public void lastIndexOf_unicode_success(String string, String lastIndexOf, int expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.lastIndexOf(indexOfParam)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - Object evaluatedResult = - program.eval(ImmutableMap.of("s", string, "indexOfParam", lastIndexOf)); + eval( + "s.lastIndexOf(indexOfParam)", + ImmutableMap.of("s", string, "indexOfParam", lastIndexOf)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -987,10 +944,8 @@ public void lastIndexOf_unicode_success(String string, String lastIndexOf, int e @TestParameters("{lastIndexOf: '😁'}") public void lastIndexOf_strLengthLessThanSubstrLength_returnsMinusOne(String lastIndexOf) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("''.lastIndexOf(indexOfParam)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(ImmutableMap.of("s", "", "indexOfParam", lastIndexOf)); + Object evaluatedResult = + eval("''.lastIndexOf(indexOfParam)", ImmutableMap.of("s", "", "indexOfParam", lastIndexOf)); assertThat(evaluatedResult).isEqualTo(-1); } @@ -1022,11 +977,10 @@ public void lastIndexOf_strLengthLessThanSubstrLength_returnsMinusOne(String las "{string: 'hello mellow', lastIndexOf: 'hello mellowwww ', offset: 11, expectedResult: -1}") public void lastIndexOf_asciiWithOffset_success( String string, String lastIndexOf, int offset, int expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.lastIndexOf(indexOfParam, offset)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - Object evaluatedResult = - program.eval(ImmutableMap.of("s", string, "indexOfParam", lastIndexOf, "offset", offset)); + eval( + "s.lastIndexOf(indexOfParam, offset)", + ImmutableMap.of("s", string, "indexOfParam", lastIndexOf, "offset", offset)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -1097,11 +1051,10 @@ public void lastIndexOf_asciiWithOffset_success( "{string: 'a😁😑 나😦😁😑다', lastIndexOf: 'a😁😑 나😦😁😑다😁', offset: 8, expectedResult: -1}") public void lastIndexOf_unicodeWithOffset_success( String string, String lastIndexOf, int offset, int expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.lastIndexOf(indexOfParam, offset)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - Object evaluatedResult = - program.eval(ImmutableMap.of("s", string, "indexOfParam", lastIndexOf, "offset", offset)); + eval( + "s.lastIndexOf(indexOfParam, offset)", + ImmutableMap.of("s", string, "indexOfParam", lastIndexOf, "offset", offset)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -1115,14 +1068,12 @@ public void lastIndexOf_unicodeWithOffset_success( @TestParameters("{string: '😁😑 😦', lastIndexOf: '😦', offset: 4}") public void lastIndexOf_withOffsetOutOfBounds_throwsException( String string, String lastIndexOf, int offset) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.lastIndexOf(indexOfParam, offset)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - CelEvaluationException exception = assertThrows( CelEvaluationException.class, () -> - program.eval( + eval( + "s.lastIndexOf(indexOfParam, offset)", ImmutableMap.of("s", string, "indexOfParam", lastIndexOf, "offset", offset))); assertThat(exception).hasMessageThat().contains("lastIndexOf failure: Offset out of range"); @@ -1130,13 +1081,13 @@ public void lastIndexOf_withOffsetOutOfBounds_throwsException( @Test public void lastIndexOf_offsetOverflow_throwsException() throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("'test'.lastIndexOf('t', offset)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - CelEvaluationException exception = assertThrows( CelEvaluationException.class, - () -> program.eval(ImmutableMap.of("offset", 2147483648L))); // INT_MAX + 1 + () -> + eval( + "'test'.lastIndexOf('t', offset)", + ImmutableMap.of("offset", 2147483648L))); // INT_MAX + 1 assertThat(exception) .hasMessageThat() @@ -1163,13 +1114,8 @@ public void lastIndexOf_offsetOverflow_throwsException() throws Exception { public void replace_ascii_success( String string, String searchString, String replacement, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = - COMPILER - .compile(String.format("'%s'.replace('%s', '%s')", string, searchString, replacement)) - .getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(); + Object evaluatedResult = + eval(String.format("'%s'.replace('%s', '%s')", string, searchString, replacement)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -1188,13 +1134,8 @@ public void replace_ascii_success( public void replace_unicode_success( String string, String searchString, String replacement, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = - COMPILER - .compile(String.format("'%s'.replace('%s', '%s')", string, searchString, replacement)) - .getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(); + Object evaluatedResult = + eval(String.format("'%s'.replace('%s', '%s')", string, searchString, replacement)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -1273,15 +1214,10 @@ public void replace_unicode_success( public void replace_ascii_withLimit_success( String string, String searchString, String replacement, int limit, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = - COMPILER - .compile( - String.format( - "'%s'.replace('%s', '%s', %d)", string, searchString, replacement, limit)) - .getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(); + Object evaluatedResult = + eval( + String.format( + "'%s'.replace('%s', '%s', %d)", string, searchString, replacement, limit)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -1334,28 +1270,23 @@ public void replace_ascii_withLimit_success( public void replace_unicode_withLimit_success( String string, String searchString, String replacement, int limit, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = - COMPILER - .compile( - String.format( - "'%s'.replace('%s', '%s', %d)", string, searchString, replacement, limit)) - .getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(); + Object evaluatedResult = + eval( + String.format( + "'%s'.replace('%s', '%s', %d)", string, searchString, replacement, limit)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @Test public void replace_limitOverflow_throwsException() throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("'test'.replace('','',index)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - CelEvaluationException exception = assertThrows( CelEvaluationException.class, - () -> program.eval(ImmutableMap.of("index", 2147483648L))); // INT_MAX + 1 + () -> + eval( + "'test'.replace('','',index)", + ImmutableMap.of("index", 2147483648L))); // INT_MAX + 1 assertThat(exception) .hasMessageThat() @@ -1406,10 +1337,7 @@ private enum TrimTestCase { @Test public void trim_success(@TestParameter TrimTestCase testCase) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.trim()").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(ImmutableMap.of("s", testCase.text)); + Object evaluatedResult = eval("s.trim()", ImmutableMap.of("s", testCase.text)); assertThat(evaluatedResult).isEqualTo(testCase.expectedResult); } @@ -1422,10 +1350,7 @@ public void trim_success(@TestParameter TrimTestCase testCase) throws Exception @TestParameters( "{string: 'a!@#$%^&*()-_+=?/<>.,;:''\"\\', expectedResult: 'A!@#$%^&*()-_+=?/<>.,;:''\"\\'}") public void upperAscii_success(String string, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.upperAscii()").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(ImmutableMap.of("s", string)); + Object evaluatedResult = eval("s.upperAscii()", ImmutableMap.of("s", string)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -1441,30 +1366,25 @@ public void upperAscii_success(String string, String expectedResult) throws Exce @TestParameters("{string: 'a😁b 😑c가😦d', expectedResult: 'A😁B 😑C가😦D'}") public void upperAscii_outsideAscii_success(String string, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.upperAscii()").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(ImmutableMap.of("s", string)); + Object evaluatedResult = eval("s.upperAscii()", ImmutableMap.of("s", string)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @Test public void stringExtension_functionSubset_success() throws Exception { - CelStringExtensions stringExtensions = - CelExtensions.strings(Function.CHAR_AT, Function.SUBSTRING); - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder().addLibraries(stringExtensions).build(); - CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(stringExtensions).build(); + Cel customCel = + runtimeFlavor + .builder() + .addCompilerLibraries(CelExtensions.strings(Function.CHAR_AT, Function.SUBSTRING)) + .addRuntimeLibraries(CelExtensions.strings(Function.CHAR_AT, Function.SUBSTRING)) + .build(); Object evaluatedResult = - celRuntime - .createProgram( - celCompiler - .compile("'test'.substring(2) == 'st' && 'hello'.charAt(1) == 'e'") - .getAst()) - .eval(); + eval( + customCel, + "'test'.substring(2) == 'st' && 'hello'.charAt(1) == 'e'", + ImmutableMap.of()); assertThat(evaluatedResult).isEqualTo(true); } @@ -1476,10 +1396,7 @@ public void stringExtension_functionSubset_success() throws Exception { @TestParameters("{string: 'hello world', expectedResult: 'dlrow olleh'}") @TestParameters("{string: 'ab가cd', expectedResult: 'dc가ba'}") public void reverse_success(String string, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.reverse()").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(ImmutableMap.of("s", string)); + Object evaluatedResult = eval("s.reverse()", ImmutableMap.of("s", string)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -1490,10 +1407,7 @@ public void reverse_success(String string, String expectedResult) throws Excepti "{string: '\u180e\u200b\u200c\u200d\u2060\ufeff', expectedResult:" + " '\ufeff\u2060\u200d\u200c\u200b\u180e'}") public void reverse_unicode(String string, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("s.reverse()").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(ImmutableMap.of("s", string)); + Object evaluatedResult = eval("s.reverse()", ImmutableMap.of("s", string)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -1502,14 +1416,14 @@ public void reverse_unicode(String string, String expectedResult) throws Excepti @TestParameters("{string: 'hello', expectedResult: '\"hello\"'}") @TestParameters("{string: '', expectedResult: '\"\"'}") @TestParameters( - "{string: 'contains \\\"quotes\\\"', expectedResult: '\"contains \\\\\\\"quotes\\\\\\\"\"'}") - @TestParameters("{string: 'ends with \\\\', expectedResult: '\"ends with \\\\\\\\\"'}") - @TestParameters("{string: '\\\\ starts with', expectedResult: '\"\\\\\\\\ starts with\"'}") + "{string: 'contains \\\\\\\"quotes\\\\\\\"', expectedResult: '\"contains" + + " \\\\\\\\\\\\\\\"quotes\\\\\\\\\\\\\\\"\"'}") + @TestParameters( + "{string: 'ends with \\\\\\\\', expectedResult: '\"ends with \\\\\\\\\\\\\\\\\"'}") + @TestParameters( + "{string: '\\\\\\\\ starts with', expectedResult: '\"\\\\\\\\\\\\\\\\ starts with\"'}") public void quote_success(String string, String expectedResult) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("strings.quote(s)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(ImmutableMap.of("s", string)); + Object evaluatedResult = eval("strings.quote(s)", ImmutableMap.of("s", string)); assertThat(evaluatedResult).isEqualTo(expectedResult); } @@ -1518,21 +1432,16 @@ public void quote_success(String string, String expectedResult) throws Exception public void quote_singleWithDoubleQuotes() throws Exception { String expr = "strings.quote('single-quote with \"double quote\"')"; String expected = "\"\\\"single-quote with \\\\\\\"double quote\\\\\\\"\\\"\""; - CelAbstractSyntaxTree ast = COMPILER.compile(expr + " == " + expected).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object evaluatedResult = program.eval(); + Object evaluatedResult = eval(expr + " == " + expected); assertThat(evaluatedResult).isEqualTo(true); } @Test public void quote_escapesSpecialCharacters() throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("strings.quote(s)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - Object evaluatedResult = - program.eval( + eval( + "strings.quote(s)", ImmutableMap.of("s", "\u0007bell\u000Bvtab\bback\ffeed\rret\nline\ttab\\slash 가 😁")); assertThat(evaluatedResult) @@ -1541,25 +1450,19 @@ public void quote_escapesSpecialCharacters() throws Exception { @Test public void quote_escapesMalformed_endWithHighSurrogate() throws Exception { - CelRuntime.Program program = - RUNTIME.createProgram(COMPILER.compile("strings.quote(s)").getAst()); - assertThat(program.eval(ImmutableMap.of("s", "end with high surrogate \uD83D"))) + assertThat(eval("strings.quote(s)", ImmutableMap.of("s", "end with high surrogate \uD83D"))) .isEqualTo("\"end with high surrogate \uFFFD\""); } @Test public void quote_escapesMalformed_unpairedHighSurrogate() throws Exception { - CelRuntime.Program program = - RUNTIME.createProgram(COMPILER.compile("strings.quote(s)").getAst()); - assertThat(program.eval(ImmutableMap.of("s", "bad pair \uD83DA"))) + assertThat(eval("strings.quote(s)", ImmutableMap.of("s", "bad pair \uD83DA"))) .isEqualTo("\"bad pair \uFFFDA\""); } @Test public void quote_escapesMalformed_unpairedLowSurrogate() throws Exception { - CelRuntime.Program program = - RUNTIME.createProgram(COMPILER.compile("strings.quote(s)").getAst()); - assertThat(program.eval(ImmutableMap.of("s", "bad pair \uDC00A"))) + assertThat(eval("strings.quote(s)", ImmutableMap.of("s", "bad pair \uDC00A"))) .isEqualTo("\"bad pair \uFFFDA\""); } @@ -1570,23 +1473,47 @@ public void stringExtension_compileUnallowedFunction_throws() { .addLibraries(CelExtensions.strings(Function.REPLACE)) .build(); - assertThrows( - CelValidationException.class, - () -> celCompiler.compile("'test'.substring(2) == 'st'").getAst()); + // This is a type-check failure. + Assume.assumeFalse(isParseOnly); + CelValidationResult result = celCompiler.compile("'test'.substring(2) == 'st'"); + assertThrows(CelValidationException.class, () -> result.getAst()); } @Test public void stringExtension_evaluateUnallowedFunction_throws() throws Exception { - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder() - .addLibraries(CelExtensions.strings(Function.SUBSTRING)) + Cel customCompilerCel = + runtimeFlavor + .builder() + .addCompilerLibraries(CelExtensions.strings(Function.SUBSTRING)) .build(); - CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() - .addLibraries(CelExtensions.strings(Function.REPLACE)) + Cel customRuntimeCel = + runtimeFlavor + .builder() + .addRuntimeLibraries(CelExtensions.strings(Function.REPLACE)) .build(); - CelAbstractSyntaxTree ast = celCompiler.compile("'test'.substring(2) == 'st'").getAst(); + CelAbstractSyntaxTree ast = + isParseOnly + ? customCompilerCel.parse("'test'.substring(2) == 'st'").getAst() + : customCompilerCel.compile("'test'.substring(2) == 'st'").getAst(); + + assertThrows(CelEvaluationException.class, () -> customRuntimeCel.createProgram(ast).eval()); + } + + private Object eval(Cel cel, String expression, Map variables) throws Exception { + CelAbstractSyntaxTree ast; + if (isParseOnly) { + ast = cel.parse(expression).getAst(); + } else { + ast = cel.compile(expression).getAst(); + } + return cel.createProgram(ast).eval(variables); + } + + private Object eval(String expression) throws Exception { + return eval(this.cel, expression, ImmutableMap.of()); + } - assertThrows(CelEvaluationException.class, () -> celRuntime.createProgram(ast).eval()); + private Object eval(String expression, Map variables) throws Exception { + return eval(this.cel, expression, variables); } } From b029be3cceeeb0dc957d55f8eb0518451b92b75c Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 20 Apr 2026 14:46:48 -0700 Subject: [PATCH 052/204] Support parsed-only evaluation to encoders extension PiperOrigin-RevId: 902832943 --- .../cel/extensions/CelEncoderExtensions.java | 8 +- .../extensions/CelEncoderExtensionsTest.java | 95 +++++++++---------- 2 files changed, 51 insertions(+), 52 deletions(-) diff --git a/extensions/src/main/java/dev/cel/extensions/CelEncoderExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelEncoderExtensions.java index a98f9db41..498b8555e 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelEncoderExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelEncoderExtensions.java @@ -135,9 +135,13 @@ public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) { functions.forEach( function -> { if (celOptions.evaluateCanonicalTypesToNativeValues()) { - runtimeBuilder.addFunctionBindings(function.nativeBytesFunctionBinding); + runtimeBuilder.addFunctionBindings( + CelFunctionBinding.fromOverloads( + function.getFunction(), function.nativeBytesFunctionBinding)); } else { - runtimeBuilder.addFunctionBindings(function.protoBytesFunctionBinding); + runtimeBuilder.addFunctionBindings( + CelFunctionBinding.fromOverloads( + function.getFunction(), function.protoBytesFunctionBinding)); } }); } diff --git a/extensions/src/test/java/dev/cel/extensions/CelEncoderExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelEncoderExtensionsTest.java index 7eed3dd5a..b0a501ddb 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelEncoderExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelEncoderExtensionsTest.java @@ -19,36 +19,45 @@ import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.bundle.Cel; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; import dev.cel.common.CelValidationException; import dev.cel.common.types.SimpleType; import dev.cel.common.values.CelByteString; -import dev.cel.compiler.CelCompiler; -import dev.cel.compiler.CelCompilerFactory; import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelRuntime; -import dev.cel.runtime.CelRuntimeFactory; +import dev.cel.testing.CelRuntimeFlavor; +import org.junit.Assume; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) public class CelEncoderExtensionsTest { private static final CelOptions CEL_OPTIONS = - CelOptions.current().build(); - - private static final CelCompiler CEL_COMPILER = - CelCompilerFactory.standardCelCompilerBuilder() - .addVar("stringVar", SimpleType.STRING) - .addLibraries(CelExtensions.encoders(CEL_OPTIONS)) - .build(); - private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder() - .setOptions(CEL_OPTIONS) - .addLibraries(CelExtensions.encoders(CEL_OPTIONS)) - .build(); + CelOptions.current().enableHeterogeneousNumericComparisons(true).build(); + + @TestParameter public CelRuntimeFlavor runtimeFlavor; + @TestParameter public boolean isParseOnly; + + private Cel cel; + + @Before + public void setUp() { + // Legacy runtime does not support parsed-only evaluation mode. + Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.LEGACY) && isParseOnly); + this.cel = + runtimeFlavor + .builder() + .setOptions(CEL_OPTIONS) + .addCompilerLibraries(CelExtensions.encoders(CEL_OPTIONS)) + .addRuntimeLibraries(CelExtensions.encoders(CEL_OPTIONS)) + .addVar("stringVar", SimpleType.STRING) + .build(); + } @Test public void library() { @@ -63,22 +72,14 @@ public void library() { @Test public void encode_success() throws Exception { - String encodedBytes = - (String) - CEL_RUNTIME - .createProgram(CEL_COMPILER.compile("base64.encode(b'hello')").getAst()) - .eval(); + String encodedBytes = (String) eval("base64.encode(b'hello')"); assertThat(encodedBytes).isEqualTo("aGVsbG8="); } @Test public void decode_success() throws Exception { - CelByteString decodedBytes = - (CelByteString) - CEL_RUNTIME - .createProgram(CEL_COMPILER.compile("base64.decode('aGVsbG8=')").getAst()) - .eval(); + CelByteString decodedBytes = (CelByteString) eval("base64.decode('aGVsbG8=')"); assertThat(decodedBytes.size()).isEqualTo(5); assertThat(new String(decodedBytes.toByteArray(), ISO_8859_1)).isEqualTo("hello"); @@ -86,12 +87,7 @@ public void decode_success() throws Exception { @Test public void decode_withoutPadding_success() throws Exception { - CelByteString decodedBytes = - (CelByteString) - CEL_RUNTIME - // RFC2045 6.8, padding can be ignored. - .createProgram(CEL_COMPILER.compile("base64.decode('aGVsbG8')").getAst()) - .eval(); + CelByteString decodedBytes = (CelByteString) eval("base64.decode('aGVsbG8')"); assertThat(decodedBytes.size()).isEqualTo(5); assertThat(new String(decodedBytes.toByteArray(), ISO_8859_1)).isEqualTo("hello"); @@ -99,50 +95,49 @@ public void decode_withoutPadding_success() throws Exception { @Test public void roundTrip_success() throws Exception { - String encodedString = - (String) - CEL_RUNTIME - .createProgram(CEL_COMPILER.compile("base64.encode(b'Hello World!')").getAst()) - .eval(); + String encodedString = (String) eval("base64.encode(b'Hello World!')"); CelByteString decodedBytes = (CelByteString) - CEL_RUNTIME - .createProgram(CEL_COMPILER.compile("base64.decode(stringVar)").getAst()) - .eval(ImmutableMap.of("stringVar", encodedString)); + eval("base64.decode(stringVar)", ImmutableMap.of("stringVar", encodedString)); assertThat(new String(decodedBytes.toByteArray(), ISO_8859_1)).isEqualTo("Hello World!"); } @Test public void encode_invalidParam_throwsCompilationException() { + Assume.assumeFalse(isParseOnly); CelValidationException e = assertThrows( - CelValidationException.class, - () -> CEL_COMPILER.compile("base64.encode('hello')").getAst()); + CelValidationException.class, () -> cel.compile("base64.encode('hello')").getAst()); assertThat(e).hasMessageThat().contains("found no matching overload for 'base64.encode'"); } @Test public void decode_invalidParam_throwsCompilationException() { + Assume.assumeFalse(isParseOnly); CelValidationException e = assertThrows( - CelValidationException.class, - () -> CEL_COMPILER.compile("base64.decode(b'aGVsbG8=')").getAst()); + CelValidationException.class, () -> cel.compile("base64.decode(b'aGVsbG8=')").getAst()); assertThat(e).hasMessageThat().contains("found no matching overload for 'base64.decode'"); } @Test public void decode_malformedBase64Char_throwsEvaluationException() throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile("base64.decode('z!')").getAst(); - CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> CEL_RUNTIME.createProgram(ast).eval()); + assertThrows(CelEvaluationException.class, () -> eval("base64.decode('z!')")); - assertThat(e) - .hasMessageThat() - .contains("Function 'base64_decode_string' failed with arg(s) 'z!'"); + assertThat(e).hasMessageThat().contains("failed with arg(s) 'z!'"); assertThat(e).hasCauseThat().hasMessageThat().contains("Illegal base64 character"); } + + private Object eval(String expr) throws Exception { + return eval(expr, ImmutableMap.of()); + } + + private Object eval(String expr, ImmutableMap vars) throws Exception { + CelAbstractSyntaxTree ast = isParseOnly ? cel.parse(expr).getAst() : cel.compile(expr).getAst(); + return cel.createProgram(ast).eval(vars); + } } From 5470c95da6fda1e418e485fd7f048e0e0573d8bf Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 20 Apr 2026 15:02:26 -0700 Subject: [PATCH 053/204] Restructure Java test runner to not require JVM classinfo inspection for loading descriptors PiperOrigin-RevId: 902839390 --- common/internal/BUILD.bazel | 5 -- .../java/dev/cel/common/internal/BUILD.bazel | 13 ----- .../DefaultInstanceMessageFactory.java | 5 +- .../internal/ProtoJavaQualifiedNames.java | 52 ------------------- .../main/java/dev/cel/protobuf/BUILD.bazel | 2 - .../protobuf/CelLiteDescriptorGenerator.java | 4 +- .../protobuf/ProtoDescriptorCollector.java | 5 +- testing/BUILD.bazel | 5 ++ .../dev/cel/testing/testrunner/BUILD.bazel | 13 +++-- .../testing/testrunner/CelTestContext.java | 26 ++++++---- .../CelTestSuiteTextProtoParser.java | 8 ++- .../testrunner/DefaultResultMatcher.java | 16 +++++- .../cel/testing/testrunner/RegistryUtils.java | 27 +++------- .../cel/testing/testrunner/TestExecutor.java | 2 + .../testing/testrunner/TestRunnerLibrary.java | 31 ++++------- .../java/dev/cel/testing/utils/BUILD.bazel | 9 +--- .../cel/testing/utils/ClassLoaderUtils.java | 32 ------------ .../dev/cel/testing/utils/ExprValueUtils.java | 39 ++++---------- .../testing/utils/ProtoDescriptorUtils.java | 34 +++--------- .../dev/cel/testing/testrunner/BUILD.bazel | 5 +- .../CustomVariableBindingUserTest.java | 32 ++++++++---- 21 files changed, 115 insertions(+), 250 deletions(-) delete mode 100644 common/src/main/java/dev/cel/common/internal/ProtoJavaQualifiedNames.java diff --git a/common/internal/BUILD.bazel b/common/internal/BUILD.bazel index 0a07e0d63..781566713 100644 --- a/common/internal/BUILD.bazel +++ b/common/internal/BUILD.bazel @@ -128,11 +128,6 @@ cel_android_library( exports = ["//common/src/main/java/dev/cel/common/internal:internal_android"], ) -java_library( - name = "proto_java_qualified_names", - exports = ["//common/src/main/java/dev/cel/common/internal:proto_java_qualified_names"], -) - java_library( name = "proto_time_utils", exports = ["//common/src/main/java/dev/cel/common/internal:proto_time_utils"], diff --git a/common/src/main/java/dev/cel/common/internal/BUILD.bazel b/common/src/main/java/dev/cel/common/internal/BUILD.bazel index 912b4de4b..6b470d98c 100644 --- a/common/src/main/java/dev/cel/common/internal/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/internal/BUILD.bazel @@ -153,7 +153,6 @@ java_library( tags = [ ], deps = [ - ":proto_java_qualified_names", ":reflection_util", "//common/annotations", "@maven//:com_google_guava_guava", @@ -396,18 +395,6 @@ java_library( ], ) -java_library( - name = "proto_java_qualified_names", - srcs = ["ProtoJavaQualifiedNames.java"], - tags = [ - ], - deps = [ - "//common/annotations", - "@maven//:com_google_guava_guava", - "@maven//:com_google_protobuf_protobuf_java", - ], -) - java_library( name = "reflection_util", srcs = ["ReflectionUtil.java"], diff --git a/common/src/main/java/dev/cel/common/internal/DefaultInstanceMessageFactory.java b/common/src/main/java/dev/cel/common/internal/DefaultInstanceMessageFactory.java index fcb0e7056..163d0273e 100644 --- a/common/src/main/java/dev/cel/common/internal/DefaultInstanceMessageFactory.java +++ b/common/src/main/java/dev/cel/common/internal/DefaultInstanceMessageFactory.java @@ -15,6 +15,7 @@ package dev.cel.common.internal; import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.GeneratorNames; import com.google.protobuf.Message; import com.google.protobuf.MessageLite; import dev.cel.common.annotations.Internal; @@ -45,9 +46,7 @@ public static DefaultInstanceMessageFactory getInstance() { public Optional getPrototype(Descriptor descriptor) { MessageLite defaultInstance = DefaultInstanceMessageLiteFactory.getInstance() - .getPrototype( - descriptor.getFullName(), - ProtoJavaQualifiedNames.getFullyQualifiedJavaClassName(descriptor)) + .getPrototype(descriptor.getFullName(), GeneratorNames.getBytecodeClassName(descriptor)) .orElse(null); if (defaultInstance == null) { return Optional.empty(); diff --git a/common/src/main/java/dev/cel/common/internal/ProtoJavaQualifiedNames.java b/common/src/main/java/dev/cel/common/internal/ProtoJavaQualifiedNames.java deleted file mode 100644 index f27181a50..000000000 --- a/common/src/main/java/dev/cel/common/internal/ProtoJavaQualifiedNames.java +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2025 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dev.cel.common.internal; - -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.Descriptors.FileDescriptor; -import com.google.protobuf.GeneratorNames; -import dev.cel.common.annotations.Internal; - -/** - * Helper class for constructing a fully qualified Java class name from a protobuf descriptor. - * - *

CEL Library Internals. Do Not Use. - */ -@Internal -public final class ProtoJavaQualifiedNames { - /** - * Retrieves the full Java class name from the given descriptor - * - * @return fully qualified class name. - *

Example 1: dev.cel.expr.Value - *

Example 2: com.google.rpc.context.AttributeContext$Resource (Nested classes) - *

Example 3: com.google.api.expr.cel.internal.testdata$SingleFileProto$SingleFile$Path - * (Nested class with java multiple files disabled) - */ - public static String getFullyQualifiedJavaClassName(Descriptor descriptor) { - return GeneratorNames.getBytecodeClassName(descriptor); - } - - /** - * Gets the java package name from the descriptor. See - * https://developers.google.com/protocol-buffers/docs/reference/java-generated#package for rules - * on package name generation - */ - public static String getJavaPackageName(FileDescriptor fileDescriptor) { - return GeneratorNames.getFileJavaPackage(fileDescriptor.toProto()); - } - - private ProtoJavaQualifiedNames() {} -} diff --git a/protobuf/src/main/java/dev/cel/protobuf/BUILD.bazel b/protobuf/src/main/java/dev/cel/protobuf/BUILD.bazel index 6e7b473eb..b2dac98e7 100644 --- a/protobuf/src/main/java/dev/cel/protobuf/BUILD.bazel +++ b/protobuf/src/main/java/dev/cel/protobuf/BUILD.bazel @@ -21,7 +21,6 @@ java_binary( ":java_file_generator", ":proto_descriptor_collector", "//common:cel_descriptor_util", - "//common/internal:proto_java_qualified_names", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", "@maven//:info_picocli_picocli", @@ -50,7 +49,6 @@ java_library( ":cel_lite_descriptor", ":debug_printer", ":lite_descriptor_codegen_metadata", - "//common/internal:proto_java_qualified_names", "//common/internal:well_known_proto", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", diff --git a/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptorGenerator.java b/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptorGenerator.java index 276dd7f91..8c4eaea1c 100644 --- a/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptorGenerator.java +++ b/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptorGenerator.java @@ -23,8 +23,8 @@ import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FileDescriptor; import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.GeneratorNames; import dev.cel.common.CelDescriptorUtil; -import dev.cel.common.internal.ProtoJavaQualifiedNames; import dev.cel.protobuf.JavaFileGenerator.GeneratedClass; import dev.cel.protobuf.JavaFileGenerator.JavaFileGeneratorOption; import java.io.File; @@ -117,7 +117,7 @@ public Integer call() throws Exception { private ImmutableList codegenCelLiteDescriptors( FileDescriptor targetFileDescriptor) throws Exception { - String javaPackageName = ProtoJavaQualifiedNames.getJavaPackageName(targetFileDescriptor); + String javaPackageName = GeneratorNames.getFileJavaPackage(targetFileDescriptor.toProto()); String javaClassName; List descriptors = targetFileDescriptor.getMessageTypes(); diff --git a/protobuf/src/main/java/dev/cel/protobuf/ProtoDescriptorCollector.java b/protobuf/src/main/java/dev/cel/protobuf/ProtoDescriptorCollector.java index 0031fe6a6..c2fe20557 100644 --- a/protobuf/src/main/java/dev/cel/protobuf/ProtoDescriptorCollector.java +++ b/protobuf/src/main/java/dev/cel/protobuf/ProtoDescriptorCollector.java @@ -22,7 +22,7 @@ import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FieldDescriptor.JavaType; import com.google.protobuf.Descriptors.FileDescriptor; -import dev.cel.common.internal.ProtoJavaQualifiedNames; +import com.google.protobuf.GeneratorNames; import dev.cel.common.internal.WellKnownProto; import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.EncodingType; @@ -93,8 +93,7 @@ ImmutableList collectCodegenMetadata(Descriptor d // Maps are resolved as an actual Java map, and doesn't have a MessageLite.Builder associated. if (!messageDescriptor.getOptions().getMapEntry()) { String sanitizedJavaClassName = - ProtoJavaQualifiedNames.getFullyQualifiedJavaClassName(messageDescriptor) - .replace('$', '.'); + GeneratorNames.getBytecodeClassName(messageDescriptor).replace('$', '.'); descriptorCodegenBuilder.setJavaClassName(sanitizedJavaClassName); } diff --git a/testing/BUILD.bazel b/testing/BUILD.bazel index b9e68f003..cc389fed1 100644 --- a/testing/BUILD.bazel +++ b/testing/BUILD.bazel @@ -45,3 +45,8 @@ java_library( name = "expr_value_utils", exports = ["//testing/src/main/java/dev/cel/testing/utils:expr_value_utils"], ) + +java_library( + name = "proto_descriptor_utils", + exports = ["//testing/src/main/java/dev/cel/testing/utils:proto_descriptor_utils"], +) diff --git a/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel b/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel index 5af0665f9..d0fed9bea 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel +++ b/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel @@ -93,10 +93,10 @@ java_library( "//bundle:environment_yaml_parser", "//common:cel_ast", "//common:cel_descriptor_util", + "//common:cel_descriptors", "//common:compiler_common", "//common:options", "//common:proto_ast", - "//common/internal:default_instance_message_factory", "//policy", "//policy:compiler_factory", "//policy:parser", @@ -104,7 +104,6 @@ java_library( "//policy:validation_exception", "//runtime", "//testing:expr_value_utils", - "//testing/testrunner:proto_descriptor_utils", "@cel_spec//proto/cel/expr:expr_java_proto", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", @@ -166,10 +165,11 @@ java_library( "//:auto_value", "//bundle:cel", "//common:cel_descriptor_util", + "//common:cel_descriptors", "//common:options", "//policy:parser", "//runtime", - "//testing/testrunner:proto_descriptor_utils", + "//testing:proto_descriptor_utils", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", @@ -182,8 +182,7 @@ java_library( tags = [ ], deps = [ - "//common/internal:default_instance_message_factory", - "//testing/testrunner:proto_descriptor_utils", + "//common:cel_descriptors", "@maven//:com_google_protobuf_protobuf_java", ], ) @@ -212,8 +211,10 @@ java_library( "//:java_truth", "//bundle:cel", "//common:cel_ast", + "//common:cel_descriptors", "//runtime", "//testing:expr_value_utils", + "//testing:proto_descriptor_utils", "@cel_spec//proto/cel/expr:expr_java_proto", "@maven//:com_google_protobuf_protobuf_java", "@maven//:com_google_truth_extensions_truth_proto_extension", @@ -229,7 +230,9 @@ java_library( ":cel_test_suite", ":cel_test_suite_exception", ":registry_utils", + "//common:cel_descriptors", "//common/annotations", + "//testing:proto_descriptor_utils", "@cel_spec//proto/cel/expr:expr_java_proto", "@cel_spec//proto/cel/expr/conformance/test:suite_java_proto", "@maven//:com_google_guava_guava", diff --git a/testing/src/main/java/dev/cel/testing/testrunner/CelTestContext.java b/testing/src/main/java/dev/cel/testing/testrunner/CelTestContext.java index 5635b6152..1be0bab25 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/CelTestContext.java +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelTestContext.java @@ -25,6 +25,7 @@ import dev.cel.bundle.Cel; import dev.cel.bundle.CelFactory; import dev.cel.common.CelDescriptorUtil; +import dev.cel.common.CelDescriptors; import dev.cel.common.CelOptions; import dev.cel.policy.CelPolicyParser; import dev.cel.runtime.CelLateFunctionBindings; @@ -125,6 +126,20 @@ public interface BindingTransformer { abstract ImmutableSet fileTypes(); + @Memoized + public Optional celDescriptors() { + if (fileDescriptorSetPath().isPresent()) { + try { + return Optional.of( + ProtoDescriptorUtils.getDescriptorsFromFile(fileDescriptorSetPath().get())); + } catch (IOException e) { + throw new IllegalStateException( + "Failed to load descriptors from path: " + fileDescriptorSetPath().get(), e); + } + } + return Optional.empty(); + } + @Memoized public Optional typeRegistry() { if (fileTypes().isEmpty() && !fileDescriptorSetPath().isPresent()) { @@ -136,15 +151,8 @@ public Optional typeRegistry() { CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(fileTypes()) .messageTypeDescriptors()); } - if (fileDescriptorSetPath().isPresent()) { - try { - builder.add( - ProtoDescriptorUtils.getAllDescriptorsFromJvm(fileDescriptorSetPath().get()) - .messageTypeDescriptors()); - } catch (IOException e) { - throw new IllegalStateException( - "Failed to load descriptors from path: " + fileDescriptorSetPath().get(), e); - } + if (celDescriptors().isPresent()) { + builder.add(celDescriptors().get().messageTypeDescriptors()); } return Optional.of(builder.build()); } diff --git a/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteTextProtoParser.java b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteTextProtoParser.java index 5e7e62498..9c0ab4720 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteTextProtoParser.java +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteTextProtoParser.java @@ -22,6 +22,7 @@ import com.google.protobuf.TextFormat; import com.google.protobuf.TextFormat.ParseException; import com.google.protobuf.TypeRegistry; +import dev.cel.common.CelDescriptors; import dev.cel.common.annotations.Internal; import dev.cel.expr.conformance.test.InputValue; import dev.cel.expr.conformance.test.TestCase; @@ -30,6 +31,7 @@ import dev.cel.testing.testrunner.CelTestSuite.CelTestSection; import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase; import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase.Input.Binding; +import dev.cel.testing.utils.ProtoDescriptorUtils; import java.io.IOException; import java.util.Map; @@ -71,8 +73,10 @@ private TestSuite parseTestSuite( TypeRegistry typeRegistry = customTypeRegistry; ExtensionRegistry extensionRegistry = customExtensionRegistry; if (fileDescriptorSetPath != null) { - extensionRegistry = RegistryUtils.getExtensionRegistry(fileDescriptorSetPath); - typeRegistry = RegistryUtils.getTypeRegistry(fileDescriptorSetPath); + CelDescriptors descriptors = + ProtoDescriptorUtils.getDescriptorsFromFile(fileDescriptorSetPath); + extensionRegistry = RegistryUtils.getExtensionRegistry(descriptors); + typeRegistry = RegistryUtils.getTypeRegistry(descriptors); } TextFormat.Parser parser = TextFormat.Parser.newBuilder().setTypeRegistry(typeRegistry).build(); TestSuite.Builder builder = TestSuite.newBuilder(); diff --git a/testing/src/main/java/dev/cel/testing/testrunner/DefaultResultMatcher.java b/testing/src/main/java/dev/cel/testing/testrunner/DefaultResultMatcher.java index 2d33253af..279d591a2 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/DefaultResultMatcher.java +++ b/testing/src/main/java/dev/cel/testing/testrunner/DefaultResultMatcher.java @@ -22,11 +22,13 @@ import dev.cel.expr.MapValue; import dev.cel.bundle.Cel; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelDescriptors; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelRuntime.Program; import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase.Output; import dev.cel.testing.testrunner.ResultMatcher.ResultMatcherParams; import dev.cel.testing.testrunner.ResultMatcher.ResultMatcherParams.ComputedOutput; +import dev.cel.testing.utils.ProtoDescriptorUtils; import java.io.IOException; final class DefaultResultMatcher implements ResultMatcher { @@ -41,6 +43,10 @@ public void match(ResultMatcherParams params, Cel cel) throws Exception { "Error: " + params.computedOutput().error().getMessage(), params.computedOutput().error()); } + if (params.computedOutput().kind().equals(ComputedOutput.Kind.UNKNOWN_SET)) { + throw new AssertionError( + "Expected value but got UnknownSet: " + params.computedOutput().unknownSet()); + } CelAbstractSyntaxTree exprAst = cel.compile(result.resultExpr()).getAst(); Program exprProgram = cel.createProgram(exprAst); Object evaluationResult = null; @@ -59,6 +65,10 @@ public void match(ResultMatcherParams params, Cel cel) throws Exception { "Error: " + params.computedOutput().error().getMessage(), params.computedOutput().error()); } + if (params.computedOutput().kind().equals(ComputedOutput.Kind.UNKNOWN_SET)) { + throw new AssertionError( + "Expected value but got UnknownSet: " + params.computedOutput().unknownSet()); + } assertExprValue( params.computedOutput().exprValue(), toExprValue(result.resultValue(), params.resultType())); @@ -85,12 +95,14 @@ private static void assertExprValue(ExprValue exprValue, ExprValue expectedExprV throws IOException { String fileDescriptorSetPath = System.getProperty("file_descriptor_set_path"); if (fileDescriptorSetPath != null) { + CelDescriptors descriptors = + ProtoDescriptorUtils.getDescriptorsFromFile(fileDescriptorSetPath); assertThat(exprValue) .ignoringRepeatedFieldOrderOfFieldDescriptors( MapValue.getDescriptor().findFieldByName("entries")) .unpackingAnyUsing( - RegistryUtils.getTypeRegistry(fileDescriptorSetPath), - RegistryUtils.getExtensionRegistry(fileDescriptorSetPath)) + RegistryUtils.getTypeRegistry(descriptors), + RegistryUtils.getExtensionRegistry(descriptors)) .isEqualTo(expectedExprValue); } else { assertThat(exprValue) diff --git a/testing/src/main/java/dev/cel/testing/testrunner/RegistryUtils.java b/testing/src/main/java/dev/cel/testing/testrunner/RegistryUtils.java index b2f195606..a10904abb 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/RegistryUtils.java +++ b/testing/src/main/java/dev/cel/testing/testrunner/RegistryUtils.java @@ -13,44 +13,33 @@ // limitations under the License. package dev.cel.testing.testrunner; -import static dev.cel.testing.utils.ProtoDescriptorUtils.getAllDescriptorsFromJvm; + import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.DynamicMessage; import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.Message; import com.google.protobuf.TypeRegistry; -import dev.cel.common.internal.DefaultInstanceMessageFactory; -import java.io.IOException; -import java.util.NoSuchElementException; +import dev.cel.common.CelDescriptors; /** Utility class for creating registries from a file descriptor set. */ public final class RegistryUtils { /** Returns the {@link TypeRegistry} for the given file descriptor set. */ - public static TypeRegistry getTypeRegistry(String fileDescriptorSetPath) throws IOException { - return TypeRegistry.newBuilder() - .add(getAllDescriptorsFromJvm(fileDescriptorSetPath).messageTypeDescriptors()) - .build(); + public static TypeRegistry getTypeRegistry(CelDescriptors descriptors) { + return TypeRegistry.newBuilder().add(descriptors.messageTypeDescriptors()).build(); } /** Returns the {@link ExtensionRegistry} for the given file descriptor set. */ - public static ExtensionRegistry getExtensionRegistry(String fileDescriptorSetPath) - throws IOException { + public static ExtensionRegistry getExtensionRegistry(CelDescriptors descriptors) { ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); - getAllDescriptorsFromJvm(fileDescriptorSetPath) + descriptors .extensionDescriptors() .forEach( (descriptorName, descriptor) -> { if (descriptor.getType().equals(FieldDescriptor.Type.MESSAGE)) { - Message output = - DefaultInstanceMessageFactory.getInstance() - .getPrototype(descriptor.getMessageType()) - .orElseThrow( - () -> - new NoSuchElementException( - "Could not find a default message for: " - + descriptor.getFullName())); + Message output = DynamicMessage.getDefaultInstance(descriptor.getMessageType()); extensionRegistry.add(descriptor, output); } else { extensionRegistry.add(descriptor); diff --git a/testing/src/main/java/dev/cel/testing/testrunner/TestExecutor.java b/testing/src/main/java/dev/cel/testing/testrunner/TestExecutor.java index 6f6dff3c1..181d99c6f 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/TestExecutor.java +++ b/testing/src/main/java/dev/cel/testing/testrunner/TestExecutor.java @@ -236,6 +236,8 @@ public String describe() { testResult.setStatus(JUnitXmlReporter.TestResult.FAILURE); testResult.setThrowable(result.getFailures().get(0).getException()); testReporter.onTestFailure(testResult); + System.err.println("Test failed: " + testName); + result.getFailures().forEach(failure -> failure.getException().printStackTrace()); } } } diff --git a/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java b/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java index 2465d330e..69c365972 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java +++ b/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java @@ -28,6 +28,7 @@ import com.google.common.collect.ImmutableMap; import com.google.protobuf.Any; import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.DynamicMessage; import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.Message; import com.google.protobuf.TextFormat; @@ -38,10 +39,10 @@ import dev.cel.bundle.CelEnvironmentYamlParser; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelDescriptorUtil; +import dev.cel.common.CelDescriptors; import dev.cel.common.CelOptions; import dev.cel.common.CelProtoAbstractSyntaxTree; import dev.cel.common.CelValidationException; -import dev.cel.common.internal.DefaultInstanceMessageFactory; import dev.cel.policy.CelPolicy; import dev.cel.policy.CelPolicyCompilerFactory; import dev.cel.policy.CelPolicyParser; @@ -52,12 +53,10 @@ import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase; import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase.Input.Binding; import dev.cel.testing.testrunner.ResultMatcher.ResultMatcherParams; -import dev.cel.testing.utils.ProtoDescriptorUtils; import java.io.File; import java.io.IOException; import java.nio.file.Paths; import java.util.Map; -import java.util.NoSuchElementException; import java.util.Optional; import java.util.logging.Logger; import org.jspecify.annotations.Nullable; @@ -201,16 +200,13 @@ private static Cel extendCel(CelTestContext celTestContext, CelOptions celOption // // Note: This needs to be added first because the config file may contain type information // regarding proto messages that need to be added to the cel object. - if (celTestContext.fileDescriptorSetPath().isPresent()) { + CelDescriptors descriptors = celTestContext.celDescriptors().orElse(null); + if (descriptors != null) { extendedCel = extendedCel .toCelBuilder() - .addMessageTypes( - ProtoDescriptorUtils.getAllDescriptorsFromJvm( - celTestContext.fileDescriptorSetPath().get()) - .messageTypeDescriptors()) - .setExtensionRegistry( - RegistryUtils.getExtensionRegistry(celTestContext.fileDescriptorSetPath().get())) + .addMessageTypes(descriptors.messageTypeDescriptors()) + .setExtensionRegistry(RegistryUtils.getExtensionRegistry(descriptors)) .build(); } @@ -369,22 +365,13 @@ private static Message unpackAny(Any any, CelTestContext celTestContext) throws "Proto descriptors are required for unpacking Any messages."); } Descriptor descriptor = - RegistryUtils.getTypeRegistry(celTestContext.fileDescriptorSetPath().get()) + RegistryUtils.getTypeRegistry(celTestContext.celDescriptors().get()) .getDescriptorForTypeUrl(any.getTypeUrl()); - return getDefaultInstance(descriptor) + return DynamicMessage.getDefaultInstance(descriptor) .getParserForType() .parseFrom( any.getValue(), - RegistryUtils.getExtensionRegistry(celTestContext.fileDescriptorSetPath().get())); - } - - private static Message getDefaultInstance(Descriptor descriptor) throws IOException { - return DefaultInstanceMessageFactory.getInstance() - .getPrototype(descriptor) - .orElseThrow( - () -> - new NoSuchElementException( - "Could not find a default message for: " + descriptor.getFullName())); + RegistryUtils.getExtensionRegistry(celTestContext.celDescriptors().get())); } private static Message getEvaluatedContextExpr( diff --git a/testing/src/main/java/dev/cel/testing/utils/BUILD.bazel b/testing/src/main/java/dev/cel/testing/utils/BUILD.bazel index 2947709e5..eea56752d 100644 --- a/testing/src/main/java/dev/cel/testing/utils/BUILD.bazel +++ b/testing/src/main/java/dev/cel/testing/utils/BUILD.bazel @@ -15,19 +15,16 @@ java_library( tags = [ ], deps = [ - "//common:cel_descriptor_util", "//common:cel_descriptors", - "//common/internal:default_instance_message_factory", "//common/internal:proto_time_utils", "//common/types", "//common/types:type_providers", "//common/values", "//common/values:cel_byte_string", "//runtime:unknown_attributes", + "//testing:proto_descriptor_utils", "//testing/testrunner:registry_utils", "@cel_spec//proto/cel/expr:expr_java_proto", - "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", - "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", "@maven_android//:com_google_protobuf_protobuf_javalite", @@ -41,7 +38,6 @@ java_library( ], deps = [ "@maven//:com_google_guava_guava", - "@maven//:com_google_protobuf_protobuf_java", "@maven//:io_github_classgraph_classgraph", ], ) @@ -49,12 +45,9 @@ java_library( java_library( name = "proto_descriptor_utils", srcs = ["ProtoDescriptorUtils.java"], - tags = [ - ], deps = [ "//common:cel_descriptor_util", "//common:cel_descriptors", - "//testing/testrunner:class_loader_utils", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", ], diff --git a/testing/src/main/java/dev/cel/testing/utils/ClassLoaderUtils.java b/testing/src/main/java/dev/cel/testing/utils/ClassLoaderUtils.java index 652ec85c6..31f45d48f 100644 --- a/testing/src/main/java/dev/cel/testing/utils/ClassLoaderUtils.java +++ b/testing/src/main/java/dev/cel/testing/utils/ClassLoaderUtils.java @@ -15,51 +15,19 @@ import com.google.common.base.Supplier; import com.google.common.base.Suppliers; -import com.google.common.collect.ImmutableList; -import com.google.protobuf.Descriptors.Descriptor; import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassInfo; import io.github.classgraph.ClassInfoList; import io.github.classgraph.ScanResult; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.util.logging.Logger; /** Utility class for loading classes using {@link ClassGraph}. */ public final class ClassLoaderUtils { - private static final Logger logger = Logger.getLogger(ClassLoaderUtils.class.getName()); - // Using `enableAllInfo()` to scan all class files upfront. This avoids repeated parsing // of class files by individual methods, improving efficiency. private static final Supplier CLASS_SCAN_RESULT = Suppliers.memoize(() -> new ClassGraph().enableAllInfo().scan()); - /** - * Loads all descriptor type classes from the JVM. - * - * @return A list of {@link Descriptor} objects representing the descriptors loaded from the JVM. - * @throws IOException If there is an error during the loading process. - */ - public static ImmutableList loadDescriptors() throws IOException { - ClassInfoList classInfoList = CLASS_SCAN_RESULT.get().getAllStandardClasses(); - ImmutableList.Builder compileTimeLoadedDescriptors = ImmutableList.builder(); - - for (ClassInfo classInfo : classInfoList) { - try { - Class classInfoClass = classInfo.loadClass(); - Descriptor descriptor = (Descriptor) classInfoClass.getMethod("getDescriptor").invoke(null); - compileTimeLoadedDescriptors.add(descriptor); - } catch (InvocationTargetException e) { - logger.severe( - "Failed to load descriptor: " + classInfo.getName() + " with error: " + e); - } catch (Exception e) { - // Ignore classes that do not have a getDescriptor method. - } - } - return compileTimeLoadedDescriptors.build(); - } - /** * Loads all subclasses of the given class from the JVM. * diff --git a/testing/src/main/java/dev/cel/testing/utils/ExprValueUtils.java b/testing/src/main/java/dev/cel/testing/utils/ExprValueUtils.java index 9bccecc95..10ab52786 100644 --- a/testing/src/main/java/dev/cel/testing/utils/ExprValueUtils.java +++ b/testing/src/main/java/dev/cel/testing/utils/ExprValueUtils.java @@ -24,11 +24,12 @@ import com.google.protobuf.Any; import com.google.protobuf.ByteString; import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.DynamicMessage; import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.Message; import com.google.protobuf.NullValue; import com.google.protobuf.TypeRegistry; -import dev.cel.common.internal.DefaultInstanceMessageFactory; +import dev.cel.common.CelDescriptors; import dev.cel.common.internal.ProtoTimeUtils; import dev.cel.common.types.CelType; import dev.cel.common.types.ListType; @@ -44,7 +45,6 @@ import java.time.Instant; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; import java.util.Optional; /** Utility class for ExprValue and Value type conversions during test execution. */ @@ -53,29 +53,23 @@ public final class ExprValueUtils { private ExprValueUtils() {} - /** * Converts a {@link Value} to a Java native object using the given file descriptor set to parse * `Any` messages. * * @param value The {@link Value} to convert. - * @param fileDescriptorSetPath The path to the file descriptor set. * @return The converted Java object. * @throws IOException If there's an error during conversion. */ - public static Object fromValue(Value value, String fileDescriptorSetPath) throws IOException { - TypeRegistry typeRegistry = RegistryUtils.getTypeRegistry(fileDescriptorSetPath); - ExtensionRegistry extensionRegistry = RegistryUtils.getExtensionRegistry(fileDescriptorSetPath); + public static Object fromValue(Value value, CelDescriptors descriptors) throws IOException { + TypeRegistry typeRegistry = RegistryUtils.getTypeRegistry(descriptors); + ExtensionRegistry extensionRegistry = RegistryUtils.getExtensionRegistry(descriptors); return fromValue(value, typeRegistry, extensionRegistry); } - /** - * Converts a {@link Value} to a Java native object. - * - * @param value The {@link Value} to convert. - * @return The converted Java object. - * @throws IOException If there's an error during conversion. - */ + public static Object fromValue(Value value, String fileDescriptorSetPath) throws IOException { + return fromValue(value, ProtoDescriptorUtils.getDescriptorsFromFile(fileDescriptorSetPath)); + } /** * Converts a {@link Value} to a Java native object using custom registries. @@ -97,7 +91,7 @@ public static Object fromValue( "Unknown type, descriptor was not found in registry: " + value.getObjectValue().getTypeUrl()); } - Message prototype = getDefaultInstance(descriptor); + Message prototype = DynamicMessage.getDefaultInstance(descriptor); return prototype .getParserForType() .parseFrom(value.getObjectValue().getValue(), extensionRegistry); @@ -197,7 +191,8 @@ public static Value toValue(Object object, CelType type) throws Exception { if (object instanceof dev.cel.expr.Value) { object = Value.parseFrom( - ((dev.cel.expr.Value) object).toByteArray(), ExtensionRegistry.getEmptyRegistry()); + ((dev.cel.expr.Value) object).toByteArray(), + ExtensionRegistry.getEmptyRegistry()); } if (object instanceof Value) { return (Value) object; @@ -302,16 +297,4 @@ public static Value toValue(Object object, CelType type) throws Exception { throw new IllegalArgumentException( String.format("Unexpected result type: %s", object.getClass())); } - - private static Message getDefaultInstance(Descriptor descriptor) { - return DefaultInstanceMessageFactory.getInstance() - .getPrototype(descriptor) - .orElseThrow( - () -> - new NoSuchElementException( - "Could not find a default message for: " + descriptor.getFullName())); - } - - - } diff --git a/testing/src/main/java/dev/cel/testing/utils/ProtoDescriptorUtils.java b/testing/src/main/java/dev/cel/testing/utils/ProtoDescriptorUtils.java index b6fa2e64b..880c03e12 100644 --- a/testing/src/main/java/dev/cel/testing/utils/ProtoDescriptorUtils.java +++ b/testing/src/main/java/dev/cel/testing/utils/ProtoDescriptorUtils.java @@ -1,4 +1,4 @@ -// Copyright 2025 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,18 +11,11 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -package dev.cel.testing.utils; -import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.collect.ImmutableSet.toImmutableSet; -import static dev.cel.testing.utils.ClassLoaderUtils.loadDescriptors; +package dev.cel.testing.utils; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; import com.google.common.io.Files; import com.google.protobuf.DescriptorProtos.FileDescriptorSet; -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.Descriptors.FileDescriptor; import com.google.protobuf.ExtensionRegistry; import dev.cel.common.CelDescriptorUtil; import dev.cel.common.CelDescriptors; @@ -33,30 +26,15 @@ public final class ProtoDescriptorUtils { /** - * Returns all the descriptors from the JVM. + * Returns all the descriptors from the file descriptor set file. * * @return The {@link CelDescriptors} object containing all the descriptors. */ - public static CelDescriptors getAllDescriptorsFromJvm(String fileDescriptorSetPath) + public static CelDescriptors getDescriptorsFromFile(String fileDescriptorSetPath) throws IOException { - ImmutableList compileTimeLoadedDescriptors = loadDescriptors(); FileDescriptorSet fileDescriptorSet = getFileDescriptorSet(fileDescriptorSetPath); - ImmutableSet runtimeFileDescriptorNames = - CelDescriptorUtil.getFileDescriptorsFromFileDescriptorSet(fileDescriptorSet).stream() - .map(FileDescriptor::getFullName) - .collect(toImmutableSet()); - - // Get all the file descriptors from the descriptors which are loaded from the JVM and use the - // ones which match the ones provided by the user in the file descriptor set. - ImmutableList userProvidedFileDescriptors = - CelDescriptorUtil.getFileDescriptorsForDescriptors(compileTimeLoadedDescriptors).stream() - .filter( - fileDescriptor -> runtimeFileDescriptorNames.contains(fileDescriptor.getFullName())) - .collect(toImmutableList()); - - // Get all the descriptors from the file descriptors above which include nested, extension and - // message type descriptors as well. - return CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(userProvidedFileDescriptors); + return CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( + CelDescriptorUtil.getFileDescriptorsFromFileDescriptorSet(fileDescriptorSet)); } private static FileDescriptorSet getFileDescriptorSet(String fileDescriptorSetPath) diff --git a/testing/src/test/java/dev/cel/testing/testrunner/BUILD.bazel b/testing/src/test/java/dev/cel/testing/testrunner/BUILD.bazel index 755ef732d..a12654d2c 100644 --- a/testing/src/test/java/dev/cel/testing/testrunner/BUILD.bazel +++ b/testing/src/test/java/dev/cel/testing/testrunner/BUILD.bazel @@ -51,6 +51,7 @@ java_library( name = "custom_variable_binding_user_test", srcs = ["CustomVariableBindingUserTest.java"], deps = [ + "//bundle:cel", "//testing/testrunner:cel_test_context", "//testing/testrunner:cel_user_test_template", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", @@ -186,10 +187,6 @@ cel_java_test( name = "custom_variable_binding_test_runner_sample", cel_expr = "custom_variable_bindings/policy.yaml", config = "custom_variable_bindings/config.yaml", - proto_deps = [ - "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", - "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", - ], test_data_path = "//testing/src/test/resources/policy", test_src = ":custom_variable_binding_user_test", test_suite = "custom_variable_bindings/tests.yaml", diff --git a/testing/src/test/java/dev/cel/testing/testrunner/CustomVariableBindingUserTest.java b/testing/src/test/java/dev/cel/testing/testrunner/CustomVariableBindingUserTest.java index 707b5eef9..37382052c 100644 --- a/testing/src/test/java/dev/cel/testing/testrunner/CustomVariableBindingUserTest.java +++ b/testing/src/test/java/dev/cel/testing/testrunner/CustomVariableBindingUserTest.java @@ -15,7 +15,8 @@ package dev.cel.testing.testrunner; import com.google.common.collect.ImmutableMap; -import com.google.protobuf.Any; +import com.google.protobuf.ExtensionRegistry; +import dev.cel.bundle.CelFactory; import dev.cel.expr.conformance.proto2.TestAllTypes; import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; import org.junit.runner.RunWith; @@ -29,15 +30,24 @@ public class CustomVariableBindingUserTest extends CelUserTestTemplate { public CustomVariableBindingUserTest() { - super( - CelTestContext.newBuilder() - .setVariableBindings( - ImmutableMap.of( - "spec", - Any.pack( - TestAllTypes.newBuilder() - .setExtension(TestAllTypesExtensions.int32Ext, 1) - .build()))) - .build()); + super(newTestContext()); + } + + private static CelTestContext newTestContext() { + ExtensionRegistry registry = ExtensionRegistry.newInstance(); + registry.add(TestAllTypesExtensions.int32Ext); + + return CelTestContext.newBuilder() + .setCel( + CelFactory.standardCelBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .addFileTypes(TestAllTypesExtensions.getDescriptor()) + .setExtensionRegistry(registry) + .build()) + .setVariableBindings( + ImmutableMap.of( + "spec", + TestAllTypes.newBuilder().setExtension(TestAllTypesExtensions.int32Ext, 1).build())) + .build(); } } From 54060a2198d674faae14a96bb5dc4744df26de1d Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 20 Apr 2026 16:39:13 -0700 Subject: [PATCH 054/204] Support parsed-only evaluation to sets extension PiperOrigin-RevId: 902882306 --- .../extensions/SetsExtensionsRuntimeImpl.java | 42 ++- .../cel/extensions/CelSetsExtensionsTest.java | 313 ++++++++---------- .../runtime/CelLiteRuntimeAndroidTest.java | 3 +- 3 files changed, 164 insertions(+), 194 deletions(-) diff --git a/extensions/src/main/java/dev/cel/extensions/SetsExtensionsRuntimeImpl.java b/extensions/src/main/java/dev/cel/extensions/SetsExtensionsRuntimeImpl.java index a42fba189..a02fdba8a 100644 --- a/extensions/src/main/java/dev/cel/extensions/SetsExtensionsRuntimeImpl.java +++ b/extensions/src/main/java/dev/cel/extensions/SetsExtensionsRuntimeImpl.java @@ -45,28 +45,34 @@ ImmutableSet newFunctionBindings() { for (SetsFunction function : functions) { switch (function) { case CONTAINS: - bindingBuilder.add( - CelFunctionBinding.from( - "list_sets_contains_list", - Collection.class, - Collection.class, - this::containsAll)); + bindingBuilder.addAll( + CelFunctionBinding.fromOverloads( + function.getFunction(), + CelFunctionBinding.from( + "list_sets_contains_list", + Collection.class, + Collection.class, + this::containsAll))); break; case EQUIVALENT: - bindingBuilder.add( - CelFunctionBinding.from( - "list_sets_equivalent_list", - Collection.class, - Collection.class, - (listA, listB) -> containsAll(listA, listB) && containsAll(listB, listA))); + bindingBuilder.addAll( + CelFunctionBinding.fromOverloads( + function.getFunction(), + CelFunctionBinding.from( + "list_sets_equivalent_list", + Collection.class, + Collection.class, + (listA, listB) -> containsAll(listA, listB) && containsAll(listB, listA)))); break; case INTERSECTS: - bindingBuilder.add( - CelFunctionBinding.from( - "list_sets_intersects_list", - Collection.class, - Collection.class, - this::setIntersects)); + bindingBuilder.addAll( + CelFunctionBinding.fromOverloads( + function.getFunction(), + CelFunctionBinding.from( + "list_sets_intersects_list", + Collection.class, + Collection.class, + this::setIntersects))); break; } } diff --git a/extensions/src/test/java/dev/cel/extensions/CelSetsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelSetsExtensionsTest.java index 1aac5a023..9007bba2e 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelSetsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelSetsExtensionsTest.java @@ -19,8 +19,11 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelBuilder; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; @@ -30,47 +33,34 @@ import dev.cel.common.CelValidationResult; import dev.cel.common.types.ListType; import dev.cel.common.types.SimpleType; -import dev.cel.compiler.CelCompiler; -import dev.cel.compiler.CelCompilerFactory; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelRuntime; -import dev.cel.runtime.CelRuntimeFactory; +import dev.cel.testing.CelRuntimeFlavor; import java.util.List; +import java.util.Map; +import org.junit.Assume; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) public final class CelSetsExtensionsTest { - private static final CelOptions CEL_OPTIONS = CelOptions.current().build(); - private static final CelCompiler COMPILER = - CelCompilerFactory.standardCelCompilerBuilder() - .addMessageTypes(TestAllTypes.getDescriptor()) - .setOptions(CEL_OPTIONS) - .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) - .addLibraries(CelExtensions.sets(CEL_OPTIONS)) - .addVar("list", ListType.create(SimpleType.INT)) - .addVar("subList", ListType.create(SimpleType.INT)) - .addFunctionDeclarations( - CelFunctionDecl.newFunctionDeclaration( - "new_int", - CelOverloadDecl.newGlobalOverload( - "new_int_int64", SimpleType.INT, SimpleType.INT))) - .build(); - - private static final CelRuntime RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder() - .addMessageTypes(TestAllTypes.getDescriptor()) - .addLibraries(CelExtensions.sets(CEL_OPTIONS)) - .setOptions(CEL_OPTIONS) - .addFunctionBindings( - CelFunctionBinding.from( - "new_int_int64", - Long.class, - // Intentionally return java.lang.Integer to test primitive type adaptation - Math::toIntExact)) - .build(); + private static final CelOptions CEL_OPTIONS = + CelOptions.current().enableHeterogeneousNumericComparisons(true).build(); + + @TestParameter public CelRuntimeFlavor runtimeFlavor; + @TestParameter public boolean isParseOnly; + + private Cel cel; + + @Before + public void setUp() { + // Legacy runtime does not support parsed-only evaluation mode. + Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.LEGACY) && isParseOnly); + this.cel = setupEnv(runtimeFlavor.builder()); + } @Test public void library() { @@ -87,22 +77,14 @@ public void library() { public void contains_integerListWithSameValue_succeeds() throws Exception { ImmutableList list = ImmutableList.of(1, 2, 3, 4); ImmutableList subList = ImmutableList.of(1, 2, 3, 4); - CelAbstractSyntaxTree ast = COMPILER.compile("sets.contains(list, subList)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object result = program.eval(ImmutableMap.of("list", list, "subList", subList)); - - assertThat(result).isEqualTo(true); + assertThat( + eval("sets.contains(list, subList)", ImmutableMap.of("list", list, "subList", subList))) + .isEqualTo(true); } @Test public void contains_integerListAsExpression_succeeds() throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("sets.contains([1, 1], [1])").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object result = program.eval(); - - assertThat(result).isEqualTo(true); + assertThat(eval("sets.contains([1, 1], [1])")).isEqualTo(true); } @Test @@ -119,12 +101,7 @@ public void contains_integerListAsExpression_succeeds() throws Exception { + " [TestAllTypes{single_int64: 2, single_uint64: 3u}])', expected: false}") public void contains_withProtoMessage_succeeds(String expression, boolean expected) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - boolean result = (boolean) program.eval(); - - assertThat(result).isEqualTo(expected); + assertThat(eval(expression)).isEqualTo(expected); } @Test @@ -133,12 +110,7 @@ public void contains_withProtoMessage_succeeds(String expression, boolean expect @TestParameters("{expression: 'sets.contains([new_int(2)], [1])', expected: false}") public void contains_withFunctionReturningInteger_succeeds(String expression, boolean expected) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - boolean result = (boolean) program.eval(); - - assertThat(result).isEqualTo(expected); + assertThat(eval(expression)).isEqualTo(expected); } @Test @@ -157,12 +129,9 @@ public void contains_withFunctionReturningInteger_succeeds(String expression, bo @TestParameters("{list: [1], subList: [1, 2], expected: false}") public void contains_withIntTypes_succeeds( List list, List subList, boolean expected) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("sets.contains(list, subList)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object result = program.eval(ImmutableMap.of("list", list, "subList", subList)); - - assertThat(result).isEqualTo(expected); + assertThat( + eval("sets.contains(list, subList)", ImmutableMap.of("list", list, "subList", subList))) + .isEqualTo(expected); } @Test @@ -177,12 +146,9 @@ public void contains_withIntTypes_succeeds( @TestParameters("{list: [2, 3.0], subList: [2, 3], expected: true}") public void contains_withDoubleTypes_succeeds( List list, List subList, boolean expected) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("sets.contains(list, subList)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object result = program.eval(ImmutableMap.of("list", list, "subList", subList)); - - assertThat(result).isEqualTo(expected); + assertThat( + eval("sets.contains(list, subList)", ImmutableMap.of("list", list, "subList", subList))) + .isEqualTo(expected); } @Test @@ -193,12 +159,7 @@ public void contains_withDoubleTypes_succeeds( @TestParameters("{expression: 'sets.contains([[1], [2, 3.0]], [[2, 3]])', expected: true}") public void contains_withNestedLists_succeeds(String expression, boolean expected) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object result = program.eval(); - - assertThat(result).isEqualTo(expected); + assertThat(eval(expression)).isEqualTo(expected); } @Test @@ -206,19 +167,16 @@ public void contains_withNestedLists_succeeds(String expression, boolean expecte @TestParameters("{expression: 'sets.contains([1], [1, \"1\"])', expected: false}") public void contains_withMixingIntAndString_succeeds(String expression, boolean expected) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object result = program.eval(); - - assertThat(result).isEqualTo(expected); + assertThat(eval(expression)).isEqualTo(expected); } @Test - @TestParameters("{expression: 'sets.contains([1], [\"1\"])'}") - @TestParameters("{expression: 'sets.contains([\"1\"], [1])'}") - public void contains_withMixingIntAndString_throwsException(String expression) throws Exception { - CelValidationResult invalidData = COMPILER.compile(expression); + public void contains_withMixingIntAndString_throwsException( + @TestParameter({"sets.contains([1], [\"1\"])", "sets.contains([\"1\"], [1])"}) + String expression) + throws Exception { + Assume.assumeFalse(isParseOnly); + CelValidationResult invalidData = cel.compile(expression); assertThat(invalidData.getErrors()).hasSize(1); assertThat(invalidData.getErrors().get(0).getMessage()) @@ -227,12 +185,7 @@ public void contains_withMixingIntAndString_throwsException(String expression) t @Test public void contains_withMixedValues_succeeds() throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile("sets.contains([1, 2], [2u, 2.0])").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object result = program.eval(); - - assertThat(result).isEqualTo(true); + assertThat(eval("sets.contains([1, 2], [2u, 2.0])")).isEqualTo(true); } @Test @@ -249,12 +202,7 @@ public void contains_withMixedValues_succeeds() throws Exception { "{expression: 'sets.contains([[[[[[5]]]]]], [[1], [2, 3.0], [[[[[5]]]]]])', expected: false}") public void contains_withMultiLevelNestedList_succeeds(String expression, boolean expected) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object result = program.eval(); - - assertThat(result).isEqualTo(expected); + assertThat(eval(expression)).isEqualTo(expected); } @Test @@ -269,12 +217,7 @@ public void contains_withMultiLevelNestedList_succeeds(String expression, boolea + " false}") public void contains_withMapValues_succeeds(String expression, boolean expected) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - boolean result = (boolean) program.eval(); - - assertThat(result).isEqualTo(expected); + assertThat(eval(expression)).isEqualTo(expected); } @Test @@ -289,12 +232,7 @@ public void contains_withMapValues_succeeds(String expression, boolean expected) @TestParameters("{expression: 'sets.equivalent([1, 2], [2, 2, 2])', expected: false}") public void equivalent_withIntTypes_succeeds(String expression, boolean expected) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object result = program.eval(); - - assertThat(result).isEqualTo(expected); + assertThat(eval(expression)).isEqualTo(expected); } @Test @@ -308,12 +246,7 @@ public void equivalent_withIntTypes_succeeds(String expression, boolean expected @TestParameters("{expression: 'sets.equivalent([1, 2], [1u, 2, 2.3])', expected: false}") public void equivalent_withMixedTypes_succeeds(String expression, boolean expected) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object result = program.eval(); - - assertThat(result).isEqualTo(expected); + assertThat(eval(expression)).isEqualTo(expected); } @Test @@ -338,12 +271,7 @@ public void equivalent_withMixedTypes_succeeds(String expression, boolean expect + " [TestAllTypes{single_int64: 2, single_uint64: 3u}])', expected: false}") public void equivalent_withProtoMessage_succeeds(String expression, boolean expected) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - boolean result = (boolean) program.eval(); - - assertThat(result).isEqualTo(expected); + assertThat(eval(expression)).isEqualTo(expected); } @Test @@ -361,12 +289,7 @@ public void equivalent_withProtoMessage_succeeds(String expression, boolean expe + " expected: false}") public void equivalent_withMapValues_succeeds(String expression, boolean expected) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - boolean result = (boolean) program.eval(); - - assertThat(result).isEqualTo(expected); + assertThat(eval(expression)).isEqualTo(expected); } @Test @@ -391,12 +314,7 @@ public void equivalent_withMapValues_succeeds(String expression, boolean expecte @TestParameters("{expression: 'sets.intersects([1], [1.1, 2u])', expected: false}") public void intersects_withMixedTypes_succeeds(String expression, boolean expected) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - Object result = program.eval(); - - assertThat(result).isEqualTo(expected); + assertThat(eval(expression)).isEqualTo(expected); } @Test @@ -414,12 +332,11 @@ public void intersects_withMixedTypes_succeeds(String expression, boolean expect @TestParameters("{expression: 'sets.intersects([{2: 1}], [{1: 1}])', expected: false}") public void intersects_withMapValues_succeeds(String expression, boolean expected) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - boolean result = (boolean) program.eval(); + // The LEGACY runtime is not spec compliant, because decimal keys are not allowed for maps. + Assume.assumeFalse( + runtimeFlavor.equals(CelRuntimeFlavor.PLANNER) && expression.contains("1.0:")); - assertThat(result).isEqualTo(expected); + assertThat(eval(expression)).isEqualTo(expected); } @Test @@ -444,25 +361,21 @@ public void intersects_withMapValues_succeeds(String expression, boolean expecte + " [TestAllTypes{single_int64: 2, single_uint64: 3u}])', expected: false}") public void intersects_withProtoMessage_succeeds(String expression, boolean expected) throws Exception { - CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - - boolean result = (boolean) program.eval(); - - assertThat(result).isEqualTo(expected); + assertThat(eval(expression)).isEqualTo(expected); } @Test public void setsExtension_containsFunctionSubset_succeeds() throws Exception { CelSetsExtensions setsExtensions = CelExtensions.sets(CelOptions.DEFAULT, SetsFunction.CONTAINS); - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder().addLibraries(setsExtensions).build(); - CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(setsExtensions).build(); + Cel cel = + runtimeFlavor + .builder() + .addCompilerLibraries(setsExtensions) + .addRuntimeLibraries(setsExtensions) + .build(); - Object evaluatedResult = - celRuntime.createProgram(celCompiler.compile("sets.contains([1, 2], [2])").getAst()).eval(); + Object evaluatedResult = eval(cel, "sets.contains([1, 2], [2])", ImmutableMap.of()); assertThat(evaluatedResult).isEqualTo(true); } @@ -471,15 +384,14 @@ public void setsExtension_containsFunctionSubset_succeeds() throws Exception { public void setsExtension_equivalentFunctionSubset_succeeds() throws Exception { CelSetsExtensions setsExtensions = CelExtensions.sets(CelOptions.DEFAULT, SetsFunction.EQUIVALENT); - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder().addLibraries(setsExtensions).build(); - CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(setsExtensions).build(); + Cel cel = + runtimeFlavor + .builder() + .addCompilerLibraries(setsExtensions) + .addRuntimeLibraries(setsExtensions) + .build(); - Object evaluatedResult = - celRuntime - .createProgram(celCompiler.compile("sets.equivalent([1, 1], [1])").getAst()) - .eval(); + Object evaluatedResult = eval(cel, "sets.equivalent([1, 1], [1])", ImmutableMap.of()); assertThat(evaluatedResult).isEqualTo(true); } @@ -488,44 +400,95 @@ public void setsExtension_equivalentFunctionSubset_succeeds() throws Exception { public void setsExtension_intersectsFunctionSubset_succeeds() throws Exception { CelSetsExtensions setsExtensions = CelExtensions.sets(CelOptions.DEFAULT, SetsFunction.INTERSECTS); - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder().addLibraries(setsExtensions).build(); - CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(setsExtensions).build(); + Cel cel = + runtimeFlavor + .builder() + .addCompilerLibraries(setsExtensions) + .addRuntimeLibraries(setsExtensions) + .build(); - Object evaluatedResult = - celRuntime - .createProgram(celCompiler.compile("sets.intersects([1, 1], [1])").getAst()) - .eval(); + Object evaluatedResult = eval(cel, "sets.intersects([1, 1], [1])", ImmutableMap.of()); assertThat(evaluatedResult).isEqualTo(true); } @Test public void setsExtension_compileUnallowedFunction_throws() { + Assume.assumeFalse(isParseOnly); CelSetsExtensions setsExtensions = CelExtensions.sets(CelOptions.DEFAULT, SetsFunction.EQUIVALENT); - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder().addLibraries(setsExtensions).build(); + Cel cel = runtimeFlavor.builder().addCompilerLibraries(setsExtensions).build(); assertThrows( - CelValidationException.class, - () -> celCompiler.compile("sets.contains([1, 2], [2])").getAst()); + CelValidationException.class, () -> cel.compile("sets.contains([1, 2], [2])").getAst()); } @Test public void setsExtension_evaluateUnallowedFunction_throws() throws Exception { CelSetsExtensions setsExtensions = CelExtensions.sets(CelOptions.DEFAULT, SetsFunction.CONTAINS, SetsFunction.EQUIVALENT); - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder().addLibraries(setsExtensions).build(); - CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() - .addLibraries(CelExtensions.sets(CelOptions.DEFAULT, SetsFunction.EQUIVALENT)) + CelSetsExtensions runtimeLibrary = + CelExtensions.sets(CelOptions.DEFAULT, SetsFunction.EQUIVALENT); + Cel cel = + runtimeFlavor + .builder() + .addCompilerLibraries(setsExtensions) + .addRuntimeLibraries(runtimeLibrary) .build(); - CelAbstractSyntaxTree ast = celCompiler.compile("sets.contains([1, 2], [2])").getAst(); + CelAbstractSyntaxTree ast = + isParseOnly + ? cel.parse("sets.contains([1, 2], [2])").getAst() + : cel.compile("sets.contains([1, 2], [2])").getAst(); + + if (runtimeFlavor.equals(CelRuntimeFlavor.PLANNER) && !isParseOnly) { + // Fails at plan time + assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast)); + } else { + CelRuntime.Program program = cel.createProgram(ast); + assertThrows(CelEvaluationException.class, () -> program.eval()); + } + } + + private Object eval(Cel cel, String expression, Map variables) throws Exception { + CelAbstractSyntaxTree ast; + if (isParseOnly) { + ast = cel.parse(expression).getAst(); + } else { + ast = cel.compile(expression).getAst(); + } + return cel.createProgram(ast).eval(variables); + } + + private Object eval(String expression) throws Exception { + return eval(this.cel, expression, ImmutableMap.of()); + } + + private Object eval(String expression, Map variables) throws Exception { + return eval(this.cel, expression, variables); + } - assertThrows(CelEvaluationException.class, () -> celRuntime.createProgram(ast).eval()); + private static Cel setupEnv(CelBuilder celBuilder) { + return celBuilder + .addMessageTypes(TestAllTypes.getDescriptor()) + .setOptions(CEL_OPTIONS) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .addCompilerLibraries(CelExtensions.sets(CEL_OPTIONS)) + .addRuntimeLibraries(CelExtensions.sets(CEL_OPTIONS)) + .addVar("list", ListType.create(SimpleType.INT)) + .addVar("subList", ListType.create(SimpleType.INT)) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "new_int", + CelOverloadDecl.newGlobalOverload("new_int_int64", SimpleType.INT, SimpleType.INT))) + .addFunctionBindings( + CelFunctionBinding.fromOverloads( + "new_int", + CelFunctionBinding.from( + "new_int_int64", + Long.class, + // Intentionally return java.lang.Integer to test primitive type adaptation + Math::toIntExact))) + .build(); } } diff --git a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeAndroidTest.java b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeAndroidTest.java index 54ce24417..73492d126 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeAndroidTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeAndroidTest.java @@ -149,9 +149,10 @@ public void toRuntimeBuilder_propertiesCopied() { assertThat(newRuntimeBuilder.standardFunctionBuilder.build()) .containsExactly(intFunction, equalsOperator) .inOrder(); - assertThat(newRuntimeBuilder.customFunctionBindings).hasSize(2); + assertThat(newRuntimeBuilder.customFunctionBindings).hasSize(3); assertThat(newRuntimeBuilder.customFunctionBindings).containsKey("string_isEmpty"); assertThat(newRuntimeBuilder.customFunctionBindings).containsKey("list_sets_intersects_list"); + assertThat(newRuntimeBuilder.customFunctionBindings).containsKey("sets.intersects"); } @Test From d12fb7e6b7bec515a0214f245feda2a1348a5a55 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 22 Apr 2026 13:42:38 -0700 Subject: [PATCH 055/204] Support parsed only evaluation to math extension. Remove signed long support for `uint` PiperOrigin-RevId: 904020157 --- .../main/java/dev/cel/extensions/BUILD.bazel | 2 +- .../dev/cel/extensions/CelExtensions.java | 69 ++- .../dev/cel/extensions/CelMathExtensions.java | 263 ++++++----- .../cel/extensions/CelMathExtensionsTest.java | 417 ++++++++---------- 4 files changed, 353 insertions(+), 398 deletions(-) diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel index 2eb26846f..f8e4bfc8c 100644 --- a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel @@ -42,6 +42,7 @@ java_library( ":strings", "//common:options", "//extensions:extension_library", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], ) @@ -121,7 +122,6 @@ java_library( ":extension_library", "//checker:checker_builder", "//common:compiler_common", - "//common:options", "//common/ast", "//common/exceptions:numeric_overflow", "//common/internal:comparison_functions", diff --git a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java index 2d14ed118..8f1770f3f 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java @@ -19,7 +19,9 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Streams; +import com.google.errorprone.annotations.InlineMe; import dev.cel.common.CelOptions; +import dev.cel.extensions.CelMathExtensions.Function; import java.util.Set; /** @@ -121,12 +123,9 @@ public static CelProtoExtensions protos() { *

This will include all functions denoted in {@link CelMathExtensions.Function}, including any * future additions. To expose only a subset of these, use {@link #math(CelOptions, * CelMathExtensions.Function...)} or {@link #math(CelOptions,int)} instead. - * - * @param celOptions CelOptions to configure CelMathExtension with. This should be the same - * options object used to configure the compilation/runtime environments. */ - public static CelMathExtensions math(CelOptions celOptions) { - return CelMathExtensions.library(celOptions).latest(); + public static CelMathExtensions math() { + return CelMathExtensions.library().latest(); } /** @@ -134,8 +133,8 @@ public static CelMathExtensions math(CelOptions celOptions) { * *

Refer to README.md for functions available in each version. */ - public static CelMathExtensions math(CelOptions celOptions, int version) { - return CelMathExtensions.library(celOptions).version(version); + public static CelMathExtensions math(int version) { + return CelMathExtensions.library().version(version); } /** @@ -150,13 +149,9 @@ public static CelMathExtensions math(CelOptions celOptions, int version) { * collision. * *

This will include only the specific functions denoted by {@link CelMathExtensions.Function}. - * - * @param celOptions CelOptions to configure CelMathExtension with. This should be the same - * options object used to configure the compilation/runtime environments. */ - public static CelMathExtensions math( - CelOptions celOptions, CelMathExtensions.Function... functions) { - return math(celOptions, ImmutableSet.copyOf(functions)); + public static CelMathExtensions math(CelMathExtensions.Function... functions) { + return math(ImmutableSet.copyOf(functions)); } /** @@ -171,13 +166,49 @@ public static CelMathExtensions math( * collision. * *

This will include only the specific functions denoted by {@link CelMathExtensions.Function}. - * - * @param celOptions CelOptions to configure CelMathExtension with. This should be the same - * options object used to configure the compilation/runtime environments. */ + public static CelMathExtensions math(Set functions) { + return new CelMathExtensions(functions); + } + + /** + * @deprecated Use {@link #math()} instead. + */ + @Deprecated + @InlineMe(replacement = "CelExtensions.math()", imports = "dev.cel.extensions.CelExtensions") + public static CelMathExtensions math(CelOptions unused) { + return math(); + } + + /** + * @deprecated Use {@link #math(int)} instead. + */ + @Deprecated + @InlineMe( + replacement = "CelExtensions.math(version)", + imports = "dev.cel.extensions.CelExtensions") + public static CelMathExtensions math(CelOptions unused, int version) { + return math(version); + } + + /** + * @deprecated Use {@link #math(Function...)} instead. + */ + @Deprecated + public static CelMathExtensions math(CelOptions unused, CelMathExtensions.Function... functions) { + return math(ImmutableSet.copyOf(functions)); + } + + /** + * @deprecated Use {@link #math(Set)} instead. + */ + @Deprecated + @InlineMe( + replacement = "CelExtensions.math(functions)", + imports = "dev.cel.extensions.CelExtensions") public static CelMathExtensions math( - CelOptions celOptions, Set functions) { - return new CelMathExtensions(celOptions, functions); + CelOptions unused, Set functions) { + return math(functions); } /** @@ -354,7 +385,7 @@ public static CelExtensionLibrary getE case "lists": return CelListsExtensions.library(); case "math": - return CelMathExtensions.library(options); + return CelMathExtensions.library(); case "optional": return CelOptionalLibrary.library(); case "protos": diff --git a/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java index 22336eb22..78a0fd51c 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java @@ -27,7 +27,6 @@ import dev.cel.checker.CelCheckerBuilder; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelIssue; -import dev.cel.common.CelOptions; import dev.cel.common.CelOverloadDecl; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; @@ -136,7 +135,8 @@ public final class CelMathExtensions return builder.buildOrThrow(); } - enum Function { + /** Enumeration of functions for Math extension. */ + public enum Function { MAX( CelFunctionDecl.newFunctionDeclaration( MATH_MAX_FUNCTION, @@ -206,51 +206,59 @@ enum Function { MATH_MAX_OVERLOAD_DOC, SimpleType.DYN, ListType.create(SimpleType.DYN))), - ImmutableSet.of( - CelFunctionBinding.from("math_@max_double", Double.class, x -> x), - CelFunctionBinding.from("math_@max_int", Long.class, x -> x), - CelFunctionBinding.from( - "math_@max_double_double", Double.class, Double.class, CelMathExtensions::maxPair), - CelFunctionBinding.from( - "math_@max_int_int", Long.class, Long.class, CelMathExtensions::maxPair), - CelFunctionBinding.from( - "math_@max_int_double", Long.class, Double.class, CelMathExtensions::maxPair), - CelFunctionBinding.from( - "math_@max_double_int", Double.class, Long.class, CelMathExtensions::maxPair), - CelFunctionBinding.from("math_@max_list_dyn", List.class, CelMathExtensions::maxList)), - ImmutableSet.of( - CelFunctionBinding.from("math_@max_uint", Long.class, x -> x), - CelFunctionBinding.from( - "math_@max_uint_uint", Long.class, Long.class, CelMathExtensions::maxPair), - CelFunctionBinding.from( - "math_@max_double_uint", Double.class, Long.class, CelMathExtensions::maxPair), - CelFunctionBinding.from( - "math_@max_uint_int", Long.class, Long.class, CelMathExtensions::maxPair), - CelFunctionBinding.from( - "math_@max_uint_double", Long.class, Double.class, CelMathExtensions::maxPair), - CelFunctionBinding.from( - "math_@max_int_uint", Long.class, Long.class, CelMathExtensions::maxPair)), - ImmutableSet.of( - CelFunctionBinding.from("math_@max_uint", UnsignedLong.class, x -> x), - CelFunctionBinding.from( - "math_@max_uint_uint", - UnsignedLong.class, - UnsignedLong.class, - CelMathExtensions::maxPair), - CelFunctionBinding.from( - "math_@max_double_uint", - Double.class, - UnsignedLong.class, - CelMathExtensions::maxPair), - CelFunctionBinding.from( - "math_@max_uint_int", UnsignedLong.class, Long.class, CelMathExtensions::maxPair), - CelFunctionBinding.from( - "math_@max_uint_double", - UnsignedLong.class, - Double.class, - CelMathExtensions::maxPair), - CelFunctionBinding.from( - "math_@max_int_uint", Long.class, UnsignedLong.class, CelMathExtensions::maxPair))), + ImmutableSet.builder() + .add(CelFunctionBinding.from("math_@max_double", Double.class, x -> x)) + .add(CelFunctionBinding.from("math_@max_int", Long.class, x -> x)) + .add( + CelFunctionBinding.from( + "math_@max_double_double", + Double.class, + Double.class, + CelMathExtensions::maxPair)) + .add( + CelFunctionBinding.from( + "math_@max_int_int", Long.class, Long.class, CelMathExtensions::maxPair)) + .add( + CelFunctionBinding.from( + "math_@max_int_double", Long.class, Double.class, CelMathExtensions::maxPair)) + .add( + CelFunctionBinding.from( + "math_@max_double_int", Double.class, Long.class, CelMathExtensions::maxPair)) + .add( + CelFunctionBinding.from( + "math_@max_list_dyn", List.class, CelMathExtensions::maxList)) + .add(CelFunctionBinding.from("math_@max_uint", UnsignedLong.class, x -> x)) + .add( + CelFunctionBinding.from( + "math_@max_uint_uint", + UnsignedLong.class, + UnsignedLong.class, + CelMathExtensions::maxPair)) + .add( + CelFunctionBinding.from( + "math_@max_double_uint", + Double.class, + UnsignedLong.class, + CelMathExtensions::maxPair)) + .add( + CelFunctionBinding.from( + "math_@max_uint_int", + UnsignedLong.class, + Long.class, + CelMathExtensions::maxPair)) + .add( + CelFunctionBinding.from( + "math_@max_uint_double", + UnsignedLong.class, + Double.class, + CelMathExtensions::maxPair)) + .add( + CelFunctionBinding.from( + "math_@max_int_uint", + Long.class, + UnsignedLong.class, + CelMathExtensions::maxPair)) + .build()), MIN( CelFunctionDecl.newFunctionDeclaration( MATH_MIN_FUNCTION, @@ -320,51 +328,59 @@ enum Function { MATH_MIN_OVERLOAD_DOC, SimpleType.DYN, ListType.create(SimpleType.DYN))), - ImmutableSet.of( - CelFunctionBinding.from("math_@min_double", Double.class, x -> x), - CelFunctionBinding.from("math_@min_int", Long.class, x -> x), - CelFunctionBinding.from( - "math_@min_double_double", Double.class, Double.class, CelMathExtensions::minPair), - CelFunctionBinding.from( - "math_@min_int_int", Long.class, Long.class, CelMathExtensions::minPair), - CelFunctionBinding.from( - "math_@min_int_double", Long.class, Double.class, CelMathExtensions::minPair), - CelFunctionBinding.from( - "math_@min_double_int", Double.class, Long.class, CelMathExtensions::minPair), - CelFunctionBinding.from("math_@min_list_dyn", List.class, CelMathExtensions::minList)), - ImmutableSet.of( - CelFunctionBinding.from("math_@min_uint", Long.class, x -> x), - CelFunctionBinding.from( - "math_@min_uint_uint", Long.class, Long.class, CelMathExtensions::minPair), - CelFunctionBinding.from( - "math_@min_double_uint", Double.class, Long.class, CelMathExtensions::minPair), - CelFunctionBinding.from( - "math_@min_uint_int", Long.class, Long.class, CelMathExtensions::minPair), - CelFunctionBinding.from( - "math_@min_uint_double", Long.class, Double.class, CelMathExtensions::minPair), - CelFunctionBinding.from( - "math_@min_int_uint", Long.class, Long.class, CelMathExtensions::minPair)), - ImmutableSet.of( - CelFunctionBinding.from("math_@min_uint", UnsignedLong.class, x -> x), - CelFunctionBinding.from( - "math_@min_uint_uint", - UnsignedLong.class, - UnsignedLong.class, - CelMathExtensions::minPair), - CelFunctionBinding.from( - "math_@min_double_uint", - Double.class, - UnsignedLong.class, - CelMathExtensions::minPair), - CelFunctionBinding.from( - "math_@min_uint_int", UnsignedLong.class, Long.class, CelMathExtensions::minPair), - CelFunctionBinding.from( - "math_@min_uint_double", - UnsignedLong.class, - Double.class, - CelMathExtensions::minPair), - CelFunctionBinding.from( - "math_@min_int_uint", Long.class, UnsignedLong.class, CelMathExtensions::minPair))), + ImmutableSet.builder() + .add(CelFunctionBinding.from("math_@min_double", Double.class, x -> x)) + .add(CelFunctionBinding.from("math_@min_int", Long.class, x -> x)) + .add( + CelFunctionBinding.from( + "math_@min_double_double", + Double.class, + Double.class, + CelMathExtensions::minPair)) + .add( + CelFunctionBinding.from( + "math_@min_int_int", Long.class, Long.class, CelMathExtensions::minPair)) + .add( + CelFunctionBinding.from( + "math_@min_int_double", Long.class, Double.class, CelMathExtensions::minPair)) + .add( + CelFunctionBinding.from( + "math_@min_double_int", Double.class, Long.class, CelMathExtensions::minPair)) + .add( + CelFunctionBinding.from( + "math_@min_list_dyn", List.class, CelMathExtensions::minList)) + .add(CelFunctionBinding.from("math_@min_uint", UnsignedLong.class, x -> x)) + .add( + CelFunctionBinding.from( + "math_@min_uint_uint", + UnsignedLong.class, + UnsignedLong.class, + CelMathExtensions::minPair)) + .add( + CelFunctionBinding.from( + "math_@min_double_uint", + Double.class, + UnsignedLong.class, + CelMathExtensions::minPair)) + .add( + CelFunctionBinding.from( + "math_@min_uint_int", + UnsignedLong.class, + Long.class, + CelMathExtensions::minPair)) + .add( + CelFunctionBinding.from( + "math_@min_uint_double", + UnsignedLong.class, + Double.class, + CelMathExtensions::minPair)) + .add( + CelFunctionBinding.from( + "math_@min_int_uint", + Long.class, + UnsignedLong.class, + CelMathExtensions::minPair)) + .build()), CEIL( CelFunctionDecl.newFunctionDeclaration( MATH_CEIL_FUNCTION, @@ -646,36 +662,14 @@ enum Function { private final CelFunctionDecl functionDecl; private final ImmutableSet functionBindings; - private final ImmutableSet functionBindingsULongSigned; - private final ImmutableSet functionBindingsULongUnsigned; String getFunction() { return functionDecl.name(); } Function(CelFunctionDecl functionDecl, ImmutableSet bindings) { - this(functionDecl, bindings, ImmutableSet.of(), ImmutableSet.of()); - } - - Function( - CelFunctionDecl functionDecl, - ImmutableSet functionBindings, - ImmutableSet functionBindingsULongSigned, - ImmutableSet functionBindingsULongUnsigned) { this.functionDecl = functionDecl; - this.functionBindings = - functionBindings.isEmpty() - ? ImmutableSet.of() - : CelFunctionBinding.fromOverloads(functionDecl.name(), functionBindings); - this.functionBindingsULongSigned = - functionBindingsULongSigned.isEmpty() - ? ImmutableSet.of() - : CelFunctionBinding.fromOverloads(functionDecl.name(), functionBindingsULongSigned); - this.functionBindingsULongUnsigned = - functionBindingsULongUnsigned.isEmpty() - ? ImmutableSet.of() - : CelFunctionBinding.fromOverloads( - functionDecl.name(), functionBindingsULongUnsigned); + this.functionBindings = bindings; } } @@ -684,10 +678,8 @@ private static final class Library implements CelExtensionLibrarybuilder() .addAll(version1.functions) .add(Function.SQRT) - .build(), - enableUnsignedLongs); + .build()); } @Override @@ -734,25 +724,20 @@ public ImmutableSet versions() { } } - private static final Library LIBRARY_UNSIGNED_LONGS_ENABLED = new Library(true); - private static final Library LIBRARY_UNSIGNED_LONGS_DISABLED = new Library(false); + private static final Library LIBRARY = new Library(); - static CelExtensionLibrary library(CelOptions celOptions) { - return celOptions.enableUnsignedLongs() - ? LIBRARY_UNSIGNED_LONGS_ENABLED - : LIBRARY_UNSIGNED_LONGS_DISABLED; + static CelExtensionLibrary library() { + return LIBRARY; } - private final boolean enableUnsignedLongs; private final ImmutableSet functions; private final int version; - CelMathExtensions(CelOptions celOptions, Set functions) { - this(-1, functions, celOptions.enableUnsignedLongs()); + CelMathExtensions(Set functions) { + this(-1, functions); } - private CelMathExtensions(int version, Set functions, boolean enableUnsignedLongs) { - this.enableUnsignedLongs = enableUnsignedLongs; + private CelMathExtensions(int version, Set functions) { this.version = version; this.functions = ImmutableSet.copyOf(functions); } @@ -788,11 +773,11 @@ public void setCheckerOptions(CelCheckerBuilder checkerBuilder) { public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) { functions.forEach( function -> { - runtimeBuilder.addFunctionBindings(function.functionBindings); - runtimeBuilder.addFunctionBindings( - enableUnsignedLongs - ? function.functionBindingsULongUnsigned - : function.functionBindingsULongSigned); + ImmutableSet combined = function.functionBindings; + if (!combined.isEmpty()) { + runtimeBuilder.addFunctionBindings( + CelFunctionBinding.fromOverloads(function.functionDecl.name(), combined)); + } }); } diff --git a/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java index bcdfb0a21..383e50aa2 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java @@ -20,8 +20,10 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.primitives.UnsignedLong; +import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; +import dev.cel.bundle.Cel; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; @@ -35,34 +37,36 @@ import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelRuntime; import dev.cel.runtime.CelRuntimeFactory; +import dev.cel.testing.CelRuntimeFlavor; +import java.util.Map; +import org.junit.Assume; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) public class CelMathExtensionsTest { - private static final CelOptions CEL_OPTIONS = - CelOptions.current().enableUnsignedLongs(false).build(); - private static final CelCompiler CEL_COMPILER = - CelCompilerFactory.standardCelCompilerBuilder() - .setOptions(CEL_OPTIONS) - .addLibraries(CelExtensions.math(CEL_OPTIONS)) - .build(); - private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder() - .setOptions(CEL_OPTIONS) - .addLibraries(CelExtensions.math(CEL_OPTIONS)) - .build(); - private static final CelOptions CEL_UNSIGNED_OPTIONS = CelOptions.current().build(); - private static final CelCompiler CEL_UNSIGNED_COMPILER = - CelCompilerFactory.standardCelCompilerBuilder() - .setOptions(CEL_UNSIGNED_OPTIONS) - .addLibraries(CelExtensions.math(CEL_UNSIGNED_OPTIONS)) - .build(); - private static final CelRuntime CEL_UNSIGNED_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder() - .setOptions(CEL_UNSIGNED_OPTIONS) - .addLibraries(CelExtensions.math(CEL_UNSIGNED_OPTIONS)) - .build(); + @TestParameter public CelRuntimeFlavor runtimeFlavor; + @TestParameter public boolean isParseOnly; + + private Cel cel; + + @Before + public void setUp() { + // Legacy runtime does not support parsed-only evaluation mode. + Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.LEGACY) && isParseOnly); + this.cel = + runtimeFlavor + .builder() + .setOptions( + CelOptions.current() + .enableHeterogeneousNumericComparisons( + runtimeFlavor.equals(CelRuntimeFlavor.PLANNER)) + .build()) + .addCompilerLibraries(CelExtensions.math()) + .addRuntimeLibraries(CelExtensions.math()) + .build(); + } @Test @TestParameters("{expr: 'math.greatest(-5)', expectedResult: -5}") @@ -97,9 +101,7 @@ public class CelMathExtensionsTest { "{expr: 'math.greatest([dyn(5.4), dyn(10), dyn(3u), dyn(-5.0), dyn(3.5)])', expectedResult:" + " 10}") public void greatest_intResult_success(String expr, long expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = eval(expr); assertThat(result).isEqualTo(expectedResult); } @@ -136,9 +138,7 @@ public void greatest_intResult_success(String expr, long expectedResult) throws "{expr: 'math.greatest([dyn(5.4), dyn(10.0), dyn(3u), dyn(-5.0), dyn(3.5)])', expectedResult:" + " 10.0}") public void greatest_doubleResult_success(String expr, double expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = eval(expr); assertThat(result).isEqualTo(expectedResult); } @@ -163,16 +163,16 @@ public void greatest_doubleResult_success(String expr, double expectedResult) th + " '10.0'}") public void greatest_doubleResult_withUnsignedLongsEnabled_success( String expr, double expectedResult) throws Exception { - CelOptions celOptions = CelOptions.current().enableUnsignedLongs(true).build(); + CelOptions celOptions = CelOptions.DEFAULT; CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() .setOptions(celOptions) - .addLibraries(CelExtensions.math(celOptions)) + .addLibraries(CelExtensions.math()) .build(); CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder() .setOptions(celOptions) - .addLibraries(CelExtensions.math(celOptions)) + .addLibraries(CelExtensions.math()) .build(); CelAbstractSyntaxTree ast = celCompiler.compile(expr).getAst(); @@ -182,44 +182,7 @@ public void greatest_doubleResult_withUnsignedLongsEnabled_success( } @Test - @TestParameters("{expr: 'math.greatest(5u)', expectedResult: 5}") - @TestParameters("{expr: 'math.greatest(1u, 1.0)', expectedResult: 1}") - @TestParameters("{expr: 'math.greatest(1u, 1)', expectedResult: 1}") - @TestParameters("{expr: 'math.greatest(1u, 1u)', expectedResult: 1}") - @TestParameters("{expr: 'math.greatest(3u, 3.0)', expectedResult: 3}") - @TestParameters("{expr: 'math.greatest(9u, 10u)', expectedResult: 10}") - @TestParameters("{expr: 'math.greatest(15u, 14u)', expectedResult: 15}") - @TestParameters( - "{expr: 'math.greatest(1, 9223372036854775807u)', expectedResult: 9223372036854775807}") - @TestParameters( - "{expr: 'math.greatest(9223372036854775807u, 1)', expectedResult: 9223372036854775807}") - @TestParameters("{expr: 'math.greatest(1u, 1, 1)', expectedResult: 1}") - @TestParameters("{expr: 'math.greatest(3u, 1u, 10u)', expectedResult: 10}") - @TestParameters("{expr: 'math.greatest(1u, 5u, 2u)', expectedResult: 5}") - @TestParameters("{expr: 'math.greatest(-1, 1u, 0u)', expectedResult: 1}") - @TestParameters("{expr: 'math.greatest(dyn(1u), 1, 1.0)', expectedResult: 1}") - @TestParameters("{expr: 'math.greatest(5u, 1.0, 3u)', expectedResult: 5}") - @TestParameters("{expr: 'math.greatest(5.4, 10u, 3u, -5.0, 3.5)', expectedResult: 10}") - @TestParameters( - "{expr: 'math.greatest(5.4, 10, 3u, -5.0, 9223372036854775807)', expectedResult:" - + " 9223372036854775807}") - @TestParameters( - "{expr: 'math.greatest(9223372036854775807, 10, 3u, -5.0, 0)', expectedResult:" - + " 9223372036854775807}") - @TestParameters("{expr: 'math.greatest([5.4, 10, 3u, -5.0, 3.5])', expectedResult: 10}") - @TestParameters( - "{expr: 'math.greatest([dyn(5.4), dyn(10), dyn(3u), dyn(-5.0), dyn(3.5)])', expectedResult:" - + " 10}") - public void greatest_unsignedLongResult_withSignedLongType_success( - String expr, long expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - - Object result = CEL_RUNTIME.createProgram(ast).eval(); - - assertThat(result).isEqualTo(expectedResult); - } - - @Test + @TestParameters("{expr: 'math.greatest(5u)', expectedResult: '5'}") @TestParameters( "{expr: 'math.greatest(18446744073709551615u)', expectedResult: '18446744073709551615'}") @TestParameters("{expr: 'math.greatest(1u, 1.0)', expectedResult: '1'}") @@ -251,16 +214,16 @@ public void greatest_unsignedLongResult_withSignedLongType_success( + " '10'}") public void greatest_unsignedLongResult_withUnsignedLongType_success( String expr, String expectedResult) throws Exception { - CelOptions celOptions = CelOptions.current().enableUnsignedLongs(true).build(); + CelOptions celOptions = CelOptions.DEFAULT; CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() .setOptions(celOptions) - .addLibraries(CelExtensions.math(celOptions)) + .addLibraries(CelExtensions.math()) .build(); CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder() .setOptions(celOptions) - .addLibraries(CelExtensions.math(celOptions)) + .addLibraries(CelExtensions.math()) .build(); CelAbstractSyntaxTree ast = celCompiler.compile(expr).getAst(); @@ -271,9 +234,9 @@ public void greatest_unsignedLongResult_withUnsignedLongType_success( @Test public void greatest_noArgs_throwsCompilationException() { + Assume.assumeFalse(isParseOnly); CelValidationException e = - assertThrows( - CelValidationException.class, () -> CEL_COMPILER.compile("math.greatest()").getAst()); + assertThrows(CelValidationException.class, () -> cel.compile("math.greatest()").getAst()); assertThat(e).hasMessageThat().contains("math.greatest() requires at least one argument"); } @@ -283,8 +246,9 @@ public void greatest_noArgs_throwsCompilationException() { @TestParameters("{expr: 'math.greatest({})'}") @TestParameters("{expr: 'math.greatest([])'}") public void greatest_invalidSingleArg_throwsCompilationException(String expr) { + Assume.assumeFalse(isParseOnly); CelValidationException e = - assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); assertThat(e).hasMessageThat().contains("math.greatest() invalid single argument value"); } @@ -297,8 +261,9 @@ public void greatest_invalidSingleArg_throwsCompilationException(String expr) { @TestParameters("{expr: 'math.greatest([1, {}, 2])'}") @TestParameters("{expr: 'math.greatest([1, [], 2])'}") public void greatest_invalidArgs_throwsCompilationException(String expr) { + Assume.assumeFalse(isParseOnly); CelValidationException e = - assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); assertThat(e) .hasMessageThat() @@ -312,19 +277,16 @@ public void greatest_invalidArgs_throwsCompilationException(String expr) { @TestParameters("{expr: 'math.greatest([1, dyn({}), 2])'}") @TestParameters("{expr: 'math.greatest([1, dyn([]), 2])'}") public void greatest_invalidDynArgs_throwsRuntimeException(String expr) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - - CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> CEL_RUNTIME.createProgram(ast).eval()); + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> eval(expr)); - assertThat(e).hasMessageThat().contains("Function 'math_@max_list_dyn' failed with arg(s)"); + assertThat(e).hasMessageThat().contains("failed with arg(s)"); } @Test public void greatest_listVariableIsEmpty_throwsRuntimeException() throws Exception { CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() - .addLibraries(CelExtensions.math(CEL_OPTIONS)) + .addLibraries(CelExtensions.math()) .addVar("listVar", ListType.create(SimpleType.INT)) .build(); CelAbstractSyntaxTree ast = celCompiler.compile("math.greatest(listVar)").getAst(); @@ -332,12 +294,9 @@ public void greatest_listVariableIsEmpty_throwsRuntimeException() throws Excepti CelEvaluationException e = assertThrows( CelEvaluationException.class, - () -> - CEL_RUNTIME - .createProgram(ast) - .eval(ImmutableMap.of("listVar", ImmutableList.of()))); + () -> cel.createProgram(ast).eval(ImmutableMap.of("listVar", ImmutableList.of()))); - assertThat(e).hasMessageThat().contains("Function 'math_@max_list_dyn' failed with arg(s)"); + assertThat(e).hasMessageThat().contains("failed with arg(s)"); assertThat(e) .hasCauseThat() .hasMessageThat() @@ -347,25 +306,25 @@ public void greatest_listVariableIsEmpty_throwsRuntimeException() throws Excepti @Test @TestParameters("{expr: '100.greatest(1) == 1'}") @TestParameters("{expr: 'dyn(100).greatest(1) == 1'}") - public void greatest_nonProtoNamespace_success(String expr) throws Exception { - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder() - .addLibraries(CelExtensions.math(CEL_OPTIONS)) + public void greatest_nonMathNamespace_success(String expr) throws Exception { + Cel cel = + runtimeFlavor + .builder() + .addCompilerLibraries(CelExtensions.math()) + .addRuntimeLibraries(CelExtensions.math()) .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( "greatest", CelOverloadDecl.newMemberOverload( "int_greatest_int", SimpleType.INT, SimpleType.INT, SimpleType.INT))) - .build(); - CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() .addFunctionBindings( - CelFunctionBinding.from( - "int_greatest_int", Long.class, Long.class, (arg1, arg2) -> arg2)) + CelFunctionBinding.fromOverloads( + "greatest", + CelFunctionBinding.from( + "int_greatest_int", Long.class, Long.class, (arg1, arg2) -> arg2))) .build(); - CelAbstractSyntaxTree ast = celCompiler.compile(expr).getAst(); - boolean result = (boolean) celRuntime.createProgram(ast).eval(); + boolean result = (boolean) eval(cel, expr); assertThat(result).isTrue(); } @@ -400,13 +359,14 @@ public void greatest_nonProtoNamespace_success(String expr) throws Exception { "{expr: 'math.least(-9223372036854775808, 10, 3u, -5.0, 0)', expectedResult:" + " -9223372036854775808}") @TestParameters("{expr: 'math.least([5.4, -10, 3u, -5.0, 3.5])', expectedResult: -10}") + @TestParameters("{expr: 'math.least(1, 9223372036854775807u)', expectedResult: 1}") + @TestParameters("{expr: 'math.least(9223372036854775807u, 1)', expectedResult: 1}") + @TestParameters("{expr: 'math.least(9223372036854775807, 10, 3u, 5.0, 0)', expectedResult: 0}") @TestParameters( "{expr: 'math.least([dyn(5.4), dyn(-10), dyn(3u), dyn(-5.0), dyn(3.5)])', expectedResult:" + " -10}") public void least_intResult_success(String expr, long expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = eval(expr); assertThat(result).isEqualTo(expectedResult); } @@ -443,9 +403,7 @@ public void least_intResult_success(String expr, long expectedResult) throws Exc "{expr: 'math.least([dyn(5.4), dyn(10.0), dyn(3u), dyn(-5.0), dyn(3.5)])', expectedResult:" + " -5.0}") public void least_doubleResult_success(String expr, double expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = eval(expr); assertThat(result).isEqualTo(expectedResult); } @@ -474,12 +432,12 @@ public void least_doubleResult_withUnsignedLongsEnabled_success( CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() .setOptions(celOptions) - .addLibraries(CelExtensions.math(celOptions)) + .addLibraries(CelExtensions.math()) .build(); CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder() .setOptions(celOptions) - .addLibraries(CelExtensions.math(celOptions)) + .addLibraries(CelExtensions.math()) .build(); CelAbstractSyntaxTree ast = celCompiler.compile(expr).getAst(); @@ -489,37 +447,15 @@ public void least_doubleResult_withUnsignedLongsEnabled_success( } @Test - @TestParameters("{expr: 'math.least(5u)', expectedResult: 5}") - @TestParameters("{expr: 'math.least(1u, 1.0)', expectedResult: 1}") - @TestParameters("{expr: 'math.least(1u, 1)', expectedResult: 1}") - @TestParameters("{expr: 'math.least(1u, 1u)', expectedResult: 1}") - @TestParameters("{expr: 'math.least(3u, 3.0)', expectedResult: 3}") - @TestParameters("{expr: 'math.least(9u, 10u)', expectedResult: 9}") - @TestParameters("{expr: 'math.least(15u, 14u)', expectedResult: 14}") - @TestParameters("{expr: 'math.least(1, 9223372036854775807u)', expectedResult: 1}") - @TestParameters("{expr: 'math.least(9223372036854775807u, 1)', expectedResult: 1}") - @TestParameters("{expr: 'math.least(1u, 1, 1)', expectedResult: 1}") - @TestParameters("{expr: 'math.least(3u, 1u, 10u)', expectedResult: 1}") - @TestParameters("{expr: 'math.least(1u, 5u, 2u)', expectedResult: 1}") - @TestParameters("{expr: 'math.least(9, 1u, 0u)', expectedResult: 0}") - @TestParameters("{expr: 'math.least(dyn(1u), 1, 1.0)', expectedResult: 1}") - @TestParameters("{expr: 'math.least(5.0, 1u, 3u)', expectedResult: 1}") - @TestParameters("{expr: 'math.least(5.4, 1u, 3u, 9, 3.5)', expectedResult: 1}") - @TestParameters("{expr: 'math.least(5.4, 10, 3u, 5.0, 9223372036854775807)', expectedResult: 3}") - @TestParameters("{expr: 'math.least(9223372036854775807, 10, 3u, 5.0, 0)', expectedResult: 0}") - @TestParameters("{expr: 'math.least([5.4, 10, 3u, 5.0, 3.5])', expectedResult: 3}") + @TestParameters("{expr: 'math.least(9, 1u, 0u)', expectedResult: '0'}") + @TestParameters("{expr: 'math.least(dyn(1u), 1, 1.0)', expectedResult: '1'}") + @TestParameters("{expr: 'math.least(5.0, 1u, 3u)', expectedResult: '1'}") + @TestParameters("{expr: 'math.least(5.4, 1u, 3u, 9, 3.5)', expectedResult: '1'}") @TestParameters( - "{expr: 'math.least([dyn(5.4), dyn(10), dyn(3u), dyn(5.0), dyn(3.5)])', expectedResult: 3}") - public void least_unsignedLongResult_withSignedLongType_success(String expr, long expectedResult) - throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - - Object result = CEL_RUNTIME.createProgram(ast).eval(); - - assertThat(result).isEqualTo(expectedResult); - } - - @Test + "{expr: 'math.least(5.4, 10, 3u, 5.0, 9223372036854775807)', expectedResult: '3'}") + @TestParameters("{expr: 'math.least([5.4, 10, 3u, 5.0, 3.5])', expectedResult: '3'}") + @TestParameters( + "{expr: 'math.least([dyn(5.4), dyn(10), dyn(3u), dyn(5.0), dyn(3.5)])', expectedResult: '3'}") @TestParameters( "{expr: 'math.least(18446744073709551615u)', expectedResult: '18446744073709551615'}") @TestParameters("{expr: 'math.least(1u, 1.0)', expectedResult: '1'}") @@ -553,12 +489,12 @@ public void least_unsignedLongResult_withUnsignedLongType_success( CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() .setOptions(celOptions) - .addLibraries(CelExtensions.math(celOptions)) + .addLibraries(CelExtensions.math()) .build(); CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder() .setOptions(celOptions) - .addLibraries(CelExtensions.math(celOptions)) + .addLibraries(CelExtensions.math()) .build(); CelAbstractSyntaxTree ast = celCompiler.compile(expr).getAst(); @@ -569,9 +505,9 @@ public void least_unsignedLongResult_withUnsignedLongType_success( @Test public void least_noArgs_throwsCompilationException() { + Assume.assumeFalse(isParseOnly); CelValidationException e = - assertThrows( - CelValidationException.class, () -> CEL_COMPILER.compile("math.least()").getAst()); + assertThrows(CelValidationException.class, () -> cel.compile("math.least()").getAst()); assertThat(e).hasMessageThat().contains("math.least() requires at least one argument"); } @@ -581,8 +517,9 @@ public void least_noArgs_throwsCompilationException() { @TestParameters("{expr: 'math.least({})'}") @TestParameters("{expr: 'math.least([])'}") public void least_invalidSingleArg_throwsCompilationException(String expr) { + Assume.assumeFalse(isParseOnly); CelValidationException e = - assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); assertThat(e).hasMessageThat().contains("math.least() invalid single argument value"); } @@ -595,8 +532,9 @@ public void least_invalidSingleArg_throwsCompilationException(String expr) { @TestParameters("{expr: 'math.least([1, {}, 2])'}") @TestParameters("{expr: 'math.least([1, [], 2])'}") public void least_invalidArgs_throwsCompilationException(String expr) { + Assume.assumeFalse(isParseOnly); CelValidationException e = - assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); assertThat(e) .hasMessageThat() @@ -610,19 +548,16 @@ public void least_invalidArgs_throwsCompilationException(String expr) { @TestParameters("{expr: 'math.least([1, dyn({}), 2])'}") @TestParameters("{expr: 'math.least([1, dyn([]), 2])'}") public void least_invalidDynArgs_throwsRuntimeException(String expr) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - - CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> CEL_RUNTIME.createProgram(ast).eval()); + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> eval(expr)); - assertThat(e).hasMessageThat().contains("Function 'math_@min_list_dyn' failed with arg(s)"); + assertThat(e).hasMessageThat().contains("failed with arg(s)"); } @Test public void least_listVariableIsEmpty_throwsRuntimeException() throws Exception { CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder() - .addLibraries(CelExtensions.math(CEL_OPTIONS)) + .addLibraries(CelExtensions.math()) .addVar("listVar", ListType.create(SimpleType.INT)) .build(); CelAbstractSyntaxTree ast = celCompiler.compile("math.least(listVar)").getAst(); @@ -630,12 +565,9 @@ public void least_listVariableIsEmpty_throwsRuntimeException() throws Exception CelEvaluationException e = assertThrows( CelEvaluationException.class, - () -> - CEL_RUNTIME - .createProgram(ast) - .eval(ImmutableMap.of("listVar", ImmutableList.of()))); + () -> cel.createProgram(ast).eval(ImmutableMap.of("listVar", ImmutableList.of()))); - assertThat(e).hasMessageThat().contains("Function 'math_@min_list_dyn' failed with arg(s)"); + assertThat(e).hasMessageThat().contains("failed with arg(s)"); assertThat(e) .hasCauseThat() .hasMessageThat() @@ -645,24 +577,25 @@ public void least_listVariableIsEmpty_throwsRuntimeException() throws Exception @Test @TestParameters("{expr: '100.least(1) == 1'}") @TestParameters("{expr: 'dyn(100).least(1) == 1'}") - public void least_nonProtoNamespace_success(String expr) throws Exception { - CelCompiler celCompiler = - CelCompilerFactory.standardCelCompilerBuilder() - .addLibraries(CelExtensions.math(CEL_OPTIONS)) + public void least_nonMathNamespace_success(String expr) throws Exception { + Cel cel = + runtimeFlavor + .builder() + .addCompilerLibraries(CelExtensions.math()) + .addRuntimeLibraries(CelExtensions.math()) .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( "least", CelOverloadDecl.newMemberOverload( "int_least", SimpleType.INT, SimpleType.INT, SimpleType.INT))) - .build(); - CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() .addFunctionBindings( - CelFunctionBinding.from("int_least", Long.class, Long.class, (arg1, arg2) -> arg2)) + CelFunctionBinding.fromOverloads( + "least", + CelFunctionBinding.from( + "int_least", Long.class, Long.class, (arg1, arg2) -> arg2))) .build(); - CelAbstractSyntaxTree ast = celCompiler.compile(expr).getAst(); - boolean result = (boolean) celRuntime.createProgram(ast).eval(); + boolean result = (boolean) eval(cel, expr); assertThat(result).isTrue(); } @@ -676,9 +609,9 @@ public void least_nonProtoNamespace_success(String expr) throws Exception { @TestParameters("{expr: 'math.isNaN(math.sign(0.0/0.0))', expectedResult: true}") @TestParameters("{expr: 'math.isNaN(math.sqrt(-4))', expectedResult: true}") public void isNaN_success(String expr, boolean expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -690,7 +623,7 @@ public void isNaN_success(String expr, boolean expectedResult) throws Exception @TestParameters("{expr: 'math.isNaN(1u)'}") public void isNaN_invalidArgs_throwsException(String expr) { CelValidationException e = - assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); assertThat(e).hasMessageThat().contains("found no matching overload for 'math.isNaN'"); } @@ -701,9 +634,9 @@ public void isNaN_invalidArgs_throwsException(String expr) { @TestParameters("{expr: 'math.isFinite(1.0/0.0)', expectedResult: false}") @TestParameters("{expr: 'math.isFinite(0.0/0.0)', expectedResult: false}") public void isFinite_success(String expr, boolean expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -715,7 +648,7 @@ public void isFinite_success(String expr, boolean expectedResult) throws Excepti @TestParameters("{expr: 'math.isFinite(1u)'}") public void isFinite_invalidArgs_throwsException(String expr) { CelValidationException e = - assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); assertThat(e).hasMessageThat().contains("found no matching overload for 'math.isFinite'"); } @@ -726,9 +659,9 @@ public void isFinite_invalidArgs_throwsException(String expr) { @TestParameters("{expr: 'math.isInf(0.0/0.0)', expectedResult: false}") @TestParameters("{expr: 'math.isInf(10.0)', expectedResult: false}") public void isInf_success(String expr, boolean expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -740,7 +673,7 @@ public void isInf_success(String expr, boolean expectedResult) throws Exception @TestParameters("{expr: 'math.isInf(1u)'}") public void isInf_invalidArgs_throwsException(String expr) { CelValidationException e = - assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); assertThat(e).hasMessageThat().contains("found no matching overload for 'math.isInf'"); } @@ -752,9 +685,9 @@ public void isInf_invalidArgs_throwsException(String expr) { @TestParameters("{expr: 'math.ceil(20.0)' , expectedResult: 20.0}") @TestParameters("{expr: 'math.ceil(0.0/0.0)' , expectedResult: NaN}") public void ceil_success(String expr, double expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -766,7 +699,7 @@ public void ceil_success(String expr, double expectedResult) throws Exception { @TestParameters("{expr: 'math.ceil(1u)'}") public void ceil_invalidArgs_throwsException(String expr) { CelValidationException e = - assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); assertThat(e).hasMessageThat().contains("found no matching overload for 'math.ceil'"); } @@ -777,9 +710,9 @@ public void ceil_invalidArgs_throwsException(String expr) { @TestParameters("{expr: 'math.floor(0.0/0.0)' , expectedResult: NaN}") @TestParameters("{expr: 'math.floor(50.0)' , expectedResult: 50.0}") public void floor_success(String expr, double expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -791,7 +724,7 @@ public void floor_success(String expr, double expectedResult) throws Exception { @TestParameters("{expr: 'math.floor(1u)'}") public void floor_invalidArgs_throwsException(String expr) { CelValidationException e = - assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); assertThat(e).hasMessageThat().contains("found no matching overload for 'math.floor'"); } @@ -806,9 +739,9 @@ public void floor_invalidArgs_throwsException(String expr) { @TestParameters("{expr: 'math.round(1.0/0.0)' , expectedResult: Infinity}") @TestParameters("{expr: 'math.round(-1.0/0.0)' , expectedResult: -Infinity}") public void round_success(String expr, double expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -820,7 +753,7 @@ public void round_success(String expr, double expectedResult) throws Exception { @TestParameters("{expr: 'math.round(1u)'}") public void round_invalidArgs_throwsException(String expr) { CelValidationException e = - assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); assertThat(e).hasMessageThat().contains("found no matching overload for 'math.round'"); } @@ -832,9 +765,9 @@ public void round_invalidArgs_throwsException(String expr) { @TestParameters("{expr: 'math.trunc(1.0/0.0)' , expectedResult: Infinity}") @TestParameters("{expr: 'math.trunc(-1.0/0.0)' , expectedResult: -Infinity}") public void trunc_success(String expr, double expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -846,7 +779,7 @@ public void trunc_success(String expr, double expectedResult) throws Exception { @TestParameters("{expr: 'math.trunc(1u)'}") public void trunc_invalidArgs_throwsException(String expr) { CelValidationException e = - assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); assertThat(e).hasMessageThat().contains("found no matching overload for 'math.trunc'"); } @@ -856,9 +789,9 @@ public void trunc_invalidArgs_throwsException(String expr) { @TestParameters("{expr: 'math.abs(-1657643)', expectedResult: 1657643}") @TestParameters("{expr: 'math.abs(-2147483648)', expectedResult: 2147483648}") public void abs_intResult_success(String expr, long expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -871,9 +804,9 @@ public void abs_intResult_success(String expr, long expectedResult) throws Excep @TestParameters("{expr: 'math.abs(1.0/0.0)' , expectedResult: Infinity}") @TestParameters("{expr: 'math.abs(-1.0/0.0)' , expectedResult: Infinity}") public void abs_doubleResult_success(String expr, double expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -883,7 +816,7 @@ public void abs_overflow_throwsException() { CelValidationException e = assertThrows( CelValidationException.class, - () -> CEL_COMPILER.compile("math.abs(-9223372036854775809)").getAst()); + () -> cel.compile("math.abs(-9223372036854775809)").getAst()); assertThat(e) .hasMessageThat() @@ -896,9 +829,9 @@ public void abs_overflow_throwsException() { @TestParameters("{expr: 'math.sign(-0)', expectedResult: 0}") @TestParameters("{expr: 'math.sign(11213)', expectedResult: 1}") public void sign_intResult_success(String expr, int expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -914,9 +847,9 @@ public void sign_intResult_success(String expr, int expectedResult) throws Excep @TestParameters("{expr: 'math.sign(1.0/0.0)' , expectedResult: 1.0}") @TestParameters("{expr: 'math.sign(-1.0/0.0)' , expectedResult: -1.0}") public void sign_doubleResult_success(String expr, double expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -926,7 +859,7 @@ public void sign_doubleResult_success(String expr, double expectedResult) throws @TestParameters("{expr: 'math.sign(\"\")'}") public void sign_invalidArgs_throwsException(String expr) { CelValidationException e = - assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); assertThat(e).hasMessageThat().contains("found no matching overload for 'math.sign'"); } @@ -938,9 +871,9 @@ public void sign_invalidArgs_throwsException(String expr) { "{expr: 'math.bitAnd(9223372036854775807,9223372036854775807)' , expectedResult:" + " 9223372036854775807}") public void bitAnd_signedInt_success(String expr, long expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -950,9 +883,9 @@ public void bitAnd_signedInt_success(String expr, long expectedResult) throws Ex @TestParameters("{expr: 'math.bitAnd(1u,3u)' , expectedResult: 1}") public void bitAnd_unSignedInt_success(String expr, UnsignedLong expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_UNSIGNED_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_UNSIGNED_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -963,7 +896,7 @@ public void bitAnd_unSignedInt_success(String expr, UnsignedLong expectedResult) @TestParameters("{expr: 'math.bitAnd(1)'}") public void bitAnd_invalidArgs_throwsException(String expr) { CelValidationException e = - assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); assertThat(e).hasMessageThat().contains("found no matching overload for 'math.bitAnd'"); } @@ -973,10 +906,7 @@ public void bitAnd_maxValArg_throwsException() { CelValidationException e = assertThrows( CelValidationException.class, - () -> - CEL_COMPILER - .compile("math.bitAnd(9223372036854775807,9223372036854775809)") - .getAst()); + () -> cel.compile("math.bitAnd(9223372036854775807,9223372036854775809)").getAst()); assertThat(e) .hasMessageThat() @@ -987,9 +917,9 @@ public void bitAnd_maxValArg_throwsException() { @TestParameters("{expr: 'math.bitOr(1,2)' , expectedResult: 3}") @TestParameters("{expr: 'math.bitOr(1,-1)' , expectedResult: -1}") public void bitOr_signedInt_success(String expr, long expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -998,9 +928,9 @@ public void bitOr_signedInt_success(String expr, long expectedResult) throws Exc @TestParameters("{expr: 'math.bitOr(1u,2u)' , expectedResult: 3}") @TestParameters("{expr: 'math.bitOr(1090u,3u)' , expectedResult: 1091}") public void bitOr_unSignedInt_success(String expr, UnsignedLong expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_UNSIGNED_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_UNSIGNED_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -1011,7 +941,7 @@ public void bitOr_unSignedInt_success(String expr, UnsignedLong expectedResult) @TestParameters("{expr: 'math.bitOr(1)'}") public void bitOr_invalidArgs_throwsException(String expr) { CelValidationException e = - assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); assertThat(e).hasMessageThat().contains("found no matching overload for 'math.bitOr'"); } @@ -1020,9 +950,9 @@ public void bitOr_invalidArgs_throwsException(String expr) { @TestParameters("{expr: 'math.bitXor(1,2)' , expectedResult: 3}") @TestParameters("{expr: 'math.bitXor(3,5)' , expectedResult: 6}") public void bitXor_signedInt_success(String expr, long expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -1032,9 +962,9 @@ public void bitXor_signedInt_success(String expr, long expectedResult) throws Ex @TestParameters("{expr: 'math.bitXor(3u, 5u)' , expectedResult: 6}") public void bitXor_unSignedInt_success(String expr, UnsignedLong expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_UNSIGNED_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_UNSIGNED_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -1045,7 +975,7 @@ public void bitXor_unSignedInt_success(String expr, UnsignedLong expectedResult) @TestParameters("{expr: 'math.bitXor(1)'}") public void bitXor_invalidArgs_throwsException(String expr) { CelValidationException e = - assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); assertThat(e).hasMessageThat().contains("found no matching overload for 'math.bitXor'"); } @@ -1055,9 +985,9 @@ public void bitXor_invalidArgs_throwsException(String expr) { @TestParameters("{expr: 'math.bitNot(0)' , expectedResult: -1}") @TestParameters("{expr: 'math.bitNot(-1)' , expectedResult: 0}") public void bitNot_signedInt_success(String expr, long expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -1067,9 +997,9 @@ public void bitNot_signedInt_success(String expr, long expectedResult) throws Ex @TestParameters("{expr: 'math.bitNot(12310u)' , expectedResult: 18446744073709539305}") public void bitNot_unSignedInt_success(String expr, UnsignedLong expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_UNSIGNED_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_UNSIGNED_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -1080,7 +1010,7 @@ public void bitNot_unSignedInt_success(String expr, UnsignedLong expectedResult) @TestParameters("{expr: 'math.bitNot(\"\")'}") public void bitNot_invalidArgs_throwsException(String expr) { CelValidationException e = - assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).getAst()); + assertThrows(CelValidationException.class, () -> cel.compile(expr).getAst()); assertThat(e).hasMessageThat().contains("found no matching overload for 'math.bitNot'"); } @@ -1090,9 +1020,9 @@ public void bitNot_invalidArgs_throwsException(String expr) { @TestParameters("{expr: 'math.bitShiftLeft(12121, 11)' , expectedResult: 24823808}") @TestParameters("{expr: 'math.bitShiftLeft(-1, 64)' , expectedResult: 0}") public void bitShiftLeft_signedInt_success(String expr, long expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -1103,9 +1033,9 @@ public void bitShiftLeft_signedInt_success(String expr, long expectedResult) thr @TestParameters("{expr: 'math.bitShiftLeft(1u, 65)' , expectedResult: 0}") public void bitShiftLeft_unSignedInt_success(String expr, UnsignedLong expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_UNSIGNED_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_UNSIGNED_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -1114,11 +1044,10 @@ public void bitShiftLeft_unSignedInt_success(String expr, UnsignedLong expectedR @TestParameters("{expr: 'math.bitShiftLeft(1, -2)'}") @TestParameters("{expr: 'math.bitShiftLeft(1u, -2)'}") public void bitShiftLeft_invalidArgs_throwsException(String expr) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); CelEvaluationException e = - assertThrows( - CelEvaluationException.class, () -> CEL_UNSIGNED_RUNTIME.createProgram(ast).eval()); + assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); assertThat(e).hasMessageThat().contains("evaluation error"); assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); @@ -1131,9 +1060,9 @@ public void bitShiftLeft_invalidArgs_throwsException(String expr) throws Excepti @TestParameters("{expr: 'math.bitShiftRight(12121, 11)' , expectedResult: 5}") @TestParameters("{expr: 'math.bitShiftRight(-1, 64)' , expectedResult: 0}") public void bitShiftRight_signedInt_success(String expr, long expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); - Object result = CEL_RUNTIME.createProgram(ast).eval(); + Object result = cel.createProgram(ast).eval(); assertThat(result).isEqualTo(expectedResult); } @@ -1144,9 +1073,7 @@ public void bitShiftRight_signedInt_success(String expr, long expectedResult) th @TestParameters("{expr: 'math.bitShiftRight(1u, 65)' , expectedResult: 0}") public void bitShiftRight_unSignedInt_success(String expr, UnsignedLong expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_UNSIGNED_COMPILER.compile(expr).getAst(); - - Object result = CEL_UNSIGNED_RUNTIME.createProgram(ast).eval(); + Object result = eval(expr); assertThat(result).isEqualTo(expectedResult); } @@ -1155,11 +1082,7 @@ public void bitShiftRight_unSignedInt_success(String expr, UnsignedLong expected @TestParameters("{expr: 'math.bitShiftRight(23111u, -212)'}") @TestParameters("{expr: 'math.bitShiftRight(23, -212)'}") public void bitShiftRight_invalidArgs_throwsException(String expr) throws Exception { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expr).getAst(); - - CelEvaluationException e = - assertThrows( - CelEvaluationException.class, () -> CEL_UNSIGNED_RUNTIME.createProgram(ast).eval()); + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> eval(expr)); assertThat(e).hasMessageThat().contains("evaluation error"); assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); @@ -1174,10 +1097,26 @@ public void bitShiftRight_invalidArgs_throwsException(String expr) throws Except @TestParameters("{expr: 'math.sqrt(1.0/0.0)', expectedResult: Infinity}") @TestParameters("{expr: 'math.sqrt(-1)', expectedResult: NaN}") public void sqrt_success(String expr, double expectedResult) throws Exception { - CelAbstractSyntaxTree ast = CEL_UNSIGNED_COMPILER.compile(expr).getAst(); - - Object result = CEL_UNSIGNED_RUNTIME.createProgram(ast).eval(); + Object result = eval(expr); assertThat(result).isEqualTo(expectedResult); } + + private Object eval(Cel cel, String expression, Map variables) throws Exception { + CelAbstractSyntaxTree ast; + if (isParseOnly) { + ast = cel.parse(expression).getAst(); + } else { + ast = cel.compile(expression).getAst(); + } + return cel.createProgram(ast).eval(variables); + } + + private Object eval(Cel celInstance, String expression) throws Exception { + return eval(celInstance, expression, ImmutableMap.of()); + } + + private Object eval(String expression) throws Exception { + return eval(this.cel, expression, ImmutableMap.of()); + } } From e7dff9b38fc6f5bb2665f0cd4fb7e27a14672bc4 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 22 Apr 2026 14:44:11 -0700 Subject: [PATCH 056/204] Create a base class for extension tests PiperOrigin-RevId: 904054337 --- .../main/java/dev/cel/extensions/BUILD.bazel | 1 + .../test/java/dev/cel/extensions/BUILD.bazel | 1 + .../extensions/CelBindingsExtensionsTest.java | 51 ++------- .../CelComprehensionsExtensionsTest.java | 55 +++------- .../extensions/CelEncoderExtensionsTest.java | 39 ++----- .../cel/extensions/CelExtensionTestBase.java | 66 ++++++++++++ .../extensions/CelListsExtensionsTest.java | 46 +++----- .../extensions/CelProtoExtensionsTest.java | 44 +++----- .../extensions/CelRegexExtensionsTest.java | 36 ++----- .../cel/extensions/CelSetsExtensionsTest.java | 85 +++++---------- .../extensions/CelStringExtensionsTest.java | 102 ++++++------------ 11 files changed, 197 insertions(+), 329 deletions(-) create mode 100644 extensions/src/test/java/dev/cel/extensions/CelExtensionTestBase.java diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel index f8e4bfc8c..454b2a2fd 100644 --- a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel @@ -122,6 +122,7 @@ java_library( ":extension_library", "//checker:checker_builder", "//common:compiler_common", + "//common:options", "//common/ast", "//common/exceptions:numeric_overflow", "//common/internal:comparison_functions", diff --git a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel index 19fd3657e..eed240317 100644 --- a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel @@ -12,6 +12,7 @@ java_library( "//bundle:cel", "//bundle:cel_experimental_factory", "//common:cel_ast", + "//common:cel_exception", "//common:compiler_common", "//common:container", "//common:options", diff --git a/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java index b87967d0e..00fcad473 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java @@ -23,7 +23,6 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.bundle.Cel; -import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; import dev.cel.common.CelOverloadDecl; @@ -36,36 +35,24 @@ import dev.cel.parser.CelStandardMacro; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; -import dev.cel.testing.CelRuntimeFlavor; import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Assume; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) -public final class CelBindingsExtensionsTest { - - @TestParameter public CelRuntimeFlavor runtimeFlavor; - @TestParameter public boolean isParseOnly; - - private Cel cel; - - @Before - public void setUp() { - // Legacy runtime does not support parsed-only evaluation mode. - Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.LEGACY) && isParseOnly); - cel = - runtimeFlavor - .builder() - .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .addCompilerLibraries(CelOptionalLibrary.INSTANCE, CelExtensions.bindings()) - .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) - .build(); +public final class CelBindingsExtensionsTest extends CelExtensionTestBase { + + @Override + protected Cel newCelEnv() { + return runtimeFlavor + .builder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addCompilerLibraries(CelOptionalLibrary.INSTANCE, CelExtensions.bindings()) + .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) + .build(); } @Test @@ -331,21 +318,5 @@ public void lazyBinding_boundAttributeInNestedComprehension() throws Exception { assertThat(invocation.get()).isEqualTo(1); } - private Object eval(Cel cel, String expression) throws Exception { - return eval(cel, expression, ImmutableMap.of()); - } - - private Object eval(Cel cel, String expression, Map variables) throws Exception { - CelAbstractSyntaxTree ast; - if (isParseOnly) { - ast = cel.parse(expression).getAst(); - } else { - ast = cel.compile(expression).getAst(); - } - return cel.createProgram(ast).eval(variables); - } - private Object eval(String expression) throws Exception { - return eval(this.cel, expression, ImmutableMap.of()); - } } diff --git a/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java index 207178cfe..42dc3e07d 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java @@ -19,7 +19,6 @@ import static org.junit.Assert.assertThrows; import com.google.common.base.Throwables; -import com.google.common.collect.ImmutableMap; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; @@ -40,15 +39,13 @@ import dev.cel.parser.CelUnparserFactory; import dev.cel.runtime.CelEvaluationException; import dev.cel.testing.CelRuntimeFlavor; -import java.util.Map; import org.junit.Assume; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; /** Test for {@link CelExtensions#comprehensions()} */ @RunWith(TestParameterInjector.class) -public class CelComprehensionsExtensionsTest { +public class CelComprehensionsExtensionsTest extends CelExtensionTestBase { private static final CelOptions CEL_OPTIONS = CelOptions.current() @@ -57,29 +54,21 @@ public class CelComprehensionsExtensionsTest { .populateMacroCalls(true) .build(); - @TestParameter public CelRuntimeFlavor runtimeFlavor; - @TestParameter public boolean isParseOnly; - - private Cel cel; - - @Before - public void setUp() { - // Legacy runtime does not support parsed-only evaluation mode. - Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.LEGACY) && isParseOnly); - this.cel = - runtimeFlavor - .builder() - .setOptions(CEL_OPTIONS) - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .addCompilerLibraries(CelExtensions.comprehensions()) - .addCompilerLibraries(CelExtensions.lists()) - .addCompilerLibraries(CelExtensions.strings()) - .addCompilerLibraries(CelOptionalLibrary.INSTANCE, CelExtensions.bindings()) - .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) - .addRuntimeLibraries(CelExtensions.lists()) - .addRuntimeLibraries(CelExtensions.strings()) - .addRuntimeLibraries(CelExtensions.comprehensions()) - .build(); + @Override + protected Cel newCelEnv() { + return runtimeFlavor + .builder() + .setOptions(CEL_OPTIONS) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addCompilerLibraries(CelExtensions.comprehensions()) + .addCompilerLibraries(CelExtensions.lists()) + .addCompilerLibraries(CelExtensions.strings()) + .addCompilerLibraries(CelOptionalLibrary.INSTANCE, CelExtensions.bindings()) + .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) + .addRuntimeLibraries(CelExtensions.lists()) + .addRuntimeLibraries(CelExtensions.strings()) + .addRuntimeLibraries(CelExtensions.comprehensions()) + .build(); } private static final CelUnparser UNPARSER = CelUnparserFactory.newUnparser(); @@ -376,17 +365,5 @@ public void mutableMapValue_select_missingKeyException() throws Exception { assertThat(e).hasCauseThat().hasMessageThat().contains("key 'b' is not present in map."); } - private Object eval(String expression) throws Exception { - return eval(this.cel, expression, ImmutableMap.of()); - } - private Object eval(Cel cel, String expression, Map variables) throws Exception { - CelAbstractSyntaxTree ast; - if (isParseOnly) { - ast = cel.parse(expression).getAst(); - } else { - ast = cel.compile(expression).getAst(); - } - return cel.createProgram(ast).eval(variables); - } } diff --git a/extensions/src/test/java/dev/cel/extensions/CelEncoderExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelEncoderExtensionsTest.java index b0a501ddb..afeaa9105 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelEncoderExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelEncoderExtensionsTest.java @@ -19,44 +19,32 @@ import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableMap; -import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.bundle.Cel; -import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; import dev.cel.common.CelValidationException; import dev.cel.common.types.SimpleType; import dev.cel.common.values.CelByteString; import dev.cel.runtime.CelEvaluationException; -import dev.cel.testing.CelRuntimeFlavor; import org.junit.Assume; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) -public class CelEncoderExtensionsTest { +public class CelEncoderExtensionsTest extends CelExtensionTestBase { private static final CelOptions CEL_OPTIONS = CelOptions.current().enableHeterogeneousNumericComparisons(true).build(); - @TestParameter public CelRuntimeFlavor runtimeFlavor; - @TestParameter public boolean isParseOnly; - - private Cel cel; - - @Before - public void setUp() { - // Legacy runtime does not support parsed-only evaluation mode. - Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.LEGACY) && isParseOnly); - this.cel = - runtimeFlavor - .builder() - .setOptions(CEL_OPTIONS) - .addCompilerLibraries(CelExtensions.encoders(CEL_OPTIONS)) - .addRuntimeLibraries(CelExtensions.encoders(CEL_OPTIONS)) - .addVar("stringVar", SimpleType.STRING) - .build(); + @Override + protected Cel newCelEnv() { + return runtimeFlavor + .builder() + .setOptions(CEL_OPTIONS) + .addCompilerLibraries(CelExtensions.encoders(CEL_OPTIONS)) + .addRuntimeLibraries(CelExtensions.encoders(CEL_OPTIONS)) + .addVar("stringVar", SimpleType.STRING) + .build(); } @Test @@ -132,12 +120,5 @@ public void decode_malformedBase64Char_throwsEvaluationException() throws Except assertThat(e).hasCauseThat().hasMessageThat().contains("Illegal base64 character"); } - private Object eval(String expr) throws Exception { - return eval(expr, ImmutableMap.of()); - } - private Object eval(String expr, ImmutableMap vars) throws Exception { - CelAbstractSyntaxTree ast = isParseOnly ? cel.parse(expr).getAst() : cel.compile(expr).getAst(); - return cel.createProgram(ast).eval(vars); - } } diff --git a/extensions/src/test/java/dev/cel/extensions/CelExtensionTestBase.java b/extensions/src/test/java/dev/cel/extensions/CelExtensionTestBase.java new file mode 100644 index 000000000..c80ee38b6 --- /dev/null +++ b/extensions/src/test/java/dev/cel/extensions/CelExtensionTestBase.java @@ -0,0 +1,66 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.extensions; + +import com.google.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.TestParameter; +import dev.cel.bundle.Cel; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelException; +import dev.cel.testing.CelRuntimeFlavor; +import java.util.Map; +import org.junit.Assume; +import org.junit.Before; + +/** + * Abstract base class for extension tests to facilitate executing tests with both legacy and + * planner runtime, along with parsed-only and checked expression evaluations for the planner. + */ +abstract class CelExtensionTestBase { + @TestParameter public CelRuntimeFlavor runtimeFlavor; + @TestParameter public boolean isParseOnly; + + @Before + public void setUpBase() { + // Legacy runtime does not support parsed-only evaluation. + Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.LEGACY) && isParseOnly); + this.cel = newCelEnv(); + } + + protected Cel cel; + + /** + * Subclasses must implement this to provide a Cel instance configured with the specific + * extensions being tested. + */ + protected abstract Cel newCelEnv(); + + protected Object eval(String expr) throws CelException { + return eval(cel, expr, ImmutableMap.of()); + } + + protected Object eval(String expr, Map variables) throws CelException { + return eval(cel, expr, variables); + } + + protected Object eval(Cel cel, String expr) throws CelException { + return eval(cel, expr, ImmutableMap.of()); + } + + protected Object eval(Cel cel, String expr, Map variables) throws CelException { + CelAbstractSyntaxTree ast = isParseOnly ? cel.parse(expr).getAst() : cel.compile(expr).getAst(); + return cel.createProgram(ast).eval(variables); + } +} diff --git a/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java index f36d90e2d..f5536da4e 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java @@ -19,12 +19,9 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedMultiset; import com.google.common.collect.ImmutableSortedSet; -import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.bundle.Cel; -import dev.cel.bundle.CelBuilder; -import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelValidationException; import dev.cel.common.CelValidationResult; @@ -32,25 +29,24 @@ import dev.cel.expr.conformance.test.SimpleTest; import dev.cel.parser.CelStandardMacro; import dev.cel.runtime.CelEvaluationException; -import dev.cel.testing.CelRuntimeFlavor; -import java.util.Map; import org.junit.Assume; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) -public class CelListsExtensionsTest { - @TestParameter public CelRuntimeFlavor runtimeFlavor; - @TestParameter public boolean isParseOnly; +public class CelListsExtensionsTest extends CelExtensionTestBase { - private Cel cel; - - @Before - public void setUp() { - // Legacy runtime does not support parsed-only evaluation mode. - Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.LEGACY) && isParseOnly); - this.cel = setupEnv(runtimeFlavor.builder()); + @Override + protected Cel newCelEnv() { + return runtimeFlavor + .builder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addCompilerLibraries(CelExtensions.lists()) + .addRuntimeLibraries(CelExtensions.lists()) + .setContainer(CelContainer.ofName("cel.expr.conformance.test")) + .addMessageTypes(SimpleTest.getDescriptor()) + .addVar("non_list", SimpleType.DYN) + .build(); } @Test @@ -322,23 +318,5 @@ public void sortBy_throws_evaluationException(String expression, String expected .contains(expectedError); } - private static Cel setupEnv(CelBuilder celBuilder) { - return celBuilder - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .addCompilerLibraries(CelExtensions.lists()) - .addRuntimeLibraries(CelExtensions.lists()) - .setContainer(CelContainer.ofName("cel.expr.conformance.test")) - .addMessageTypes(SimpleTest.getDescriptor()) - .addVar("non_list", SimpleType.DYN) - .build(); - } - - private Object eval(Cel cel, String expr) throws Exception { - return eval(cel, expr, ImmutableMap.of()); - } - private Object eval(Cel cel, String expr, Map vars) throws Exception { - CelAbstractSyntaxTree ast = isParseOnly ? cel.parse(expr).getAst() : cel.compile(expr).getAst(); - return cel.createProgram(ast).eval(vars); - } } diff --git a/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java index 2e55619db..f46ea5b1a 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java @@ -26,7 +26,6 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.bundle.Cel; -import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; @@ -41,34 +40,23 @@ import dev.cel.parser.CelMacro; import dev.cel.parser.CelStandardMacro; import dev.cel.runtime.CelFunctionBinding; -import dev.cel.testing.CelRuntimeFlavor; -import java.util.Map; import org.junit.Assume; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) -public final class CelProtoExtensionsTest { - - @TestParameter public CelRuntimeFlavor runtimeFlavor; - @TestParameter public boolean isParseOnly; - - private Cel cel; - - @Before - public void setUp() { - // Legacy runtime does not support parsed-only evaluation mode. - Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.LEGACY) && isParseOnly); - this.cel = - runtimeFlavor - .builder() - .addCompilerLibraries(CelExtensions.protos()) - .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .addFileTypes(TestAllTypesExtensions.getDescriptor()) - .addVar("msg", StructTypeReference.create("cel.expr.conformance.proto2.TestAllTypes")) - .setContainer(CelContainer.ofName("cel.expr.conformance.proto2")) - .build(); +public final class CelProtoExtensionsTest extends CelExtensionTestBase { + + @Override + protected Cel newCelEnv() { + return runtimeFlavor + .builder() + .addCompilerLibraries(CelExtensions.protos()) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addFileTypes(TestAllTypesExtensions.getDescriptor()) + .addVar("msg", StructTypeReference.create("cel.expr.conformance.proto2.TestAllTypes")) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto2")) + .build(); } private static final TestAllTypes PACKAGE_SCOPED_EXT_MSG = @@ -342,13 +330,5 @@ public void parseErrors(@TestParameter ParseErrorTestCase testcase) { assertThat(e).hasMessageThat().isEqualTo(testcase.error); } - private Object eval(String expression, Map variables) throws Exception { - return eval(this.cel, expression, variables); - } - private Object eval(Cel cel, String expression, Map variables) throws Exception { - CelAbstractSyntaxTree ast = - this.isParseOnly ? cel.parse(expression).getAst() : cel.compile(expression).getAst(); - return cel.createProgram(ast).eval(variables); - } } diff --git a/extensions/src/test/java/dev/cel/extensions/CelRegexExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelRegexExtensionsTest.java index 924344b25..97d0cc90c 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelRegexExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelRegexExtensionsTest.java @@ -21,35 +21,23 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.bundle.Cel; -import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; import dev.cel.runtime.CelEvaluationException; -import dev.cel.testing.CelRuntimeFlavor; import java.util.Optional; -import org.junit.Assume; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) -public final class CelRegexExtensionsTest { - - @TestParameter public CelRuntimeFlavor runtimeFlavor; - @TestParameter public boolean isParseOnly; - - private Cel cel; - - @Before - public void setUp() { - // Legacy runtime does not support parsed-only evaluation mode. - Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.LEGACY) && isParseOnly); - this.cel = - runtimeFlavor - .builder() - .addCompilerLibraries(CelExtensions.regex()) - .addRuntimeLibraries(CelExtensions.regex()) - .build(); +public final class CelRegexExtensionsTest extends CelExtensionTestBase { + + @Override + protected Cel newCelEnv() { + return runtimeFlavor + .builder() + .addCompilerLibraries(CelExtensions.regex()) + .addRuntimeLibraries(CelExtensions.regex()) + .build(); } @@ -276,9 +264,5 @@ public void extractAll_multipleCaptureGroups_throwsException(String target, Stri .contains("Regular expression has more than one capturing group:"); } - private Object eval(String expr) throws Exception { - CelAbstractSyntaxTree ast = - isParseOnly ? cel.parse(expr).getAst() : cel.compile(expr).getAst(); - return cel.createProgram(ast).eval(); - } + } diff --git a/extensions/src/test/java/dev/cel/extensions/CelSetsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelSetsExtensionsTest.java index 9007bba2e..091d456f5 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelSetsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelSetsExtensionsTest.java @@ -23,7 +23,6 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.bundle.Cel; -import dev.cel.bundle.CelBuilder; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; @@ -39,27 +38,39 @@ import dev.cel.runtime.CelRuntime; import dev.cel.testing.CelRuntimeFlavor; import java.util.List; -import java.util.Map; import org.junit.Assume; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) -public final class CelSetsExtensionsTest { +public final class CelSetsExtensionsTest extends CelExtensionTestBase { private static final CelOptions CEL_OPTIONS = CelOptions.current().enableHeterogeneousNumericComparisons(true).build(); - @TestParameter public CelRuntimeFlavor runtimeFlavor; - @TestParameter public boolean isParseOnly; - - private Cel cel; - - @Before - public void setUp() { - // Legacy runtime does not support parsed-only evaluation mode. - Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.LEGACY) && isParseOnly); - this.cel = setupEnv(runtimeFlavor.builder()); + @Override + protected Cel newCelEnv() { + return runtimeFlavor + .builder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .setOptions(CEL_OPTIONS) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .addCompilerLibraries(CelExtensions.sets(CEL_OPTIONS)) + .addRuntimeLibraries(CelExtensions.sets(CEL_OPTIONS)) + .addVar("list", ListType.create(SimpleType.INT)) + .addVar("subList", ListType.create(SimpleType.INT)) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "new_int", + CelOverloadDecl.newGlobalOverload("new_int_int64", SimpleType.INT, SimpleType.INT))) + .addFunctionBindings( + CelFunctionBinding.fromOverloads( + "new_int", + CelFunctionBinding.from( + "new_int_int64", + Long.class, + // Intentionally return java.lang.Integer to test primitive type adaptation + Math::toIntExact))) + .build(); } @Test @@ -375,7 +386,7 @@ public void setsExtension_containsFunctionSubset_succeeds() throws Exception { .addRuntimeLibraries(setsExtensions) .build(); - Object evaluatedResult = eval(cel, "sets.contains([1, 2], [2])", ImmutableMap.of()); + Object evaluatedResult = eval(cel, "sets.contains([1, 2], [2])"); assertThat(evaluatedResult).isEqualTo(true); } @@ -391,7 +402,7 @@ public void setsExtension_equivalentFunctionSubset_succeeds() throws Exception { .addRuntimeLibraries(setsExtensions) .build(); - Object evaluatedResult = eval(cel, "sets.equivalent([1, 1], [1])", ImmutableMap.of()); + Object evaluatedResult = eval(cel, "sets.equivalent([1, 1], [1])"); assertThat(evaluatedResult).isEqualTo(true); } @@ -407,7 +418,7 @@ public void setsExtension_intersectsFunctionSubset_succeeds() throws Exception { .addRuntimeLibraries(setsExtensions) .build(); - Object evaluatedResult = eval(cel, "sets.intersects([1, 1], [1])", ImmutableMap.of()); + Object evaluatedResult = eval(cel, "sets.intersects([1, 1], [1])"); assertThat(evaluatedResult).isEqualTo(true); } @@ -450,45 +461,5 @@ public void setsExtension_evaluateUnallowedFunction_throws() throws Exception { } } - private Object eval(Cel cel, String expression, Map variables) throws Exception { - CelAbstractSyntaxTree ast; - if (isParseOnly) { - ast = cel.parse(expression).getAst(); - } else { - ast = cel.compile(expression).getAst(); - } - return cel.createProgram(ast).eval(variables); - } - - private Object eval(String expression) throws Exception { - return eval(this.cel, expression, ImmutableMap.of()); - } - - private Object eval(String expression, Map variables) throws Exception { - return eval(this.cel, expression, variables); - } - private static Cel setupEnv(CelBuilder celBuilder) { - return celBuilder - .addMessageTypes(TestAllTypes.getDescriptor()) - .setOptions(CEL_OPTIONS) - .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) - .addCompilerLibraries(CelExtensions.sets(CEL_OPTIONS)) - .addRuntimeLibraries(CelExtensions.sets(CEL_OPTIONS)) - .addVar("list", ListType.create(SimpleType.INT)) - .addVar("subList", ListType.create(SimpleType.INT)) - .addFunctionDeclarations( - CelFunctionDecl.newFunctionDeclaration( - "new_int", - CelOverloadDecl.newGlobalOverload("new_int_int64", SimpleType.INT, SimpleType.INT))) - .addFunctionBindings( - CelFunctionBinding.fromOverloads( - "new_int", - CelFunctionBinding.from( - "new_int_int64", - Long.class, - // Intentionally return java.lang.Integer to test primitive type adaptation - Math::toIntExact))) - .build(); - } } diff --git a/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java index e7542b7b7..4b242ddcd 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java @@ -33,40 +33,29 @@ import dev.cel.extensions.CelStringExtensions.Function; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelRuntime; -import dev.cel.testing.CelRuntimeFlavor; import java.util.List; -import java.util.Map; import org.junit.Assume; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) -public final class CelStringExtensionsTest { - - @TestParameter public CelRuntimeFlavor runtimeFlavor; - @TestParameter public boolean isParseOnly; - - private Cel cel; - - @Before - public void setUp() { - // Legacy runtime does not support parsed-only evaluation mode. - Assume.assumeFalse(runtimeFlavor.equals(CelRuntimeFlavor.LEGACY) && isParseOnly); - this.cel = - runtimeFlavor - .builder() - .addCompilerLibraries(CelExtensions.strings()) - .addRuntimeLibraries(CelExtensions.strings()) - .addVar("s", SimpleType.STRING) - .addVar("separator", SimpleType.STRING) - .addVar("index", SimpleType.INT) - .addVar("offset", SimpleType.INT) - .addVar("indexOfParam", SimpleType.STRING) - .addVar("beginIndex", SimpleType.INT) - .addVar("endIndex", SimpleType.INT) - .addVar("limit", SimpleType.INT) - .build(); +public final class CelStringExtensionsTest extends CelExtensionTestBase { + + @Override + protected Cel newCelEnv() { + return runtimeFlavor + .builder() + .addCompilerLibraries(CelExtensions.strings()) + .addRuntimeLibraries(CelExtensions.strings()) + .addVar("s", SimpleType.STRING) + .addVar("separator", SimpleType.STRING) + .addVar("index", SimpleType.INT) + .addVar("offset", SimpleType.INT) + .addVar("indexOfParam", SimpleType.STRING) + .addVar("beginIndex", SimpleType.INT) + .addVar("endIndex", SimpleType.INT) + .addVar("limit", SimpleType.INT) + .build(); } @Test @@ -388,13 +377,10 @@ public void split_withLimit_separatorIsNonString_throwsException() { @Test public void split_withLimitOverflow_throwsException() throws Exception { + ImmutableMap variables = ImmutableMap.of("limit", 2147483648L); // INT_MAX + 1 CelEvaluationException exception = assertThrows( - CelEvaluationException.class, - () -> - eval( - "'test'.split('', limit)", - ImmutableMap.of("limit", 2147483648L))); // INT_MAX + 1 + CelEvaluationException.class, () -> eval("'test'.split('', limit)", variables)); assertThat(exception) .hasMessageThat() @@ -454,13 +440,10 @@ public void substring_beginAndEndIndex_unicode_success( @TestParameters("{string: '', beginIndex: 2}") public void substring_beginIndexOutOfRange_ascii_throwsException(String string, int beginIndex) throws Exception { + ImmutableMap variables = ImmutableMap.of("s", string, "beginIndex", beginIndex); CelEvaluationException exception = assertThrows( - CelEvaluationException.class, - () -> - eval( - "s.substring(beginIndex)", - ImmutableMap.of("s", string, "beginIndex", beginIndex))); + CelEvaluationException.class, () -> eval("s.substring(beginIndex)", variables)); String exceptionMessage = String.format( @@ -478,13 +461,10 @@ public void substring_beginIndexOutOfRange_ascii_throwsException(String string, @TestParameters("{string: '😁가나', beginIndex: 4, uniqueCharCount: 3}") public void substring_beginIndexOutOfRange_unicode_throwsException( String string, int beginIndex, int uniqueCharCount) throws Exception { + ImmutableMap variables = ImmutableMap.of("s", string, "beginIndex", beginIndex); CelEvaluationException exception = assertThrows( - CelEvaluationException.class, - () -> - eval( - "s.substring(beginIndex)", - ImmutableMap.of("s", string, "beginIndex", beginIndex))); + CelEvaluationException.class, () -> eval("s.substring(beginIndex)", variables)); String exceptionMessage = String.format( @@ -501,13 +481,12 @@ public void substring_beginIndexOutOfRange_unicode_throwsException( @TestParameters("{string: '😁😑😦', beginIndex: 2, endIndex: 1}") public void substring_beginAndEndIndexOutOfRange_throwsException( String string, int beginIndex, int endIndex) throws Exception { + ImmutableMap variables = + ImmutableMap.of("s", string, "beginIndex", beginIndex, "endIndex", endIndex); CelEvaluationException exception = assertThrows( CelEvaluationException.class, - () -> - eval( - "s.substring(beginIndex, endIndex)", - ImmutableMap.of("s", string, "beginIndex", beginIndex, "endIndex", endIndex))); + () -> eval("s.substring(beginIndex, endIndex)", variables)); String exceptionMessage = String.format("substring failure: Range [%d, %d) out of bounds", beginIndex, endIndex); @@ -516,13 +495,11 @@ public void substring_beginAndEndIndexOutOfRange_throwsException( @Test public void substring_beginIndexOverflow_throwsException() throws Exception { + ImmutableMap variables = + ImmutableMap.of("beginIndex", 2147483648L); // INT_MAX + 1 CelEvaluationException exception = assertThrows( - CelEvaluationException.class, - () -> - eval( - "'abcd'.substring(beginIndex)", - ImmutableMap.of("beginIndex", 2147483648L))); // INT_MAX + 1 + CelEvaluationException.class, () -> eval("'abcd'.substring(beginIndex)", variables)); assertThat(exception) .hasMessageThat() @@ -1381,10 +1358,7 @@ public void stringExtension_functionSubset_success() throws Exception { .build(); Object evaluatedResult = - eval( - customCel, - "'test'.substring(2) == 'st' && 'hello'.charAt(1) == 'e'", - ImmutableMap.of()); + eval(customCel, "'test'.substring(2) == 'st' && 'hello'.charAt(1) == 'e'"); assertThat(evaluatedResult).isEqualTo(true); } @@ -1499,21 +1473,5 @@ public void stringExtension_evaluateUnallowedFunction_throws() throws Exception assertThrows(CelEvaluationException.class, () -> customRuntimeCel.createProgram(ast).eval()); } - private Object eval(Cel cel, String expression, Map variables) throws Exception { - CelAbstractSyntaxTree ast; - if (isParseOnly) { - ast = cel.parse(expression).getAst(); - } else { - ast = cel.compile(expression).getAst(); - } - return cel.createProgram(ast).eval(variables); - } - - private Object eval(String expression) throws Exception { - return eval(this.cel, expression, ImmutableMap.of()); - } - private Object eval(String expression, Map variables) throws Exception { - return eval(this.cel, expression, variables); - } } From 61a01d800b24dfb79bc7035119166d2822f3ff1b Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 22 Apr 2026 15:44:18 -0700 Subject: [PATCH 057/204] Change function failures (dispatch / overload match) to always include the function name PiperOrigin-RevId: 904088792 --- .../extensions/CelListsExtensionsTest.java | 3 +- .../src/main/java/dev/cel/runtime/BUILD.bazel | 4 +- .../dev/cel/runtime/CelFunctionBinding.java | 1 + .../cel/runtime/CelLateFunctionBindings.java | 5 +++ .../dev/cel/runtime/CelResolvedOverload.java | 26 ++++++++--- .../java/dev/cel/runtime/CelRuntimeImpl.java | 10 +++++ .../dev/cel/runtime/CelRuntimeLegacyImpl.java | 10 +++++ .../dev/cel/runtime/DefaultDispatcher.java | 23 +++++++--- .../dev/cel/runtime/FunctionBindingImpl.java | 35 +++++++++++++-- .../runtime/InternalCelFunctionBinding.java | 29 ++++++++++++ .../java/dev/cel/runtime/LiteRuntimeImpl.java | 15 +++++-- .../dev/cel/runtime/planner/EvalBinary.java | 9 +++- .../dev/cel/runtime/planner/EvalHelpers.java | 19 ++++++-- .../runtime/planner/EvalLateBoundCall.java | 2 +- .../dev/cel/runtime/planner/EvalUnary.java | 8 +++- .../cel/runtime/planner/EvalVarArgsCall.java | 8 +++- .../cel/runtime/planner/EvalZeroArity.java | 16 +++++-- .../cel/runtime/planner/ProgramPlanner.java | 14 ++++-- .../src/test/java/dev/cel/runtime/BUILD.bazel | 1 + .../cel/runtime/CelResolvedOverloadTest.java | 45 ++++++++++++------- .../cel/runtime/DefaultDispatcherTest.java | 12 ++++- .../cel/runtime/DefaultInterpreterTest.java | 10 ++++- .../cel/runtime/PlannerInterpreterTest.java | 15 ++++--- .../runtime/planner/ProgramPlannerTest.java | 18 +++----- .../planner_optional_errors.baseline | 5 +++ 25 files changed, 266 insertions(+), 77 deletions(-) create mode 100644 runtime/src/main/java/dev/cel/runtime/InternalCelFunctionBinding.java create mode 100644 runtime/src/test/resources/planner_optional_errors.baseline diff --git a/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java index f5536da4e..4520f81ba 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java @@ -29,6 +29,7 @@ import dev.cel.expr.conformance.test.SimpleTest; import dev.cel.parser.CelStandardMacro; import dev.cel.runtime.CelEvaluationException; +import dev.cel.testing.CelRuntimeFlavor; import org.junit.Assume; import org.junit.Test; import org.junit.runner.RunWith; @@ -143,7 +144,7 @@ public void flatten_negativeDepth_throws() { CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> eval(cel, "[1,2,3,4].flatten(-1)")); - if (isParseOnly) { + if (runtimeFlavor.equals(CelRuntimeFlavor.PLANNER)) { assertThat(e) .hasMessageThat() .contains("evaluation error at :17: Function 'flatten' failed"); diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 6f0607de4..ef0ac71d4 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -53,6 +53,7 @@ LITE_PROGRAM_IMPL_SOURCES = [ FUNCTION_BINDING_SOURCES = [ "CelFunctionBinding.java", "FunctionBindingImpl.java", + "InternalCelFunctionBinding.java", ] # keep sorted @@ -740,6 +741,7 @@ java_library( deps = [ ":evaluation_exception", ":function_overload", + "//common/annotations", "//common/exceptions:overload_not_found", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -754,6 +756,7 @@ cel_android_library( deps = [ ":evaluation_exception", ":function_overload_android", + "//common/annotations", "//common/exceptions:overload_not_found", "@maven//:com_google_errorprone_error_prone_annotations", "@maven_android//:com_google_guava_guava", @@ -890,7 +893,6 @@ java_library( "//common/types:type_providers", "//common/values:cel_value_provider", "//common/values:proto_message_value_provider", - "//runtime/standard:add", "//runtime/standard:int", "//runtime/standard:timestamp", "@maven//:com_google_code_findbugs_annotations", diff --git a/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java index 88be0d3c3..98991d383 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java @@ -100,6 +100,7 @@ static CelFunctionBinding from( overloadId, ImmutableList.copyOf(argTypes), impl, /* isStrict= */ true); } + /** See {@link #fromOverloads(String, Collection)}. */ static ImmutableSet fromOverloads( String functionName, CelFunctionBinding... overloadBindings) { diff --git a/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java b/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java index 3d75845cf..2da08120c 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java +++ b/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java @@ -63,7 +63,12 @@ public static CelLateFunctionBindings from(Collection functi } private static CelResolvedOverload createResolvedOverload(CelFunctionBinding binding) { + String functionName = binding.getOverloadId(); + if (binding instanceof InternalCelFunctionBinding) { + functionName = ((InternalCelFunctionBinding) binding).getFunctionName(); + } return CelResolvedOverload.of( + functionName, binding.getOverloadId(), binding.getDefinition(), binding.isStrict(), diff --git a/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java b/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java index 7063720a1..fbe9a3289 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java +++ b/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java @@ -30,6 +30,9 @@ @Internal public abstract class CelResolvedOverload { + /** The base function name. */ + public abstract String getFunctionName(); + /** The overload id of the function. */ public abstract String getOverloadId(); @@ -61,7 +64,7 @@ public Object invoke(Object[] args) throws CelEvaluationException { || CelFunctionOverload.canHandle(args, getParameterTypes(), isStrict())) { return getDefinition().apply(args); } - throw new CelOverloadNotFoundException(getOverloadId()); + throw new CelOverloadNotFoundException(getFunctionName(), ImmutableList.of(getOverloadId())); } public Object invoke(Object arg) throws CelEvaluationException { @@ -69,7 +72,7 @@ public Object invoke(Object arg) throws CelEvaluationException { || CelFunctionOverload.canHandle(arg, getParameterTypes(), isStrict())) { return getOptimizedDefinition().apply(arg); } - throw new CelOverloadNotFoundException(getOverloadId()); + throw new CelOverloadNotFoundException(getFunctionName(), ImmutableList.of(getOverloadId())); } public Object invoke(Object arg1, Object arg2) throws CelEvaluationException { @@ -77,24 +80,28 @@ public Object invoke(Object arg1, Object arg2) throws CelEvaluationException { || CelFunctionOverload.canHandle(arg1, arg2, getParameterTypes(), isStrict())) { return getOptimizedDefinition().apply(arg1, arg2); } - throw new CelOverloadNotFoundException(getOverloadId()); + throw new CelOverloadNotFoundException(getFunctionName(), ImmutableList.of(getOverloadId())); } /** - * Creates a new resolved overload from the given overload id, parameter types, and definition. + * Creates a new resolved overload from the given function name, overload id, parameter types, and + * definition. */ public static CelResolvedOverload of( + String functionName, String overloadId, CelFunctionOverload definition, boolean isStrict, Class... parameterTypes) { - return of(overloadId, definition, isStrict, ImmutableList.copyOf(parameterTypes)); + return of(functionName, overloadId, definition, isStrict, ImmutableList.copyOf(parameterTypes)); } /** - * Creates a new resolved overload from the given overload id, parameter types, and definition. + * Creates a new resolved overload from the given function name, overload id, parameter types, and + * definition. */ public static CelResolvedOverload of( + String functionName, String overloadId, CelFunctionOverload definition, boolean isStrict, @@ -104,7 +111,12 @@ public static CelResolvedOverload of( ? (OptimizedFunctionOverload) definition : definition::apply; return new AutoValue_CelResolvedOverload( - overloadId, ImmutableList.copyOf(parameterTypes), isStrict, definition, optimizedDef); + functionName, + overloadId, + ImmutableList.copyOf(parameterTypes), + isStrict, + definition, + optimizedDef); } /** diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index cab2c666e..43b223fa0 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -381,7 +381,12 @@ private static DefaultDispatcher newDispatcher( DefaultDispatcher.Builder builder = DefaultDispatcher.newBuilder(); for (CelFunctionBinding binding : standardFunctions.newFunctionBindings(runtimeEquality, options)) { + String functionName = binding.getOverloadId(); + if (binding instanceof InternalCelFunctionBinding) { + functionName = ((InternalCelFunctionBinding) binding).getFunctionName(); + } builder.addOverload( + functionName, binding.getOverloadId(), binding.getArgTypes(), binding.isStrict(), @@ -389,7 +394,12 @@ private static DefaultDispatcher newDispatcher( } for (CelFunctionBinding binding : customFunctionBindings) { + String functionName = binding.getOverloadId(); + if (binding instanceof InternalCelFunctionBinding) { + functionName = ((InternalCelFunctionBinding) binding).getFunctionName(); + } builder.addOverload( + functionName, binding.getOverloadId(), binding.getArgTypes(), binding.isStrict(), diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java index 8ae4a9e3e..33702b2c6 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java @@ -305,7 +305,12 @@ public CelRuntimeLegacyImpl build() { DefaultDispatcher.Builder dispatcherBuilder = DefaultDispatcher.newBuilder(); for (CelFunctionBinding standardFunctionBinding : newStandardFunctionBindings(runtimeEquality)) { + String functionName = standardFunctionBinding.getOverloadId(); + if (standardFunctionBinding instanceof InternalCelFunctionBinding) { + functionName = ((InternalCelFunctionBinding) standardFunctionBinding).getFunctionName(); + } dispatcherBuilder.addOverload( + functionName, standardFunctionBinding.getOverloadId(), standardFunctionBinding.getArgTypes(), standardFunctionBinding.isStrict(), @@ -313,7 +318,12 @@ public CelRuntimeLegacyImpl build() { } for (CelFunctionBinding customBinding : customFunctionBindings.values()) { + String functionName = customBinding.getOverloadId(); + if (customBinding instanceof InternalCelFunctionBinding) { + functionName = ((InternalCelFunctionBinding) customBinding).getFunctionName(); + } dispatcherBuilder.addOverload( + functionName, customBinding.getOverloadId(), customBinding.getArgTypes(), customBinding.isStrict(), diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java index d6ddf3965..0a467db81 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java @@ -134,6 +134,8 @@ public static class Builder { @AutoValue @Immutable abstract static class OverloadEntry { + abstract String functionName(); + abstract ImmutableList> argTypes(); abstract boolean isStrict(); @@ -141,8 +143,12 @@ abstract static class OverloadEntry { abstract CelFunctionOverload overload(); private static OverloadEntry of( - ImmutableList> argTypes, boolean isStrict, CelFunctionOverload overload) { - return new AutoValue_DefaultDispatcher_Builder_OverloadEntry(argTypes, isStrict, overload); + String functionName, + ImmutableList> argTypes, + boolean isStrict, + CelFunctionOverload overload) { + return new AutoValue_DefaultDispatcher_Builder_OverloadEntry( + functionName, argTypes, isStrict, overload); } } @@ -150,16 +156,19 @@ private static OverloadEntry of( @CanIgnoreReturnValue public Builder addOverload( + String functionName, String overloadId, ImmutableList> argTypes, boolean isStrict, CelFunctionOverload overload) { + checkNotNull(functionName); + checkArgument(!functionName.isEmpty(), "Function name cannot be empty."); checkNotNull(overloadId); checkArgument(!overloadId.isEmpty(), "Overload ID cannot be empty."); checkNotNull(argTypes); checkNotNull(overload); - OverloadEntry newEntry = OverloadEntry.of(argTypes, isStrict, overload); + OverloadEntry newEntry = OverloadEntry.of(functionName, argTypes, isStrict, overload); overloads.merge( overloadId, @@ -188,7 +197,7 @@ private OverloadEntry mergeDynamicDispatchesOrThrow( boolean isStrict = mergedOverload.getOverloadBindings().stream().allMatch(CelFunctionBinding::isStrict); - return OverloadEntry.of(incoming.argTypes(), isStrict, mergedOverload); + return OverloadEntry.of(overloadId, incoming.argTypes(), isStrict, mergedOverload); } throw new IllegalArgumentException("Duplicate overload ID binding: " + overloadId); @@ -204,7 +213,11 @@ public DefaultDispatcher build() { resolvedOverloads.put( overloadId, CelResolvedOverload.of( - overloadId, overloadImpl, overloadEntry.isStrict(), overloadEntry.argTypes())); + overloadEntry.functionName(), + overloadId, + overloadImpl, + overloadEntry.isStrict(), + overloadEntry.argTypes())); } return new DefaultDispatcher(resolvedOverloads.buildOrThrow()); diff --git a/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java b/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java index c1306ce19..7b8efe8fd 100644 --- a/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java @@ -23,7 +23,9 @@ import dev.cel.common.exceptions.CelOverloadNotFoundException; @Immutable -final class FunctionBindingImpl implements CelFunctionBinding { +final class FunctionBindingImpl implements InternalCelFunctionBinding { + + private final String functionName; private final String overloadId; @@ -33,6 +35,11 @@ final class FunctionBindingImpl implements CelFunctionBinding { private final boolean isStrict; + @Override + public String getFunctionName() { + return functionName; + } + @Override public String getOverloadId() { return overloadId; @@ -54,20 +61,34 @@ public boolean isStrict() { } FunctionBindingImpl( + String functionName, String overloadId, ImmutableList> argTypes, CelFunctionOverload definition, boolean isStrict) { + this.functionName = functionName; this.overloadId = overloadId; this.argTypes = argTypes; this.definition = definition; this.isStrict = isStrict; } + FunctionBindingImpl( + String overloadId, + ImmutableList> argTypes, + CelFunctionOverload definition, + boolean isStrict) { + this(overloadId, overloadId, argTypes, definition, isStrict); + } + static ImmutableSet groupOverloadsToFunction( String functionName, ImmutableSet overloadBindings) { ImmutableSet.Builder builder = ImmutableSet.builder(); - builder.addAll(overloadBindings); + for (CelFunctionBinding b : overloadBindings) { + builder.add( + new FunctionBindingImpl( + functionName, b.getOverloadId(), b.getArgTypes(), b.getDefinition(), b.isStrict())); + } // If there is already a binding with the same name as the function, we treat it as a // "Singleton" binding and do not create a dynamic dispatch wrapper for it. @@ -80,11 +101,12 @@ static ImmutableSet groupOverloadsToFunction( CelFunctionBinding singleBinding = Iterables.getOnlyElement(overloadBindings); builder.add( new FunctionBindingImpl( + functionName, functionName, singleBinding.getArgTypes(), singleBinding.getDefinition(), singleBinding.isStrict())); - } else { + } else if (overloadBindings.size() > 1) { builder.add(new DynamicDispatchBinding(functionName, overloadBindings)); } } @@ -93,7 +115,7 @@ static ImmutableSet groupOverloadsToFunction( } @Immutable - static final class DynamicDispatchBinding implements CelFunctionBinding { + static final class DynamicDispatchBinding implements InternalCelFunctionBinding { private final boolean isStrict; private final DynamicDispatchOverload dynamicDispatchOverload; @@ -103,6 +125,11 @@ public String getOverloadId() { return dynamicDispatchOverload.functionName; } + @Override + public String getFunctionName() { + return dynamicDispatchOverload.functionName; + } + @Override public ImmutableList> getArgTypes() { return ImmutableList.of(); diff --git a/runtime/src/main/java/dev/cel/runtime/InternalCelFunctionBinding.java b/runtime/src/main/java/dev/cel/runtime/InternalCelFunctionBinding.java new file mode 100644 index 000000000..48a0f36d1 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/InternalCelFunctionBinding.java @@ -0,0 +1,29 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.annotations.Internal; + +/** + * Internal interface to expose the function name associated with a binding. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +@Immutable +public interface InternalCelFunctionBinding extends CelFunctionBinding { + String getFunctionName(); +} diff --git a/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java index 0e5c5cf30..d58eb3be4 100644 --- a/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java @@ -162,9 +162,18 @@ public CelLiteRuntime build() { functionBindingsBuilder .buildOrThrow() .forEach( - (String overloadId, CelFunctionBinding func) -> - dispatcherBuilder.addOverload( - overloadId, func.getArgTypes(), func.isStrict(), func.getDefinition())); + (String overloadId, CelFunctionBinding func) -> { + String functionName = func.getOverloadId(); + if (func instanceof InternalCelFunctionBinding) { + functionName = ((InternalCelFunctionBinding) func).getFunctionName(); + } + dispatcherBuilder.addOverload( + functionName, + overloadId, + func.getArgTypes(), + func.isStrict(), + func.getDefinition()); + }); Interpreter interpreter = new DefaultInterpreter( diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java index 7771da3e6..16eba3cce 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java @@ -25,6 +25,7 @@ final class EvalBinary extends PlannedInterpretable { + private final String functionName; private final CelResolvedOverload resolvedOverload; private final PlannedInterpretable arg1; private final PlannedInterpretable arg2; @@ -48,25 +49,29 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval return unknowns; } - return EvalHelpers.dispatch(resolvedOverload, celValueConverter, argVal1, argVal2); + return EvalHelpers.dispatch( + functionName, resolvedOverload, celValueConverter, argVal1, argVal2); } static EvalBinary create( long exprId, + String functionName, CelResolvedOverload resolvedOverload, PlannedInterpretable arg1, PlannedInterpretable arg2, CelValueConverter celValueConverter) { - return new EvalBinary(exprId, resolvedOverload, arg1, arg2, celValueConverter); + return new EvalBinary(exprId, functionName, resolvedOverload, arg1, arg2, celValueConverter); } private EvalBinary( long exprId, + String functionName, CelResolvedOverload resolvedOverload, PlannedInterpretable arg1, PlannedInterpretable arg2, CelValueConverter celValueConverter) { super(exprId); + this.functionName = functionName; this.resolvedOverload = resolvedOverload; this.arg1 = arg1; this.arg2 = arg2; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java index a30f91880..220642f4a 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java @@ -56,7 +56,10 @@ static Object evalStrictly( } static Object dispatch( - CelResolvedOverload overload, CelValueConverter valueConverter, Object[] args) + String functionName, + CelResolvedOverload overload, + CelValueConverter valueConverter, + Object[] args) throws CelEvaluationException { try { Object result = overload.invoke(args); @@ -66,7 +69,11 @@ static Object dispatch( } } - static Object dispatch(CelResolvedOverload overload, CelValueConverter valueConverter, Object arg) + static Object dispatch( + String functionName, + CelResolvedOverload overload, + CelValueConverter valueConverter, + Object arg) throws CelEvaluationException { try { Object result = overload.invoke(arg); @@ -77,7 +84,11 @@ static Object dispatch(CelResolvedOverload overload, CelValueConverter valueConv } static Object dispatch( - CelResolvedOverload overload, CelValueConverter valueConverter, Object arg1, Object arg2) + String functionName, + CelResolvedOverload overload, + CelValueConverter valueConverter, + Object arg1, + Object arg2) throws CelEvaluationException { try { Object result = overload.invoke(arg1, arg2); @@ -97,7 +108,7 @@ private static RuntimeException handleDispatchException( return new IllegalArgumentException( String.format( "Function '%s' failed with arg(s) '%s'", - overload.getOverloadId(), Joiner.on(", ").join(args)), + overload.getFunctionName(), Joiner.on(", ").join(args)), e); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java index cdee878ee..0bd251185 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java @@ -55,7 +55,7 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval .findOverload(functionName, overloadIds, argVals) .orElseThrow(() -> new CelOverloadNotFoundException(functionName, overloadIds)); - return EvalHelpers.dispatch(resolvedOverload, celValueConverter, argVals); + return EvalHelpers.dispatch(functionName, resolvedOverload, celValueConverter, argVals); } static EvalLateBoundCall create( diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java index 322648ee3..57834161f 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java @@ -24,6 +24,7 @@ final class EvalUnary extends PlannedInterpretable { + private final String functionName; private final CelResolvedOverload resolvedOverload; private final PlannedInterpretable arg; private final CelValueConverter celValueConverter; @@ -34,23 +35,26 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval resolvedOverload.isStrict() ? evalStrictly(arg, resolver, frame) : evalNonstrictly(arg, resolver, frame); - return EvalHelpers.dispatch(resolvedOverload, celValueConverter, argVal); + return EvalHelpers.dispatch(functionName, resolvedOverload, celValueConverter, argVal); } static EvalUnary create( long exprId, + String functionName, CelResolvedOverload resolvedOverload, PlannedInterpretable arg, CelValueConverter celValueConverter) { - return new EvalUnary(exprId, resolvedOverload, arg, celValueConverter); + return new EvalUnary(exprId, functionName, resolvedOverload, arg, celValueConverter); } private EvalUnary( long exprId, + String functionName, CelResolvedOverload resolvedOverload, PlannedInterpretable arg, CelValueConverter celValueConverter) { super(exprId); + this.functionName = functionName; this.resolvedOverload = resolvedOverload; this.arg = arg; this.celValueConverter = celValueConverter; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java index eb8745632..fe7c6c430 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java @@ -25,6 +25,7 @@ final class EvalVarArgsCall extends PlannedInterpretable { + private final String functionName; private final CelResolvedOverload resolvedOverload; @SuppressWarnings("Immutable") @@ -50,23 +51,26 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval return unknowns; } - return EvalHelpers.dispatch(resolvedOverload, celValueConverter, argVals); + return EvalHelpers.dispatch(functionName, resolvedOverload, celValueConverter, argVals); } static EvalVarArgsCall create( long exprId, + String functionName, CelResolvedOverload resolvedOverload, PlannedInterpretable[] args, CelValueConverter celValueConverter) { - return new EvalVarArgsCall(exprId, resolvedOverload, args, celValueConverter); + return new EvalVarArgsCall(exprId, functionName, resolvedOverload, args, celValueConverter); } private EvalVarArgsCall( long exprId, + String functionName, CelResolvedOverload resolvedOverload, PlannedInterpretable[] args, CelValueConverter celValueConverter) { super(exprId); + this.functionName = functionName; this.resolvedOverload = resolvedOverload; this.args = args; this.celValueConverter = celValueConverter; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalZeroArity.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalZeroArity.java index 5b3138207..7798c8253 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalZeroArity.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalZeroArity.java @@ -22,22 +22,30 @@ final class EvalZeroArity extends PlannedInterpretable { private static final Object[] EMPTY_ARRAY = new Object[0]; + private final String functionName; private final CelResolvedOverload resolvedOverload; private final CelValueConverter celValueConverter; @Override public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { - return EvalHelpers.dispatch(resolvedOverload, celValueConverter, EMPTY_ARRAY); + return EvalHelpers.dispatch(functionName, resolvedOverload, celValueConverter, EMPTY_ARRAY); } static EvalZeroArity create( - long exprId, CelResolvedOverload resolvedOverload, CelValueConverter celValueConverter) { - return new EvalZeroArity(exprId, resolvedOverload, celValueConverter); + long exprId, + String functionName, + CelResolvedOverload resolvedOverload, + CelValueConverter celValueConverter) { + return new EvalZeroArity(exprId, functionName, resolvedOverload, celValueConverter); } private EvalZeroArity( - long exprId, CelResolvedOverload resolvedOverload, CelValueConverter celValueConverter) { + long exprId, + String functionName, + CelResolvedOverload resolvedOverload, + CelValueConverter celValueConverter) { super(exprId); + this.functionName = functionName; this.resolvedOverload = resolvedOverload; this.celValueConverter = celValueConverter; } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index 9bd5f3ecd..a0b74fc99 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -308,15 +308,21 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { switch (argCount) { case 0: - return EvalZeroArity.create(expr.id(), resolvedOverload, celValueConverter); + return EvalZeroArity.create(expr.id(), functionName, resolvedOverload, celValueConverter); case 1: - return EvalUnary.create(expr.id(), resolvedOverload, evaluatedArgs[0], celValueConverter); + return EvalUnary.create( + expr.id(), functionName, resolvedOverload, evaluatedArgs[0], celValueConverter); case 2: return EvalBinary.create( - expr.id(), resolvedOverload, evaluatedArgs[0], evaluatedArgs[1], celValueConverter); + expr.id(), + functionName, + resolvedOverload, + evaluatedArgs[0], + evaluatedArgs[1], + celValueConverter); default: return EvalVarArgsCall.create( - expr.id(), resolvedOverload, evaluatedArgs, celValueConverter); + expr.id(), functionName, resolvedOverload, evaluatedArgs, celValueConverter); } } diff --git a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel index 577010971..7cd24f040 100644 --- a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel @@ -2,6 +2,7 @@ load("@rules_java//java:defs.bzl", "java_library") load("//:cel_android_rules.bzl", "cel_android_local_test") load("//:testing.bzl", "junit4_test_suites") +# Invalidate cache after file removal package( default_applicable_licenses = ["//:license"], default_testonly = True, diff --git a/runtime/src/test/java/dev/cel/runtime/CelResolvedOverloadTest.java b/runtime/src/test/java/dev/cel/runtime/CelResolvedOverloadTest.java index c1210c1ba..471282117 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelResolvedOverloadTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelResolvedOverloadTest.java @@ -27,11 +27,13 @@ public final class CelResolvedOverloadTest { CelResolvedOverload getIncrementIntOverload() { return CelResolvedOverload.of( - "increment_int", - (args) -> { - Long arg = (Long) args[0]; - return arg + 1; - }, + /* functionName= */ "increment_int", + /* overloadId= */ "increment_int_overload", + (CelFunctionOverload) + (args) -> { + Long arg = (Long) args[0]; + return arg + 1; + }, /* isStrict= */ true, Long.class); } @@ -45,14 +47,23 @@ public void canHandle_matchingTypes_returnsTrue() { public void canHandle_nullMessageType_returnsFalse() { CelResolvedOverload overload = CelResolvedOverload.of( - "identity", (args) -> args[0], /* isStrict= */ true, TestAllTypes.class); + /* functionName= */ "identity", + /* overloadId= */ "identity_overload", + (CelFunctionOverload) (args) -> args[0], + /* isStrict= */ true, + TestAllTypes.class); assertThat(overload.canHandle(new Object[] {null})).isFalse(); } @Test public void canHandle_nullPrimitive_returnsFalse() { CelResolvedOverload overload = - CelResolvedOverload.of("identity", (args) -> args[0], /* isStrict= */ true, Long.class); + CelResolvedOverload.of( + /* functionName= */ "identity", + /* overloadId= */ "identity_overload", + (CelFunctionOverload) (args) -> args[0], + /* isStrict= */ true, + Long.class); assertThat(overload.canHandle(new Object[] {null})).isFalse(); } @@ -70,10 +81,12 @@ public void canHandle_nonMatchingArgCount_returnsFalse() { public void canHandle_nonStrictOverload_returnsTrue() { CelResolvedOverload nonStrictOverload = CelResolvedOverload.of( - "non_strict", - (args) -> { - return false; - }, + /* functionName= */ "non_strict", + /* overloadId= */ "non_strict_overload", + (CelFunctionOverload) + (args) -> { + return false; + }, /* isStrict= */ false, Long.class, Long.class); @@ -87,10 +100,12 @@ public void canHandle_nonStrictOverload_returnsTrue() { public void canHandle_nonStrictOverload_returnsFalse() { CelResolvedOverload nonStrictOverload = CelResolvedOverload.of( - "non_strict", - (args) -> { - return false; - }, + /* functionName= */ "non_strict", + /* overloadId= */ "non_strict_overload", + (CelFunctionOverload) + (args) -> { + return false; + }, /* isStrict= */ false, Long.class, Long.class); diff --git a/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java b/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java index 255360ee1..d862ddb33 100644 --- a/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java +++ b/runtime/src/test/java/dev/cel/runtime/DefaultDispatcherTest.java @@ -37,11 +37,19 @@ public void setup() { overloads.put( "overload_1", CelResolvedOverload.of( - "overload_1", args -> (Long) args[0] + 1, /* isStrict= */ true, Long.class)); + /* functionName= */ "overload_1", + /* overloadId= */ "overload_1", + args -> (Long) args[0] + 1, + /* isStrict= */ true, + Long.class)); overloads.put( "overload_2", CelResolvedOverload.of( - "overload_2", args -> (Long) args[0] + 2, /* isStrict= */ true, Long.class)); + /* functionName= */ "overload_2", + /* overloadId= */ "overload_2", + args -> (Long) args[0] + 2, + /* isStrict= */ true, + Long.class)); } @Test diff --git a/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java index 1a8f45161..bd0e96856 100644 --- a/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java +++ b/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java @@ -77,15 +77,21 @@ public Object adapt(String messageName, Object message) { CelAbstractSyntaxTree ast = celCompiler.compile("[1].all(x, [2].all(y, error()))").getAst(); DefaultDispatcher.Builder dispatcherBuilder = DefaultDispatcher.newBuilder(); dispatcherBuilder.addOverload( - "error", - ImmutableList.of(long.class), + /* functionName= */ "error", + /* overloadId= */ "error_overload", + ImmutableList.>of(long.class), /* isStrict= */ true, (args) -> new IllegalArgumentException("Always throws")); CelFunctionBinding notStrictlyFalseBinding = NotStrictlyFalseOverload.NOT_STRICTLY_FALSE.newFunctionBinding( CelOptions.DEFAULT, RuntimeEquality.create(RuntimeHelpers.create(), CelOptions.DEFAULT)); + String functionName = notStrictlyFalseBinding.getOverloadId(); + if (notStrictlyFalseBinding instanceof InternalCelFunctionBinding) { + functionName = ((InternalCelFunctionBinding) notStrictlyFalseBinding).getFunctionName(); + } dispatcherBuilder.addOverload( + functionName, notStrictlyFalseBinding.getOverloadId(), notStrictlyFalseBinding.getArgTypes(), notStrictlyFalseBinding.isStrict(), diff --git a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java index 2b0e53298..c0b0f76c4 100644 --- a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java +++ b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java @@ -82,13 +82,14 @@ protected CelAbstractSyntaxTree prepareTest(CelTypeProvider typeProvider) { @Override public void optional_errors() { - if (isParseOnly) { - // Parsed-only evaluation contains function name in the - // error message instead of the function overload. - skipBaselineVerification(); - } else { - super.optional_errors(); - } + // Exercised in planner_optional_errors instead + skipBaselineVerification(); + } + + @Test + public void planner_optional_errors() { + source = "optional.unwrap([dyn(1)])"; + runTest(ImmutableMap.of()); } @Override diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index de30902d3..c58ae782b 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -75,6 +75,7 @@ import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.DefaultDispatcher; import dev.cel.runtime.DescriptorTypeResolver; +import dev.cel.runtime.InternalCelFunctionBinding; import dev.cel.runtime.PartialVars; import dev.cel.runtime.Program; import dev.cel.runtime.RuntimeEquality; @@ -244,6 +245,7 @@ private static void addBindingsToDispatcher( overloadBindings.forEach( overload -> builder.addOverload( + ((InternalCelFunctionBinding) overload).getFunctionName(), overload.getOverloadId(), overload.getArgTypes(), overload.isStrict(), @@ -494,15 +496,11 @@ public void plan_call_zeroArgs() throws Exception { public void plan_call_throws() throws Exception { CelAbstractSyntaxTree ast = compile("error()"); Program program = PLANNER.plan(ast); - String expectedOverloadId = isParseOnly ? "error" : "error_overload"; CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); assertThat(e) .hasMessageThat() - .contains( - "evaluation error at :5: Function '" - + expectedOverloadId - + "' failed with arg(s) ''"); + .contains("evaluation error at :5: Function 'error' failed with arg(s) ''"); assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(e.getCause()).hasMessageThat().contains("Intentional error"); } @@ -562,13 +560,11 @@ public void plan_call_mapIndex() throws Exception { public void plan_call_noMatchingOverload_throws() throws Exception { CelAbstractSyntaxTree ast = compile("concat(b'abc', dyn_var)"); Program program = PLANNER.plan(ast); - String errorMsg; + String errorMsg = + "No matching overload for function 'concat'. Overload candidates: concat_bytes_bytes"; if (isParseOnly) { - errorMsg = - "No matching overload for function 'concat'. Overload candidates: concat_bytes_bytes," - + " bytes_concat_bytes"; - } else { - errorMsg = "No matching overload for function 'concat_bytes_bytes'"; + // Parsed-only evaluation includes both overloads as candidates due to dynamic dispatch + errorMsg += ", bytes_concat_bytes"; } CelEvaluationException e = diff --git a/runtime/src/test/resources/planner_optional_errors.baseline b/runtime/src/test/resources/planner_optional_errors.baseline new file mode 100644 index 000000000..3d59fefca --- /dev/null +++ b/runtime/src/test/resources/planner_optional_errors.baseline @@ -0,0 +1,5 @@ +Source: optional.unwrap([dyn(1)]) +=====> +bindings: {} +error: evaluation error at test_location:15: Function 'optional.unwrap' failed with arg(s) '[1]' +error_code: INTERNAL_ERROR From f2d69d9358ed6a3d04c9c50e931adda426eedb6d Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 22 Apr 2026 17:07:16 -0700 Subject: [PATCH 058/204] Add planner test coverage for policy compilation PiperOrigin-RevId: 904128325 --- .../main/java/dev/cel/bundle/CelBuilder.java | 8 +++ .../src/main/java/dev/cel/bundle/CelImpl.java | 12 +++++ .../cel/common/values/CelValueConverter.java | 4 +- .../optimizers/ConstantFoldingOptimizer.java | 4 +- .../src/test/java/dev/cel/policy/BUILD.bazel | 2 +- .../cel/policy/CelPolicyCompilerImplTest.java | 53 +++++++++++-------- 6 files changed, 56 insertions(+), 27 deletions(-) diff --git a/bundle/src/main/java/dev/cel/bundle/CelBuilder.java b/bundle/src/main/java/dev/cel/bundle/CelBuilder.java index 1dadaeb39..f603b479f 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelBuilder.java +++ b/bundle/src/main/java/dev/cel/bundle/CelBuilder.java @@ -165,6 +165,14 @@ public interface CelBuilder { @CanIgnoreReturnValue CelBuilder addFunctionBindings(Iterable bindings); + /** Adds bindings for functions that are allowed to be late-bound (resolved at execution time). */ + @CanIgnoreReturnValue + CelBuilder addLateBoundFunctions(String... lateBoundFunctionNames); + + /** Adds bindings for functions that are allowed to be late-bound (resolved at execution time). */ + @CanIgnoreReturnValue + CelBuilder addLateBoundFunctions(Iterable lateBoundFunctionNames); + /** Set the expected {@code resultType} for the type-checked expression. */ @CanIgnoreReturnValue CelBuilder setResultType(CelType resultType); diff --git a/bundle/src/main/java/dev/cel/bundle/CelImpl.java b/bundle/src/main/java/dev/cel/bundle/CelImpl.java index ae0ab2395..f6b985065 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelImpl.java +++ b/bundle/src/main/java/dev/cel/bundle/CelImpl.java @@ -281,6 +281,18 @@ public CelBuilder addFunctionBindings(Iterable lateBoundFunctionNames) { + runtimeBuilder.addLateBoundFunctions(lateBoundFunctionNames); + return this; + } + @Override public CelBuilder setResultType(CelType resultType) { checkNotNull(resultType); diff --git a/common/src/main/java/dev/cel/common/values/CelValueConverter.java b/common/src/main/java/dev/cel/common/values/CelValueConverter.java index 2af0a76cb..70d04acc8 100644 --- a/common/src/main/java/dev/cel/common/values/CelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/CelValueConverter.java @@ -117,7 +117,7 @@ protected Object normalizePrimitive(Object value) { } /** Adapts a {@link CelValue} to a plain old Java Object. */ - private static Object unwrap(CelValue celValue) { + private Object unwrap(CelValue celValue) { Preconditions.checkNotNull(celValue); if (celValue instanceof OptionalValue) { @@ -126,7 +126,7 @@ private static Object unwrap(CelValue celValue) { return Optional.empty(); } - return Optional.of(optionalValue.value()); + return Optional.of(maybeUnwrap(optionalValue.value())); } if (celValue instanceof ErrorValue) { diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java index c017911f9..8a8786ce8 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java @@ -46,7 +46,6 @@ import dev.cel.optimizer.AstMutator; import dev.cel.optimizer.CelAstOptimizer; import dev.cel.optimizer.CelOptimizationException; -import dev.cel.runtime.CelAttribute.Qualifier; import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.PartialVars; @@ -683,8 +682,7 @@ private static Object evaluateExpr(Cel cel, CelNavigableMutableExpr navigableMut .allNodes() .filter(node -> node.getKind().equals(Kind.IDENT)) .map(node -> node.expr().ident().name()) - .filter(Qualifier::isLegalIdentifier) - .map(CelAttributePattern::create) + .map(CelAttributePattern::fromQualifiedIdentifier) .collect(toImmutableList()); CelAbstractSyntaxTree ast = CelAbstractSyntaxTree.newParsedAst( diff --git a/policy/src/test/java/dev/cel/policy/BUILD.bazel b/policy/src/test/java/dev/cel/policy/BUILD.bazel index 9106caf70..8a28caee1 100644 --- a/policy/src/test/java/dev/cel/policy/BUILD.bazel +++ b/policy/src/test/java/dev/cel/policy/BUILD.bazel @@ -35,7 +35,7 @@ java_library( "//policy:validation_exception", "//runtime", "//runtime:function_binding", - "//runtime:late_function_binding", + "//testing:cel_runtime_flavor", "//testing/protos:single_file_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@maven//:com_google_guava_guava", diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java index fec5f9b94..d5254571d 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java @@ -26,9 +26,9 @@ import com.google.testing.junit.testparameterinjector.TestParameterValue; import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider; import dev.cel.bundle.Cel; +import dev.cel.bundle.CelBuilder; import dev.cel.bundle.CelEnvironment; import dev.cel.bundle.CelEnvironmentYamlParser; -import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelOptions; import dev.cel.common.types.OptionalType; @@ -45,6 +45,7 @@ import dev.cel.policy.PolicyTestHelper.TestYamlPolicy; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelLateFunctionBindings; +import dev.cel.testing.CelRuntimeFlavor; import dev.cel.testing.testdata.SingleFile; import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; import java.io.IOException; @@ -61,7 +62,12 @@ public final class CelPolicyCompilerImplTest { private static final CelEnvironmentYamlParser ENVIRONMENT_PARSER = CelEnvironmentYamlParser.newInstance(); private static final CelOptions CEL_OPTIONS = - CelOptions.current().populateMacroCalls(true).build(); + CelOptions.current() + .populateMacroCalls(true) + .enableHeterogeneousNumericComparisons(true) + .build(); + + @TestParameter public CelRuntimeFlavor runtimeFlavor; @Test public void compileYamlPolicy_success(@TestParameter TestYamlPolicy yamlPolicy) throws Exception { @@ -258,7 +264,6 @@ public void evaluateYamlPolicy_nestedRuleProducesOptionalOutput() throws Excepti CelPolicy policy = POLICY_PARSER.parse(policySource); CelAbstractSyntaxTree compiledPolicyAst = CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy); - Optional evalResult = (Optional) cel.createProgram(compiledPolicyAst).eval(); // Result is Optional> @@ -278,7 +283,12 @@ public void evaluateYamlPolicy_lateBoundFunction() throws Exception { + " return:\n" + " type_name: 'string'\n"; CelEnvironment celEnvironment = ENVIRONMENT_PARSER.parse(configSource); - Cel cel = celEnvironment.extend(newCel(), CelOptions.DEFAULT); + CelBuilder celBuilder = newCel().toCelBuilder(); + if (runtimeFlavor == CelRuntimeFlavor.PLANNER) { + celBuilder.addLateBoundFunctions("lateBoundFunc"); + } + Cel cel = celEnvironment.extend(celBuilder.build(), CEL_OPTIONS); + String policySource = "name: late_bound_function_policy\n" + "rule:\n" @@ -298,7 +308,6 @@ public void evaluateYamlPolicy_lateBoundFunction() throws Exception { (String) cel.createProgram(compiledPolicyAst) .eval((unused) -> Optional.empty(), lateFunctionBindings); - assertThat(evalResult).isEqualTo("foo" + exampleValue); } @@ -319,7 +328,6 @@ public void evaluateYamlPolicy_withSimpleVariable() throws Exception { CelAbstractSyntaxTree compiledPolicyAst = CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy); - boolean evalResult = (boolean) cel.createProgram(compiledPolicyAst).eval(); assertThat(evalResult).isFalse(); @@ -358,8 +366,9 @@ protected ImmutableList provideValues(Context context) throw } } - private static Cel newCel() { - return CelFactory.standardCelBuilder() + private Cel newCel() { + return runtimeFlavor + .builder() .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .addCompilerLibraries(CelOptionalLibrary.INSTANCE) .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) @@ -367,19 +376,21 @@ private static Cel newCel() { .addMessageTypes(TestAllTypes.getDescriptor(), SingleFile.getDescriptor()) .setOptions(CEL_OPTIONS) .addFunctionBindings( - CelFunctionBinding.from( - "locationCode_string", - String.class, - (ip) -> { - switch (ip) { - case "10.0.0.1": - return "us"; - case "10.0.0.2": - return "de"; - default: - return "ir"; - } - })) + CelFunctionBinding.fromOverloads( + "locationCode", + CelFunctionBinding.from( + "locationCode_string", + String.class, + (ip) -> { + switch (ip) { + case "10.0.0.1": + return "us"; + case "10.0.0.2": + return "de"; + default: + return "ir"; + } + }))) .build(); } From 1cd28068e3f4fe6fcc881fb8007ee3fb2796b18e Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 22 Apr 2026 17:34:49 -0700 Subject: [PATCH 059/204] Implement trace for planner PiperOrigin-RevId: 904138053 --- .../main/java/dev/cel/extensions/BUILD.bazel | 1 - .../cel/extensions/CelExtensionTestBase.java | 4 +- .../cel/extensions/CelMathExtensionsTest.java | 4 +- .../cel/policy/CelPolicyCompilerImplTest.java | 2 +- runtime/planner/BUILD.bazel | 6 + .../src/main/java/dev/cel/runtime/BUILD.bazel | 5 + .../java/dev/cel/runtime/CelRuntimeImpl.java | 56 ++++++- .../java/dev/cel/runtime/planner/BUILD.bazel | 30 +++- .../cel/runtime/planner/BlockMemoizer.java | 2 +- .../java/dev/cel/runtime/planner/EvalAnd.java | 13 +- .../cel/runtime/planner/EvalAttribute.java | 19 ++- .../dev/cel/runtime/planner/EvalBinary.java | 11 +- .../dev/cel/runtime/planner/EvalBlock.java | 21 +-- .../cel/runtime/planner/EvalConditional.java | 11 +- .../dev/cel/runtime/planner/EvalConstant.java | 11 +- .../cel/runtime/planner/EvalCreateList.java | 12 +- .../cel/runtime/planner/EvalCreateMap.java | 15 +- .../cel/runtime/planner/EvalCreateStruct.java | 12 +- .../dev/cel/runtime/planner/EvalFold.java | 11 +- .../dev/cel/runtime/planner/EvalHelpers.java | 6 +- .../runtime/planner/EvalLateBoundCall.java | 11 +- .../cel/runtime/planner/EvalOptionalOr.java | 11 +- .../runtime/planner/EvalOptionalOrValue.java | 11 +- .../planner/EvalOptionalSelectField.java | 11 +- .../java/dev/cel/runtime/planner/EvalOr.java | 13 +- .../dev/cel/runtime/planner/EvalTestOnly.java | 15 +- .../dev/cel/runtime/planner/EvalUnary.java | 11 +- .../cel/runtime/planner/EvalVarArgsCall.java | 11 +- .../cel/runtime/planner/EvalZeroArity.java | 11 +- .../cel/runtime/planner/ExecutionFrame.java | 21 ++- .../planner/InterpretableAttribute.java | 7 +- .../runtime/planner/PlannedInterpretable.java | 24 ++- .../cel/runtime/planner/PlannedProgram.java | 68 ++++++-- .../cel/runtime/planner/ProgramPlanner.java | 88 +++++----- .../src/test/java/dev/cel/runtime/BUILD.bazel | 1 + .../java/dev/cel/runtime/CelRuntimeTest.java | 155 ++++++++++++++---- 36 files changed, 483 insertions(+), 238 deletions(-) diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel index 454b2a2fd..f8e4bfc8c 100644 --- a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel @@ -122,7 +122,6 @@ java_library( ":extension_library", "//checker:checker_builder", "//common:compiler_common", - "//common:options", "//common/ast", "//common/exceptions:numeric_overflow", "//common/internal:comparison_functions", diff --git a/extensions/src/test/java/dev/cel/extensions/CelExtensionTestBase.java b/extensions/src/test/java/dev/cel/extensions/CelExtensionTestBase.java index c80ee38b6..3a509b003 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelExtensionTestBase.java +++ b/extensions/src/test/java/dev/cel/extensions/CelExtensionTestBase.java @@ -29,8 +29,8 @@ * planner runtime, along with parsed-only and checked expression evaluations for the planner. */ abstract class CelExtensionTestBase { - @TestParameter public CelRuntimeFlavor runtimeFlavor; - @TestParameter public boolean isParseOnly; + @TestParameter CelRuntimeFlavor runtimeFlavor; + @TestParameter boolean isParseOnly; @Before public void setUpBase() { diff --git a/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java index 383e50aa2..16d5c4c83 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java @@ -46,8 +46,8 @@ @RunWith(TestParameterInjector.class) public class CelMathExtensionsTest { - @TestParameter public CelRuntimeFlavor runtimeFlavor; - @TestParameter public boolean isParseOnly; + @TestParameter private CelRuntimeFlavor runtimeFlavor; + @TestParameter private boolean isParseOnly; private Cel cel; diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java index d5254571d..d4ca76324 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java @@ -67,7 +67,7 @@ public final class CelPolicyCompilerImplTest { .enableHeterogeneousNumericComparisons(true) .build(); - @TestParameter public CelRuntimeFlavor runtimeFlavor; + @TestParameter private CelRuntimeFlavor runtimeFlavor; @Test public void compileYamlPolicy_success(@TestParameter TestYamlPolicy yamlPolicy) throws Exception { diff --git a/runtime/planner/BUILD.bazel b/runtime/planner/BUILD.bazel index 8da29f270..9b5dbee6a 100644 --- a/runtime/planner/BUILD.bazel +++ b/runtime/planner/BUILD.bazel @@ -9,3 +9,9 @@ java_library( name = "program_planner", exports = ["//runtime/src/main/java/dev/cel/runtime/planner:program_planner"], ) + +java_library( + name = "planned_program", + visibility = ["//:internal"], + exports = ["//runtime/src/main/java/dev/cel/runtime/planner:planned_program"], +) diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index ef0ac71d4..0da68d548 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -853,6 +853,11 @@ java_library( "//common/values:cel_value_provider", "//common/values:combined_cel_value_provider", "//common/values:proto_message_value_provider", + "//runtime:activation", + "//runtime:interpretable", + "//runtime:proto_message_activation_factory", + "//runtime:resolved_overload", + "//runtime/planner:planned_program", "//runtime/planner:program_planner", "//runtime/standard:type", "@maven//:com_google_errorprone_error_prone_annotations", diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index 43b223fa0..4cc738b6d 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -45,12 +45,14 @@ import dev.cel.common.values.CelValueProvider; import dev.cel.common.values.CombinedCelValueProvider; import dev.cel.common.values.ProtoMessageValueProvider; +import dev.cel.runtime.planner.PlannedProgram; import dev.cel.runtime.planner.ProgramPlanner; import dev.cel.runtime.standard.TypeFunction; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.function.Function; import org.jspecify.annotations.Nullable; @@ -98,6 +100,21 @@ public Program createProgram(CelAbstractSyntaxTree ast) throws CelEvaluationExce return toRuntimeProgram(planner().plan(ast)); } + private static final CelFunctionResolver EMPTY_FUNCTION_RESOLVER = + new CelFunctionResolver() { + @Override + public Optional findOverloadMatchingArgs( + String functionName, Collection overloadIds, Object[] args) { + return Optional.empty(); + } + + @Override + public Optional findOverloadMatchingArgs( + String functionName, Object[] args) { + return Optional.empty(); + } + }; + public Program toRuntimeProgram(dev.cel.runtime.Program program) { return new Program() { @@ -119,7 +136,13 @@ public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctio @Override public Object eval(Message message) throws CelEvaluationException { - throw new UnsupportedOperationException("Not yet supported."); + PlannedProgram plannedProgram = (PlannedProgram) program; + return plannedProgram.evalOrThrow( + plannedProgram.interpretable(), + ProtoMessageActivationFactory.fromProto(message, plannedProgram.options()), + EMPTY_FUNCTION_RESOLVER, + /* partialVars= */ null, + /* listener= */ null); } @Override @@ -141,25 +164,38 @@ public Object eval(PartialVars partialVars) throws CelEvaluationException { @Override public Object trace(CelEvaluationListener listener) throws CelEvaluationException { - throw new UnsupportedOperationException("Trace is not yet supported."); + return ((PlannedProgram) program) + .trace(GlobalResolver.EMPTY, EMPTY_FUNCTION_RESOLVER, null, listener); } @Override public Object trace(Map mapValue, CelEvaluationListener listener) throws CelEvaluationException { - throw new UnsupportedOperationException("Trace is not yet supported."); + return ((PlannedProgram) program) + .trace(Activation.copyOf(mapValue), EMPTY_FUNCTION_RESOLVER, null, listener); } @Override public Object trace(Message message, CelEvaluationListener listener) throws CelEvaluationException { - throw new UnsupportedOperationException("Trace is not yet supported."); + PlannedProgram plannedProgram = (PlannedProgram) program; + return plannedProgram.evalOrThrow( + plannedProgram.interpretable(), + ProtoMessageActivationFactory.fromProto(message, plannedProgram.options()), + EMPTY_FUNCTION_RESOLVER, + /* partialVars= */ null, + listener); } @Override public Object trace(CelVariableResolver resolver, CelEvaluationListener listener) throws CelEvaluationException { - throw new UnsupportedOperationException("Trace is not yet supported."); + return ((PlannedProgram) program) + .trace( + (name) -> resolver.find(name).orElse(null), + EMPTY_FUNCTION_RESOLVER, + null, + listener); } @Override @@ -168,7 +204,12 @@ public Object trace( CelFunctionResolver lateBoundFunctionResolver, CelEvaluationListener listener) throws CelEvaluationException { - throw new UnsupportedOperationException("Trace is not yet supported."); + return ((PlannedProgram) program) + .trace( + (name) -> resolver.find(name).orElse(null), + lateBoundFunctionResolver, + null, + listener); } @Override @@ -177,7 +218,8 @@ public Object trace( CelFunctionResolver lateBoundFunctionResolver, CelEvaluationListener listener) throws CelEvaluationException { - throw new UnsupportedOperationException("Trace is not yet supported."); + return ((PlannedProgram) program) + .trace(Activation.copyOf(mapValue), lateBoundFunctionResolver, null, listener); } @Override diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index 96382b9a9..c13d5857f 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -66,17 +66,21 @@ java_library( java_library( name = "planned_program", srcs = ["PlannedProgram.java"], + tags = [ + ], deps = [ ":error_metadata", ":localized_evaluation_exception", ":planned_interpretable", "//:auto_value", "//common:options", + "//common/annotations", "//common/exceptions:runtime_exception", "//common/values", "//runtime:activation", "//runtime:evaluation_exception", "//runtime:evaluation_exception_builder", + "//runtime:evaluation_listener", "//runtime:function_resolver", "//runtime:interpretable", "//runtime:interpreter_util", @@ -85,6 +89,7 @@ java_library( "//runtime:resolved_overload", "//runtime:variable_resolver", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", ], ) @@ -93,6 +98,7 @@ java_library( srcs = ["EvalConstant.java"], deps = [ ":planned_interpretable", + "//common/ast", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", ], @@ -104,6 +110,7 @@ java_library( deps = [ ":planned_interpretable", ":qualifier", + "//common/ast", "@maven//:com_google_errorprone_error_prone_annotations", ], ) @@ -183,6 +190,7 @@ java_library( ":interpretable_attribute", ":planned_interpretable", ":qualifier", + "//common/ast", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", ], @@ -196,6 +204,7 @@ java_library( ":planned_interpretable", ":presence_test_qualifier", ":qualifier", + "//common/ast", "//runtime:evaluation_exception", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", @@ -208,6 +217,7 @@ java_library( deps = [ ":eval_helpers", ":planned_interpretable", + "//common/ast", "//common/values", "//runtime:evaluation_exception", "//runtime:interpretable", @@ -221,6 +231,7 @@ java_library( deps = [ ":eval_helpers", ":planned_interpretable", + "//common/ast", "//common/values", "//runtime:evaluation_exception", "//runtime:interpretable", @@ -234,6 +245,7 @@ java_library( deps = [ ":eval_helpers", ":planned_interpretable", + "//common/ast", "//common/values", "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", @@ -248,6 +260,7 @@ java_library( deps = [ ":eval_helpers", ":planned_interpretable", + "//common/ast", "//common/values", "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", @@ -262,6 +275,7 @@ java_library( deps = [ ":eval_helpers", ":planned_interpretable", + "//common/ast", "//common/exceptions:overload_not_found", "//common/values", "//runtime:accumulated_unknowns", @@ -278,6 +292,7 @@ java_library( deps = [ ":eval_helpers", ":planned_interpretable", + "//common/ast", "//common/values", "//runtime:accumulated_unknowns", "//runtime:interpretable", @@ -291,6 +306,7 @@ java_library( deps = [ ":eval_helpers", ":planned_interpretable", + "//common/ast", "//common/values", "//runtime:accumulated_unknowns", "//runtime:interpretable", @@ -303,6 +319,7 @@ java_library( srcs = ["EvalConditional.java"], deps = [ ":planned_interpretable", + "//common/ast", "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", @@ -316,11 +333,11 @@ java_library( deps = [ ":eval_helpers", ":planned_interpretable", + "//common/ast", "//common/types:type_providers", "//common/values", "//common/values:cel_value_provider", "//runtime:accumulated_unknowns", - "//runtime:evaluation_exception", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -333,8 +350,8 @@ java_library( deps = [ ":eval_helpers", ":planned_interpretable", + "//common/ast", "//runtime:accumulated_unknowns", - "//runtime:evaluation_exception", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -348,6 +365,7 @@ java_library( ":eval_helpers", ":localized_evaluation_exception", ":planned_interpretable", + "//common/ast", "//common/exceptions:duplicate_key", "//common/exceptions:invalid_argument", "//runtime:accumulated_unknowns", @@ -364,6 +382,7 @@ java_library( deps = [ ":activation_wrapper", ":planned_interpretable", + "//common/ast", "//common/exceptions:runtime_exception", "//common/values:mutable_map_value", "//runtime:accumulated_unknowns", @@ -421,13 +440,16 @@ java_library( deps = [ ":localized_evaluation_exception", "//common:options", + "//common/ast", "//common/exceptions:iteration_budget_exceeded", "//runtime:evaluation_exception", + "//runtime:evaluation_listener", "//runtime:function_resolver", "//runtime:interpretable", "//runtime:partial_vars", "//runtime:resolved_overload", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", ], ) @@ -437,6 +459,7 @@ java_library( deps = [ ":eval_helpers", ":planned_interpretable", + "//common/ast", "//common/exceptions:overload_not_found", "//runtime:accumulated_unknowns", "//runtime:interpretable", @@ -451,6 +474,7 @@ java_library( deps = [ ":eval_helpers", ":planned_interpretable", + "//common/ast", "//common/exceptions:overload_not_found", "//runtime:accumulated_unknowns", "//runtime:interpretable", @@ -465,6 +489,7 @@ java_library( deps = [ ":eval_helpers", ":planned_interpretable", + "//common/ast", "//common/values", "//runtime:accumulated_unknowns", "//runtime:interpretable", @@ -478,6 +503,7 @@ java_library( srcs = ["EvalBlock.java"], deps = [ ":planned_interpretable", + "//common/ast", "//runtime:evaluation_exception", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BlockMemoizer.java b/runtime/src/main/java/dev/cel/runtime/planner/BlockMemoizer.java index 978029b3d..80a0a5de0 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BlockMemoizer.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/BlockMemoizer.java @@ -61,7 +61,7 @@ Object resolveSlot(int idx, GlobalResolver resolver) { return result; } catch (CelEvaluationException e) { LocalizedEvaluationException localizedException = - new LocalizedEvaluationException(e, e.getErrorCode(), slotExprs[idx].exprId()); + new LocalizedEvaluationException(e, e.getErrorCode(), slotExprs[idx].expr().id()); slotVals[idx] = localizedException; throw localizedException; } catch (RuntimeException e) { diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java index 91f5b2ff4..11da26a50 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java @@ -17,6 +17,7 @@ import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; import com.google.common.base.Preconditions; +import dev.cel.common.ast.CelExpr; import dev.cel.common.values.ErrorValue; import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.GlobalResolver; @@ -27,7 +28,7 @@ final class EvalAnd extends PlannedInterpretable { private final PlannedInterpretable[] args; @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { ErrorValue errorValue = null; AccumulatedUnknowns unknowns = null; for (PlannedInterpretable arg : args) { @@ -47,7 +48,7 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { } else { errorValue = ErrorValue.create( - arg.exprId(), + arg.expr().id(), new IllegalArgumentException( String.format("Expected boolean value, found: %s", argVal))); } @@ -64,12 +65,12 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { return true; } - static EvalAnd create(long exprId, PlannedInterpretable[] args) { - return new EvalAnd(exprId, args); + static EvalAnd create(CelExpr expr, PlannedInterpretable[] args) { + return new EvalAnd(expr, args); } - private EvalAnd(long exprId, PlannedInterpretable[] args) { - super(exprId); + private EvalAnd(CelExpr expr, PlannedInterpretable[] args) { + super(expr); Preconditions.checkArgument(args.length == 2); this.args = args; } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java index a0a95c47a..56ea8a832 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java @@ -15,6 +15,7 @@ package dev.cel.runtime.planner; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; import dev.cel.runtime.GlobalResolver; @Immutable @@ -23,27 +24,27 @@ final class EvalAttribute extends InterpretableAttribute { private final Attribute attr; @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) { - Object resolved = attr.resolve(exprId(), resolver, frame); + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { + Object resolved = attr.resolve(expr().id(), resolver, frame); if (resolved instanceof MissingAttribute) { - ((MissingAttribute) resolved).resolve(exprId(), resolver, frame); + ((MissingAttribute) resolved).resolve(expr().id(), resolver, frame); } return resolved; } @Override - public EvalAttribute addQualifier(long exprId, Qualifier qualifier) { + public EvalAttribute addQualifier(CelExpr expr, Qualifier qualifier) { Attribute newAttribute = attr.addQualifier(qualifier); - return create(exprId, newAttribute); + return create(expr, newAttribute); } - static EvalAttribute create(long exprId, Attribute attr) { - return new EvalAttribute(exprId, attr); + static EvalAttribute create(CelExpr expr, Attribute attr) { + return new EvalAttribute(expr, attr); } - private EvalAttribute(long exprId, Attribute attr) { - super(exprId); + private EvalAttribute(CelExpr expr, Attribute attr) { + super(expr); this.attr = attr; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java index 16eba3cce..fcade7789 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java @@ -17,6 +17,7 @@ import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; import static dev.cel.runtime.planner.EvalHelpers.evalStrictly; +import dev.cel.common.ast.CelExpr; import dev.cel.common.values.CelValueConverter; import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; @@ -32,7 +33,7 @@ final class EvalBinary extends PlannedInterpretable { private final CelValueConverter celValueConverter; @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { Object argVal1 = resolvedOverload.isStrict() ? evalStrictly(arg1, resolver, frame) @@ -54,23 +55,23 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval } static EvalBinary create( - long exprId, + CelExpr expr, String functionName, CelResolvedOverload resolvedOverload, PlannedInterpretable arg1, PlannedInterpretable arg2, CelValueConverter celValueConverter) { - return new EvalBinary(exprId, functionName, resolvedOverload, arg1, arg2, celValueConverter); + return new EvalBinary(expr, functionName, resolvedOverload, arg1, arg2, celValueConverter); } private EvalBinary( - long exprId, + CelExpr expr, String functionName, CelResolvedOverload resolvedOverload, PlannedInterpretable arg1, PlannedInterpretable arg2, CelValueConverter celValueConverter) { - super(exprId); + super(expr); this.functionName = functionName; this.resolvedOverload = resolvedOverload; this.arg1 = arg1; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalBlock.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalBlock.java index 41ad4034e..eed8791d4 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalBlock.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalBlock.java @@ -15,6 +15,7 @@ package dev.cel.runtime.planner; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.GlobalResolver; @@ -28,19 +29,19 @@ final class EvalBlock extends PlannedInterpretable { private final PlannedInterpretable resultExpr; static EvalBlock create( - long exprId, PlannedInterpretable[] slotExprs, PlannedInterpretable resultExpr) { - return new EvalBlock(exprId, slotExprs, resultExpr); + CelExpr expr, PlannedInterpretable[] slotExprs, PlannedInterpretable resultExpr) { + return new EvalBlock(expr, slotExprs, resultExpr); } private EvalBlock( - long exprId, PlannedInterpretable[] slotExprs, PlannedInterpretable resultExpr) { - super(exprId); + CelExpr expr, PlannedInterpretable[] slotExprs, PlannedInterpretable resultExpr) { + super(expr); this.slotExprs = slotExprs; this.resultExpr = resultExpr; } @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { BlockMemoizer memoizer = BlockMemoizer.create(slotExprs, frame); frame.setBlockMemoizer(memoizer); return resultExpr.eval(resolver, frame); @@ -50,17 +51,17 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval static final class EvalBlockSlot extends PlannedInterpretable { private final int slotIndex; - static EvalBlockSlot create(long exprId, int slotIndex) { - return new EvalBlockSlot(exprId, slotIndex); + static EvalBlockSlot create(CelExpr expr, int slotIndex) { + return new EvalBlockSlot(expr, slotIndex); } - private EvalBlockSlot(long exprId, int slotIndex) { - super(exprId); + private EvalBlockSlot(CelExpr expr, int slotIndex) { + super(expr); this.slotIndex = slotIndex; } @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { return frame.getBlockMemoizer().resolveSlot(slotIndex, resolver); } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java index 3be1f016a..c2d730cdf 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java @@ -15,6 +15,7 @@ package dev.cel.runtime.planner; import com.google.common.base.Preconditions; +import dev.cel.common.ast.CelExpr; import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.GlobalResolver; @@ -25,7 +26,7 @@ final class EvalConditional extends PlannedInterpretable { private final PlannedInterpretable[] args; @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { PlannedInterpretable condition = args[0]; PlannedInterpretable truthy = args[1]; PlannedInterpretable falsy = args[2]; @@ -46,12 +47,12 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval return falsy.eval(resolver, frame); } - static EvalConditional create(long exprId, PlannedInterpretable[] args) { - return new EvalConditional(exprId, args); + static EvalConditional create(CelExpr expr, PlannedInterpretable[] args) { + return new EvalConditional(expr, args); } - private EvalConditional(long exprId, PlannedInterpretable[] args) { - super(exprId); + private EvalConditional(CelExpr expr, PlannedInterpretable[] args) { + super(expr); Preconditions.checkArgument(args.length == 3); this.args = args; } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java index 2bebb059b..55554069b 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java @@ -15,6 +15,7 @@ package dev.cel.runtime.planner; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; import dev.cel.runtime.GlobalResolver; @Immutable @@ -24,16 +25,16 @@ final class EvalConstant extends PlannedInterpretable { private final Object constant; @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { return constant; } - static EvalConstant create(long exprId, Object value) { - return new EvalConstant(exprId, value); + static EvalConstant create(CelExpr expr, Object value) { + return new EvalConstant(expr, value); } - private EvalConstant(long exprId, Object constant) { - super(exprId); + private EvalConstant(CelExpr expr, Object constant) { + super(expr); this.constant = constant; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java index bae1e9302..265da3ab3 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java @@ -16,8 +16,8 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; import dev.cel.runtime.AccumulatedUnknowns; -import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.GlobalResolver; import java.util.Optional; @@ -31,7 +31,7 @@ final class EvalCreateList extends PlannedInterpretable { private final boolean[] isOptional; @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(values.length); AccumulatedUnknowns unknowns = null; for (int i = 0; i < values.length; i++) { @@ -66,12 +66,12 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval return builder.build(); } - static EvalCreateList create(long exprId, PlannedInterpretable[] values, boolean[] isOptional) { - return new EvalCreateList(exprId, values, isOptional); + static EvalCreateList create(CelExpr expr, PlannedInterpretable[] values, boolean[] isOptional) { + return new EvalCreateList(expr, values, isOptional); } - private EvalCreateList(long exprId, PlannedInterpretable[] values, boolean[] isOptional) { - super(exprId); + private EvalCreateList(CelExpr expr, PlannedInterpretable[] values, boolean[] isOptional) { + super(expr); this.values = values; this.isOptional = isOptional; } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java index 1e1b831bb..8d34c10d0 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java @@ -19,6 +19,7 @@ import com.google.common.collect.Sets; import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; import dev.cel.common.exceptions.CelDuplicateKeyException; import dev.cel.common.exceptions.CelInvalidArgumentException; import dev.cel.runtime.AccumulatedUnknowns; @@ -43,7 +44,7 @@ final class EvalCreateMap extends PlannedInterpretable { private final boolean[] isOptional; @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(keys.length); HashSet keysSeen = Sets.newHashSetWithExpectedSize(keys.length); @@ -62,7 +63,7 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval || key instanceof Boolean)) { throw new LocalizedEvaluationException( new CelInvalidArgumentException("Unsupported key type: " + key), - keyInterpretable.exprId()); + keyInterpretable.expr().id()); } boolean isDuplicate = !keysSeen.add(key); @@ -80,7 +81,7 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval if (isDuplicate) { throw new LocalizedEvaluationException( - CelDuplicateKeyException.of(key), keyInterpretable.exprId()); + CelDuplicateKeyException.of(key), keyInterpretable.expr().id()); } } @@ -119,19 +120,19 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval } static EvalCreateMap create( - long exprId, + CelExpr expr, PlannedInterpretable[] keys, PlannedInterpretable[] values, boolean[] isOptional) { - return new EvalCreateMap(exprId, keys, values, isOptional); + return new EvalCreateMap(expr, keys, values, isOptional); } private EvalCreateMap( - long exprId, + CelExpr expr, PlannedInterpretable[] keys, PlannedInterpretable[] values, boolean[] isOptional) { - super(exprId); + super(expr); Preconditions.checkArgument(keys.length == values.length); Preconditions.checkArgument(keys.length == isOptional.length); this.keys = keys; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java index cdeb0c574..36485d5be 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java @@ -16,11 +16,11 @@ import com.google.common.collect.Maps; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; import dev.cel.common.types.CelType; import dev.cel.common.values.CelValueProvider; import dev.cel.common.values.StructValue; import dev.cel.runtime.AccumulatedUnknowns; -import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.GlobalResolver; import java.util.Collections; import java.util.Map; @@ -45,7 +45,7 @@ final class EvalCreateStruct extends PlannedInterpretable { private final boolean[] isOptional; @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { Map fieldValues = Maps.newHashMapWithExpectedSize(keys.length); AccumulatedUnknowns unknowns = null; for (int i = 0; i < keys.length; i++) { @@ -96,23 +96,23 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval } static EvalCreateStruct create( - long exprId, + CelExpr expr, CelValueProvider valueProvider, CelType structType, String[] keys, PlannedInterpretable[] values, boolean[] isOptional) { - return new EvalCreateStruct(exprId, valueProvider, structType, keys, values, isOptional); + return new EvalCreateStruct(expr, valueProvider, structType, keys, values, isOptional); } private EvalCreateStruct( - long exprId, + CelExpr expr, CelValueProvider valueProvider, CelType structType, String[] keys, PlannedInterpretable[] values, boolean[] isOptional) { - super(exprId); + super(expr); this.valueProvider = valueProvider; this.structType = structType; this.keys = keys; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java index 090a8bfae..2de52e982 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java @@ -17,6 +17,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; import dev.cel.common.exceptions.CelRuntimeException; import dev.cel.common.values.MutableMapValue; import dev.cel.runtime.AccumulatedUnknowns; @@ -40,7 +41,7 @@ final class EvalFold extends PlannedInterpretable { private final PlannedInterpretable result; static EvalFold create( - long exprId, + CelExpr expr, String accuVar, PlannedInterpretable accuInit, String iterVar, @@ -50,11 +51,11 @@ static EvalFold create( PlannedInterpretable loopStep, PlannedInterpretable result) { return new EvalFold( - exprId, accuVar, accuInit, iterVar, iterVar2, iterRange, loopCondition, loopStep, result); + expr, accuVar, accuInit, iterVar, iterVar2, iterRange, loopCondition, loopStep, result); } private EvalFold( - long exprId, + CelExpr expr, String accuVar, PlannedInterpretable accuInit, String iterVar, @@ -63,7 +64,7 @@ private EvalFold( PlannedInterpretable condition, PlannedInterpretable loopStep, PlannedInterpretable result) { - super(exprId); + super(expr); this.accuVar = accuVar; this.accuInit = accuInit; this.iterVar = iterVar; @@ -75,7 +76,7 @@ private EvalFold( } @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { Object iterRangeRaw = iterRange.eval(resolver, frame); if (iterRangeRaw instanceof AccumulatedUnknowns) { return iterRangeRaw; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java index 220642f4a..f9812793e 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java @@ -34,7 +34,7 @@ static Object evalNonstrictly( // Example: foo [1] && strict_err [2] -> ID 2 is propagated. return ErrorValue.create(e.exprId(), e); } catch (Exception e) { - return ErrorValue.create(interpretable.exprId(), e); + return ErrorValue.create(interpretable.expr().id(), e); } } @@ -47,11 +47,11 @@ static Object evalStrictly( throw e; } catch (CelRuntimeException e) { // Wrap with current interpretable's location - throw new LocalizedEvaluationException(e, interpretable.exprId()); + throw new LocalizedEvaluationException(e, interpretable.expr().id()); } catch (Exception e) { // Wrap generic exceptions with location throw new LocalizedEvaluationException( - e, CelErrorCode.INTERNAL_ERROR, interpretable.exprId()); + e, CelErrorCode.INTERNAL_ERROR, interpretable.expr().id()); } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java index 0bd251185..719b4af21 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java @@ -17,6 +17,7 @@ import static dev.cel.runtime.planner.EvalHelpers.evalStrictly; import com.google.common.collect.ImmutableList; +import dev.cel.common.ast.CelExpr; import dev.cel.common.exceptions.CelOverloadNotFoundException; import dev.cel.common.values.CelValueConverter; import dev.cel.runtime.AccumulatedUnknowns; @@ -35,7 +36,7 @@ final class EvalLateBoundCall extends PlannedInterpretable { private final CelValueConverter celValueConverter; @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { Object[] argVals = new Object[args.length]; AccumulatedUnknowns unknowns = null; for (int i = 0; i < args.length; i++) { @@ -59,21 +60,21 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval } static EvalLateBoundCall create( - long exprId, + CelExpr expr, String functionName, ImmutableList overloadIds, PlannedInterpretable[] args, CelValueConverter celValueConverter) { - return new EvalLateBoundCall(exprId, functionName, overloadIds, args, celValueConverter); + return new EvalLateBoundCall(expr, functionName, overloadIds, args, celValueConverter); } private EvalLateBoundCall( - long exprId, + CelExpr expr, String functionName, ImmutableList overloadIds, PlannedInterpretable[] args, CelValueConverter celValueConverter) { - super(exprId); + super(expr); this.functionName = functionName; this.overloadIds = overloadIds; this.args = args; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.java index 5ad1933d7..37e3a8ccb 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.java @@ -16,6 +16,7 @@ import com.google.common.base.Preconditions; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; import dev.cel.common.exceptions.CelOverloadNotFoundException; import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.GlobalResolver; @@ -27,7 +28,7 @@ final class EvalOptionalOr extends PlannedInterpretable { private final PlannedInterpretable rhs; @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { Object lhsValue = EvalHelpers.evalStrictly(lhs, resolver, frame); if (lhsValue instanceof AccumulatedUnknowns) { @@ -46,12 +47,12 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { return EvalHelpers.evalStrictly(rhs, resolver, frame); } - static EvalOptionalOr create(long exprId, PlannedInterpretable lhs, PlannedInterpretable rhs) { - return new EvalOptionalOr(exprId, lhs, rhs); + static EvalOptionalOr create(CelExpr expr, PlannedInterpretable lhs, PlannedInterpretable rhs) { + return new EvalOptionalOr(expr, lhs, rhs); } - private EvalOptionalOr(long exprId, PlannedInterpretable lhs, PlannedInterpretable rhs) { - super(exprId); + private EvalOptionalOr(CelExpr expr, PlannedInterpretable lhs, PlannedInterpretable rhs) { + super(expr); this.lhs = Preconditions.checkNotNull(lhs); this.rhs = Preconditions.checkNotNull(rhs); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.java index 6634d60f6..b64c6d433 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.java @@ -16,6 +16,7 @@ import com.google.common.base.Preconditions; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; import dev.cel.common.exceptions.CelOverloadNotFoundException; import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.GlobalResolver; @@ -27,7 +28,7 @@ final class EvalOptionalOrValue extends PlannedInterpretable { private final PlannedInterpretable rhs; @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { Object lhsValue = EvalHelpers.evalStrictly(lhs, resolver, frame); if (lhsValue instanceof AccumulatedUnknowns) { return lhsValue; @@ -46,12 +47,12 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { } static EvalOptionalOrValue create( - long exprId, PlannedInterpretable lhs, PlannedInterpretable rhs) { - return new EvalOptionalOrValue(exprId, lhs, rhs); + CelExpr expr, PlannedInterpretable lhs, PlannedInterpretable rhs) { + return new EvalOptionalOrValue(expr, lhs, rhs); } - private EvalOptionalOrValue(long exprId, PlannedInterpretable lhs, PlannedInterpretable rhs) { - super(exprId); + private EvalOptionalOrValue(CelExpr expr, PlannedInterpretable lhs, PlannedInterpretable rhs) { + super(expr); this.lhs = Preconditions.checkNotNull(lhs); this.rhs = Preconditions.checkNotNull(rhs); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java index 8887aa697..4122a6e8e 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java @@ -16,6 +16,7 @@ import com.google.common.base.Preconditions; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; import dev.cel.common.values.CelValueConverter; import dev.cel.common.values.SelectableValue; import dev.cel.runtime.AccumulatedUnknowns; @@ -31,7 +32,7 @@ final class EvalOptionalSelectField extends PlannedInterpretable { private final CelValueConverter celValueConverter; @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { Object operandValue = EvalHelpers.evalStrictly(operand, resolver, frame); if (operandValue instanceof Optional) { @@ -75,21 +76,21 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { } static EvalOptionalSelectField create( - long exprId, + CelExpr expr, PlannedInterpretable operand, String field, PlannedInterpretable selectAttribute, CelValueConverter celValueConverter) { - return new EvalOptionalSelectField(exprId, operand, field, selectAttribute, celValueConverter); + return new EvalOptionalSelectField(expr, operand, field, selectAttribute, celValueConverter); } private EvalOptionalSelectField( - long exprId, + CelExpr expr, PlannedInterpretable operand, String field, PlannedInterpretable selectAttribute, CelValueConverter celValueConverter) { - super(exprId); + super(expr); this.operand = Preconditions.checkNotNull(operand); this.field = Preconditions.checkNotNull(field); this.selectAttribute = Preconditions.checkNotNull(selectAttribute); diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java index 62e617d9d..849b6e7b4 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java @@ -17,6 +17,7 @@ import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; import com.google.common.base.Preconditions; +import dev.cel.common.ast.CelExpr; import dev.cel.common.values.ErrorValue; import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.GlobalResolver; @@ -27,7 +28,7 @@ final class EvalOr extends PlannedInterpretable { private final PlannedInterpretable[] args; @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { ErrorValue errorValue = null; AccumulatedUnknowns unknowns = null; for (PlannedInterpretable arg : args) { @@ -47,7 +48,7 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { } else { errorValue = ErrorValue.create( - arg.exprId(), + arg.expr().id(), new IllegalArgumentException( String.format("Expected boolean value, found: %s", argVal))); } @@ -64,12 +65,12 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) { return false; } - static EvalOr create(long exprId, PlannedInterpretable[] args) { - return new EvalOr(exprId, args); + static EvalOr create(CelExpr expr, PlannedInterpretable[] args) { + return new EvalOr(expr, args); } - private EvalOr(long exprId, PlannedInterpretable[] args) { - super(exprId); + private EvalOr(CelExpr expr, PlannedInterpretable[] args) { + super(expr); Preconditions.checkArgument(args.length == 2); this.args = args; } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalTestOnly.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalTestOnly.java index 30ecdbd83..b3d2563f0 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalTestOnly.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalTestOnly.java @@ -15,6 +15,7 @@ package dev.cel.runtime.planner; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.GlobalResolver; @@ -24,22 +25,22 @@ final class EvalTestOnly extends InterpretableAttribute { private final InterpretableAttribute attr; @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { return attr.eval(resolver, frame); } @Override - public EvalTestOnly addQualifier(long exprId, Qualifier qualifier) { + public EvalTestOnly addQualifier(CelExpr expr, Qualifier qualifier) { PresenceTestQualifier presenceTestQualifier = PresenceTestQualifier.create(qualifier.value()); - return new EvalTestOnly(exprId(), attr.addQualifier(exprId, presenceTestQualifier)); + return new EvalTestOnly(expr(), attr.addQualifier(expr, presenceTestQualifier)); } - static EvalTestOnly create(long exprId, InterpretableAttribute attr) { - return new EvalTestOnly(exprId, attr); + static EvalTestOnly create(CelExpr expr, InterpretableAttribute attr) { + return new EvalTestOnly(expr, attr); } - private EvalTestOnly(long exprId, InterpretableAttribute attr) { - super(exprId); + private EvalTestOnly(CelExpr expr, InterpretableAttribute attr) { + super(expr); this.attr = attr; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java index 57834161f..867371ff1 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java @@ -17,6 +17,7 @@ import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; import static dev.cel.runtime.planner.EvalHelpers.evalStrictly; +import dev.cel.common.ast.CelExpr; import dev.cel.common.values.CelValueConverter; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelResolvedOverload; @@ -30,7 +31,7 @@ final class EvalUnary extends PlannedInterpretable { private final CelValueConverter celValueConverter; @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { Object argVal = resolvedOverload.isStrict() ? evalStrictly(arg, resolver, frame) @@ -39,21 +40,21 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval } static EvalUnary create( - long exprId, + CelExpr expr, String functionName, CelResolvedOverload resolvedOverload, PlannedInterpretable arg, CelValueConverter celValueConverter) { - return new EvalUnary(exprId, functionName, resolvedOverload, arg, celValueConverter); + return new EvalUnary(expr, functionName, resolvedOverload, arg, celValueConverter); } private EvalUnary( - long exprId, + CelExpr expr, String functionName, CelResolvedOverload resolvedOverload, PlannedInterpretable arg, CelValueConverter celValueConverter) { - super(exprId); + super(expr); this.functionName = functionName; this.resolvedOverload = resolvedOverload; this.arg = arg; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java index fe7c6c430..4b0171b8f 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java @@ -17,6 +17,7 @@ import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; import static dev.cel.runtime.planner.EvalHelpers.evalStrictly; +import dev.cel.common.ast.CelExpr; import dev.cel.common.values.CelValueConverter; import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; @@ -34,7 +35,7 @@ final class EvalVarArgsCall extends PlannedInterpretable { private final CelValueConverter celValueConverter; @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { Object[] argVals = new Object[args.length]; AccumulatedUnknowns unknowns = null; for (int i = 0; i < args.length; i++) { @@ -55,21 +56,21 @@ public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEval } static EvalVarArgsCall create( - long exprId, + CelExpr expr, String functionName, CelResolvedOverload resolvedOverload, PlannedInterpretable[] args, CelValueConverter celValueConverter) { - return new EvalVarArgsCall(exprId, functionName, resolvedOverload, args, celValueConverter); + return new EvalVarArgsCall(expr, functionName, resolvedOverload, args, celValueConverter); } private EvalVarArgsCall( - long exprId, + CelExpr expr, String functionName, CelResolvedOverload resolvedOverload, PlannedInterpretable[] args, CelValueConverter celValueConverter) { - super(exprId); + super(expr); this.functionName = functionName; this.resolvedOverload = resolvedOverload; this.args = args; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalZeroArity.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalZeroArity.java index 7798c8253..6e35bc22b 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalZeroArity.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalZeroArity.java @@ -14,6 +14,7 @@ package dev.cel.runtime.planner; +import dev.cel.common.ast.CelExpr; import dev.cel.common.values.CelValueConverter; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelResolvedOverload; @@ -27,24 +28,24 @@ final class EvalZeroArity extends PlannedInterpretable { private final CelValueConverter celValueConverter; @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { return EvalHelpers.dispatch(functionName, resolvedOverload, celValueConverter, EMPTY_ARRAY); } static EvalZeroArity create( - long exprId, + CelExpr expr, String functionName, CelResolvedOverload resolvedOverload, CelValueConverter celValueConverter) { - return new EvalZeroArity(exprId, functionName, resolvedOverload, celValueConverter); + return new EvalZeroArity(expr, functionName, resolvedOverload, celValueConverter); } private EvalZeroArity( - long exprId, + CelExpr expr, String functionName, CelResolvedOverload resolvedOverload, CelValueConverter celValueConverter) { - super(exprId); + super(expr); this.functionName = functionName; this.resolvedOverload = resolvedOverload; this.celValueConverter = celValueConverter; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java b/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java index 282b7c83a..b67f5520c 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ExecutionFrame.java @@ -17,11 +17,13 @@ import dev.cel.common.CelOptions; import dev.cel.common.exceptions.CelIterationLimitExceededException; import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelEvaluationListener; import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.PartialVars; import java.util.Collection; import java.util.Optional; +import org.jspecify.annotations.Nullable; /** Tracks execution context within a planned program. */ final class ExecutionFrame { @@ -29,6 +31,7 @@ final class ExecutionFrame { private final int comprehensionIterationLimit; private final CelFunctionResolver functionResolver; private final PartialVars partialVars; + private final @Nullable CelEvaluationListener listener; private int iterationCount; private BlockMemoizer blockMemoizer; @@ -62,18 +65,30 @@ BlockMemoizer getBlockMemoizer() { } static ExecutionFrame create( - CelFunctionResolver functionResolver, PartialVars partialVars, CelOptions celOptions) { + CelFunctionResolver functionResolver, + CelOptions celOptions, + @Nullable PartialVars partialVars, + @Nullable CelEvaluationListener listener) { return new ExecutionFrame( - functionResolver, partialVars, celOptions.comprehensionMaxIterations()); + functionResolver, celOptions.comprehensionMaxIterations(), partialVars, listener); } Optional partialVars() { return Optional.ofNullable(partialVars); } - private ExecutionFrame(CelFunctionResolver functionResolver, PartialVars partialVars, int limit) { + @Nullable CelEvaluationListener getListener() { + return listener; + } + + private ExecutionFrame( + CelFunctionResolver functionResolver, + int limit, + @Nullable PartialVars partialVars, + @Nullable CelEvaluationListener listener) { this.comprehensionIterationLimit = limit; this.functionResolver = functionResolver; this.partialVars = partialVars; + this.listener = listener; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/InterpretableAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/InterpretableAttribute.java index 547380c11..9ce726f0e 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/InterpretableAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/InterpretableAttribute.java @@ -15,13 +15,14 @@ package dev.cel.runtime.planner; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; @Immutable abstract class InterpretableAttribute extends PlannedInterpretable { - abstract InterpretableAttribute addQualifier(long exprId, Qualifier qualifier); + abstract InterpretableAttribute addQualifier(CelExpr expr, Qualifier qualifier); - InterpretableAttribute(long exprId) { - super(exprId); + InterpretableAttribute(CelExpr expr) { + super(expr); } } \ No newline at end of file diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java b/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java index 6f3a9d7ff..8fa52db97 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java @@ -15,21 +15,33 @@ package dev.cel.runtime.planner; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelEvaluationListener; import dev.cel.runtime.GlobalResolver; @Immutable abstract class PlannedInterpretable { - private final long exprId; + private final CelExpr expr; /** Runs interpretation with the given activation which supplies name/value bindings. */ - abstract Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException; + final Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object result = evalInternal(resolver, frame); + CelEvaluationListener listener = frame.getListener(); + if (listener != null) { + listener.callback(expr, result); + } + return result; + } + + abstract Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) + throws CelEvaluationException; - long exprId() { - return exprId; + CelExpr expr() { + return expr; } - PlannedInterpretable(long exprId) { - this.exprId = exprId; + PlannedInterpretable(CelExpr expr) { + this.expr = expr; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java index 34fc34b50..1470e4909 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java @@ -17,11 +17,13 @@ import com.google.auto.value.AutoValue; import com.google.errorprone.annotations.Immutable; import dev.cel.common.CelOptions; +import dev.cel.common.annotations.Internal; import dev.cel.common.exceptions.CelRuntimeException; import dev.cel.common.values.ErrorValue; import dev.cel.runtime.Activation; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelEvaluationExceptionBuilder; +import dev.cel.runtime.CelEvaluationListener; import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.CelVariableResolver; @@ -32,10 +34,17 @@ import java.util.Collection; import java.util.Map; import java.util.Optional; - +import org.jspecify.annotations.Nullable; + +/** + * Internal implementation of a {@link Program} that executes a planned interpretable tree. + * + *

CEL-Java internals. Do not use. + */ +@Internal @Immutable @AutoValue -abstract class PlannedProgram implements Program { +public abstract class PlannedProgram implements Program { private static final CelFunctionResolver EMPTY_FUNCTION_RESOLVER = new CelFunctionResolver() { @@ -52,33 +61,51 @@ public Optional findOverloadMatchingArgs( } }; - abstract PlannedInterpretable interpretable(); + public abstract PlannedInterpretable interpretable(); abstract ErrorMetadata metadata(); - abstract CelOptions options(); + public abstract CelOptions options(); @Override public Object eval() throws CelEvaluationException { - return evalOrThrow(interpretable(), GlobalResolver.EMPTY, EMPTY_FUNCTION_RESOLVER, null); + return evalOrThrow( + interpretable(), + GlobalResolver.EMPTY, + EMPTY_FUNCTION_RESOLVER, + /* partialVars= */ null, + /* listener= */ null); } @Override public Object eval(Map mapValue) throws CelEvaluationException { - return evalOrThrow(interpretable(), Activation.copyOf(mapValue), EMPTY_FUNCTION_RESOLVER, null); + return evalOrThrow( + interpretable(), + Activation.copyOf(mapValue), + EMPTY_FUNCTION_RESOLVER, + /* partialVars= */ null, + /* listener= */ null); } @Override public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctionResolver) throws CelEvaluationException { return evalOrThrow( - interpretable(), Activation.copyOf(mapValue), lateBoundFunctionResolver, null); + interpretable(), + Activation.copyOf(mapValue), + lateBoundFunctionResolver, + /* partialVars= */ null, + /* listener= */ null); } @Override public Object eval(CelVariableResolver resolver) throws CelEvaluationException { return evalOrThrow( - interpretable(), (name) -> resolver.find(name).orElse(null), EMPTY_FUNCTION_RESOLVER, null); + interpretable(), + (name) -> resolver.find(name).orElse(null), + EMPTY_FUNCTION_RESOLVER, + /* partialVars= */ null, + /* listener= */ null); } @Override @@ -88,7 +115,8 @@ public Object eval(CelVariableResolver resolver, CelFunctionResolver lateBoundFu interpretable(), (name) -> resolver.find(name).orElse(null), lateBoundFunctionResolver, - null); + /* partialVars= */ null, + /* listener= */ null); } @Override @@ -97,17 +125,20 @@ public Object eval(PartialVars partialVars) throws CelEvaluationException { interpretable(), (name) -> partialVars.resolver().find(name).orElse(null), EMPTY_FUNCTION_RESOLVER, - partialVars); + partialVars, + /* listener= */ null); } - private Object evalOrThrow( + public Object evalOrThrow( PlannedInterpretable interpretable, GlobalResolver resolver, CelFunctionResolver functionResolver, - PartialVars partialVars) + @Nullable PartialVars partialVars, + @Nullable CelEvaluationListener listener) throws CelEvaluationException { try { - ExecutionFrame frame = ExecutionFrame.create(functionResolver, partialVars, options()); + ExecutionFrame frame = + ExecutionFrame.create(functionResolver, options(), partialVars, listener); Object evalResult = interpretable.eval(resolver, frame); if (evalResult instanceof ErrorValue) { ErrorValue errorValue = (ErrorValue) evalResult; @@ -116,10 +147,19 @@ private Object evalOrThrow( return InterpreterUtil.maybeAdaptToCelUnknownSet(evalResult); } catch (RuntimeException e) { - throw newCelEvaluationException(interpretable.exprId(), e); + throw newCelEvaluationException(interpretable.expr().id(), e); } } + public Object trace( + GlobalResolver resolver, + CelFunctionResolver functionResolver, + PartialVars partialVars, + CelEvaluationListener listener) + throws CelEvaluationException { + return evalOrThrow(interpretable(), resolver, functionResolver, partialVars, listener); + } + private CelEvaluationException newCelEvaluationException(long exprId, Exception e) { CelEvaluationExceptionBuilder builder; if (e instanceof LocalizedEvaluationException) { diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index a0b74fc99..affe64381 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -93,7 +93,7 @@ public Program plan(CelAbstractSyntaxTree ast) throws CelEvaluationException { private PlannedInterpretable plan(CelExpr celExpr, PlannerContext ctx) { switch (celExpr.getKind()) { case CONSTANT: - return planConstant(celExpr.id(), celExpr.constant()); + return planConstant(celExpr, celExpr.constant()); case IDENT: return planIdent(celExpr, ctx); case SELECT: @@ -123,35 +123,34 @@ private PlannedInterpretable planSelect(CelExpr celExpr, PlannerContext ctx) { if (operand instanceof EvalAttribute) { attribute = (EvalAttribute) operand; } else { - attribute = - EvalAttribute.create(celExpr.id(), attributeFactory.newRelativeAttribute(operand)); + attribute = EvalAttribute.create(celExpr, attributeFactory.newRelativeAttribute(operand)); } if (select.testOnly()) { - attribute = EvalTestOnly.create(celExpr.id(), attribute); + attribute = EvalTestOnly.create(celExpr, attribute); } Qualifier qualifier = StringQualifier.create(select.field()); - return attribute.addQualifier(celExpr.id(), qualifier); + return attribute.addQualifier(celExpr, qualifier); } - private PlannedInterpretable planConstant(long exprId, CelConstant celConstant) { + private PlannedInterpretable planConstant(CelExpr expr, CelConstant celConstant) { switch (celConstant.getKind()) { case NULL_VALUE: - return EvalConstant.create(exprId, celConstant.nullValue()); + return EvalConstant.create(expr, celConstant.nullValue()); case BOOLEAN_VALUE: - return EvalConstant.create(exprId, celConstant.booleanValue()); + return EvalConstant.create(expr, celConstant.booleanValue()); case INT64_VALUE: - return EvalConstant.create(exprId, celConstant.int64Value()); + return EvalConstant.create(expr, celConstant.int64Value()); case UINT64_VALUE: - return EvalConstant.create(exprId, celConstant.uint64Value()); + return EvalConstant.create(expr, celConstant.uint64Value()); case DOUBLE_VALUE: - return EvalConstant.create(exprId, celConstant.doubleValue()); + return EvalConstant.create(expr, celConstant.doubleValue()); case STRING_VALUE: - return EvalConstant.create(exprId, celConstant.stringValue()); + return EvalConstant.create(expr, celConstant.stringValue()); case BYTES_VALUE: - return EvalConstant.create(exprId, celConstant.bytesValue()); + return EvalConstant.create(expr, celConstant.bytesValue()); default: throw new IllegalStateException("Unsupported kind: " + celConstant.getKind()); } @@ -160,29 +159,29 @@ private PlannedInterpretable planConstant(long exprId, CelConstant celConstant) private PlannedInterpretable planIdent(CelExpr celExpr, PlannerContext ctx) { CelReference ref = ctx.referenceMap().get(celExpr.id()); if (ref != null) { - return planCheckedIdent(celExpr.id(), ref, ctx.typeMap()); + return planCheckedIdent(celExpr, ref, ctx.typeMap()); } String identName = celExpr.ident().name(); - PlannedInterpretable blockSlot = maybeInterceptBlockSlot(celExpr.id(), identName).orElse(null); + PlannedInterpretable blockSlot = maybeInterceptBlockSlot(celExpr, identName).orElse(null); if (blockSlot != null) { return blockSlot; } if (ctx.isLocalVar(identName)) { - return EvalAttribute.create(celExpr.id(), attributeFactory.newAbsoluteAttribute(identName)); + return EvalAttribute.create(celExpr, attributeFactory.newAbsoluteAttribute(identName)); } - return EvalAttribute.create(celExpr.id(), attributeFactory.newMaybeAttribute(identName)); + return EvalAttribute.create(celExpr, attributeFactory.newMaybeAttribute(identName)); } private PlannedInterpretable planCheckedIdent( - long id, CelReference identRef, ImmutableMap typeMap) { + CelExpr expr, CelReference identRef, ImmutableMap typeMap) { if (identRef.value().isPresent()) { - return planConstant(id, identRef.value().get()); + return planConstant(expr, identRef.value().get()); } - CelType type = typeMap.get(id); + CelType type = typeMap.get(expr.id()); if (type.kind().equals(CelKind.TYPE)) { TypeType identType = typeProvider @@ -198,19 +197,19 @@ private PlannedInterpretable planCheckedIdent( () -> new NoSuchElementException( "Reference to an undefined type: " + identRef.name())); - return EvalConstant.create(id, identType); + return EvalConstant.create(expr, identType); } String identName = identRef.name(); - PlannedInterpretable blockSlot = maybeInterceptBlockSlot(id, identName).orElse(null); + PlannedInterpretable blockSlot = maybeInterceptBlockSlot(expr, identName).orElse(null); if (blockSlot != null) { return blockSlot; } - return EvalAttribute.create(id, attributeFactory.newAbsoluteAttribute(identRef.name())); + return EvalAttribute.create(expr, attributeFactory.newAbsoluteAttribute(identRef.name())); } - private Optional maybeInterceptBlockSlot(long id, String identName) { + private Optional maybeInterceptBlockSlot(CelExpr expr, String identName) { if (!identName.startsWith("@index")) { return Optional.empty(); } @@ -222,7 +221,7 @@ private Optional maybeInterceptBlockSlot(long id, String i if (slotIndex < 0) { throw new IllegalArgumentException("Negative block slot index: " + identName); } - return Optional.of(EvalBlock.EvalBlockSlot.create(id, slotIndex)); + return Optional.of(EvalBlock.EvalBlockSlot.create(expr, slotIndex)); } catch (NumberFormatException e) { throw new IllegalArgumentException("Invalid block slot index: " + identName, e); } @@ -260,11 +259,11 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { if (operator != null) { switch (operator) { case LOGICAL_OR: - return EvalOr.create(expr.id(), evaluatedArgs); + return EvalOr.create(expr, evaluatedArgs); case LOGICAL_AND: - return EvalAnd.create(expr.id(), evaluatedArgs); + return EvalAnd.create(expr, evaluatedArgs); case CONDITIONAL: - return EvalConditional.create(expr.id(), evaluatedArgs); + return EvalConditional.create(expr, evaluatedArgs); default: // fall-through } @@ -303,18 +302,18 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { } return EvalLateBoundCall.create( - expr.id(), functionName, overloadIds, evaluatedArgs, celValueConverter); + expr, functionName, overloadIds, evaluatedArgs, celValueConverter); } switch (argCount) { case 0: - return EvalZeroArity.create(expr.id(), functionName, resolvedOverload, celValueConverter); + return EvalZeroArity.create(expr, functionName, resolvedOverload, celValueConverter); case 1: return EvalUnary.create( - expr.id(), functionName, resolvedOverload, evaluatedArgs[0], celValueConverter); + expr, functionName, resolvedOverload, evaluatedArgs[0], celValueConverter); case 2: return EvalBinary.create( - expr.id(), + expr, functionName, resolvedOverload, evaluatedArgs[0], @@ -322,7 +321,7 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { celValueConverter); default: return EvalVarArgsCall.create( - expr.id(), functionName, resolvedOverload, evaluatedArgs, celValueConverter); + expr, functionName, resolvedOverload, evaluatedArgs, celValueConverter); } } @@ -345,7 +344,7 @@ private Optional maybeInterceptBlockCall( slotExprs[i] = plan(exprList.elements().get(i), ctx); } PlannedInterpretable resultExpr = plan(blockCall.args().get(1), ctx); - return Optional.of(EvalBlock.create(expr.id(), slotExprs, resultExpr)); + return Optional.of(EvalBlock.create(expr, slotExprs, resultExpr)); } /** @@ -368,14 +367,13 @@ private Optional maybeInterceptOptionalCalls( switch (functionName) { case "or": if (overloadId.isEmpty() || overloadId.equals("optional_or_optional")) { - return Optional.of(EvalOptionalOr.create(expr.id(), evaluatedArgs[0], evaluatedArgs[1])); + return Optional.of(EvalOptionalOr.create(expr, evaluatedArgs[0], evaluatedArgs[1])); } return Optional.empty(); case "orValue": if (overloadId.isEmpty() || overloadId.equals("optional_orValue_value")) { - return Optional.of( - EvalOptionalOrValue.create(expr.id(), evaluatedArgs[0], evaluatedArgs[1])); + return Optional.of(EvalOptionalOrValue.create(expr, evaluatedArgs[0], evaluatedArgs[1])); } return Optional.empty(); @@ -390,15 +388,14 @@ private Optional maybeInterceptOptionalCalls( attribute = (EvalAttribute) evaluatedArgs[0]; } else { attribute = - EvalAttribute.create( - expr.id(), attributeFactory.newRelativeAttribute(evaluatedArgs[0])); + EvalAttribute.create(expr, attributeFactory.newRelativeAttribute(evaluatedArgs[0])); } Qualifier qualifier = StringQualifier.create(field); - PlannedInterpretable selectAttribute = attribute.addQualifier(expr.id(), qualifier); + PlannedInterpretable selectAttribute = attribute.addQualifier(expr, qualifier); return Optional.of( EvalOptionalSelectField.create( - expr.id(), evaluatedArgs[0], field, selectAttribute, celValueConverter)); + expr, evaluatedArgs[0], field, selectAttribute, celValueConverter)); } return Optional.empty(); @@ -420,8 +417,7 @@ private PlannedInterpretable planCreateStruct(CelExpr celExpr, PlannerContext ct isOptional[i] = entry.optionalEntry(); } - return EvalCreateStruct.create( - celExpr.id(), valueProvider, structType, keys, values, isOptional); + return EvalCreateStruct.create(celExpr, valueProvider, structType, keys, values, isOptional); } private PlannedInterpretable planCreateList(CelExpr celExpr, PlannerContext ctx) { @@ -438,7 +434,7 @@ private PlannedInterpretable planCreateList(CelExpr celExpr, PlannerContext ctx) isOptional[optionalIndex] = true; } - return EvalCreateList.create(celExpr.id(), values, isOptional); + return EvalCreateList.create(celExpr, values, isOptional); } private PlannedInterpretable planCreateMap(CelExpr celExpr, PlannerContext ctx) { @@ -456,7 +452,7 @@ private PlannedInterpretable planCreateMap(CelExpr celExpr, PlannerContext ctx) isOptional[i] = entry.optionalEntry(); } - return EvalCreateMap.create(celExpr.id(), keys, values, isOptional); + return EvalCreateMap.create(celExpr, keys, values, isOptional); } private PlannedInterpretable planComprehension(CelExpr expr, PlannerContext ctx) { @@ -477,7 +473,7 @@ private PlannedInterpretable planComprehension(CelExpr expr, PlannerContext ctx) ctx.popLocalVars(comprehension.accuVar()); return EvalFold.create( - expr.id(), + expr, comprehension.accuVar(), accuInit, comprehension.iterVar(), diff --git a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel index 7cd24f040..9461e5e6a 100644 --- a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel @@ -89,6 +89,7 @@ java_library( "//runtime/standard:not_strictly_false", "//runtime/standard:standard_overload", "//runtime/standard:subtract", + "//testing:cel_runtime_flavor", "//testing/protos:message_with_enum_cel_java_proto", "//testing/protos:message_with_enum_java_proto", "//testing/protos:multi_file_cel_java_proto", diff --git a/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java b/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java index c7f142602..3e29a00db 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java @@ -16,6 +16,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import static org.junit.Assume.assumeTrue; import com.google.api.expr.v1alpha1.Constant; import com.google.api.expr.v1alpha1.Expr; @@ -51,6 +52,7 @@ import dev.cel.extensions.CelExtensions; import dev.cel.parser.CelStandardMacro; import dev.cel.parser.CelUnparserFactory; +import dev.cel.testing.CelRuntimeFlavor; import java.util.List; import java.util.Map; import java.util.Optional; @@ -61,6 +63,8 @@ @RunWith(TestParameterInjector.class) public class CelRuntimeTest { + @TestParameter private CelRuntimeFlavor runtimeFlavor; + @Test public void evaluate_anyPackedEqualityUsingProtoDifferencer_success() throws Exception { Cel cel = @@ -273,7 +277,8 @@ public void trace_callExpr_identifyFalseBranch() throws Exception { } }; Cel cel = - CelFactory.standardCelBuilder() + runtimeFlavor + .builder() .addVar("a", SimpleType.INT) .addVar("b", SimpleType.INT) .addVar("c", SimpleType.INT) @@ -297,7 +302,7 @@ public void trace_constant() throws Exception { assertThat(res).isEqualTo("hello world"); assertThat(expr.constant().getKind()).isEqualTo(CelConstant.Kind.STRING_VALUE); }; - Cel cel = CelFactory.standardCelBuilder().build(); + Cel cel = runtimeFlavor.builder().build(); CelAbstractSyntaxTree ast = cel.compile("'hello world'").getAst(); String result = (String) cel.createProgram(ast).trace(listener); @@ -312,7 +317,7 @@ public void trace_ident() throws Exception { assertThat(res).isEqualTo("test"); assertThat(expr.ident().name()).isEqualTo("a"); }; - Cel cel = CelFactory.standardCelBuilder().addVar("a", SimpleType.STRING).build(); + Cel cel = runtimeFlavor.builder().addVar("a", SimpleType.STRING).build(); CelAbstractSyntaxTree ast = cel.compile("a").getAst(); String result = (String) cel.createProgram(ast).trace(ImmutableMap.of("a", "test"), listener); @@ -330,7 +335,8 @@ public void trace_select() throws Exception { } }; Cel cel = - CelFactory.standardCelBuilder() + runtimeFlavor + .builder() .addMessageTypes(TestAllTypes.getDescriptor()) .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .build(); @@ -350,7 +356,8 @@ public void trace_struct() throws Exception { .isEqualTo("cel.expr.conformance.proto3.TestAllTypes"); }; Cel cel = - CelFactory.standardCelBuilder() + runtimeFlavor + .builder() .addMessageTypes(TestAllTypes.getDescriptor()) .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .build(); @@ -371,7 +378,7 @@ public void trace_list() throws Exception { assertThat(expr.list().elements()).hasSize(3); } }; - Cel cel = CelFactory.standardCelBuilder().build(); + Cel cel = runtimeFlavor.builder().build(); CelAbstractSyntaxTree ast = cel.compile("[1, 2, 3]").getAst(); List result = (List) cel.createProgram(ast).trace(listener); @@ -389,7 +396,7 @@ public void trace_map() throws Exception { assertThat(expr.map().entries()).hasSize(1); } }; - Cel cel = CelFactory.standardCelBuilder().build(); + Cel cel = runtimeFlavor.builder().build(); CelAbstractSyntaxTree ast = cel.compile("{1: 'a'}").getAst(); Map result = (Map) cel.createProgram(ast).trace(listener); @@ -405,8 +412,7 @@ public void trace_comprehension() throws Exception { assertThat(expr.comprehension().iterVar()).isEqualTo("i"); } }; - Cel cel = - CelFactory.standardCelBuilder().setStandardMacros(CelStandardMacro.STANDARD_MACROS).build(); + Cel cel = runtimeFlavor.builder().setStandardMacros(CelStandardMacro.STANDARD_MACROS).build(); CelAbstractSyntaxTree ast = cel.compile("[true].exists(i, i)").getAst(); boolean result = (boolean) cel.createProgram(ast).trace(listener); @@ -422,7 +428,8 @@ public void trace_withMessageInput() throws Exception { assertThat(expr.ident().name()).isEqualTo("single_int64"); }; Cel cel = - CelFactory.standardCelBuilder() + runtimeFlavor + .builder() .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("single_int64", SimpleType.INT) .build(); @@ -444,7 +451,8 @@ public void trace_withVariableResolver() throws Exception { assertThat(expr.ident().name()).isEqualTo("variable"); }; Cel cel = - CelFactory.standardCelBuilder() + runtimeFlavor + .builder() .addMessageTypes(TestAllTypes.getDescriptor()) .addVar("variable", SimpleType.STRING) .build(); @@ -461,6 +469,8 @@ public void trace_withVariableResolver() throws Exception { public void trace_shortCircuitingDisabled_logicalAndAllBranchesVisited( @TestParameter boolean first, @TestParameter boolean second, @TestParameter boolean third) throws Exception { + // TODO: Implement exhaustive eval + assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); String expression = String.format("%s && %s && %s", first, second, third); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = @@ -470,12 +480,22 @@ public void trace_shortCircuitingDisabled_logicalAndAllBranchesVisited( } }; Cel celWithShortCircuit = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableShortCircuiting(true).build()) + runtimeFlavor + .builder() + .setOptions( + CelOptions.current() + .enableShortCircuiting(true) + .enableHeterogeneousNumericComparisons(true) + .build()) .build(); Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableShortCircuiting(false).build()) + runtimeFlavor + .builder() + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) .build(); CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); @@ -496,6 +516,8 @@ public void trace_shortCircuitingDisabled_logicalAndAllBranchesVisited( @TestParameters("{source: 'x && false && false'}") public void trace_shortCircuitingDisabledWithUnknownsAndedToFalse_returnsFalse(String source) throws Exception { + // TODO: Implement exhaustive eval + assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -509,9 +531,14 @@ public void trace_shortCircuitingDisabledWithUnknownsAndedToFalse_returnsFalse(S } }; Cel cel = - CelFactory.standardCelBuilder() + runtimeFlavor + .builder() .addVar("x", SimpleType.BOOL) - .setOptions(CelOptions.current().enableShortCircuiting(false).build()) + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) .build(); CelAbstractSyntaxTree ast = cel.compile(source).getAst(); @@ -527,6 +554,8 @@ public void trace_shortCircuitingDisabledWithUnknownsAndedToFalse_returnsFalse(S @TestParameters("{source: 'x && true && true'}") public void trace_shortCircuitingDisabledWithUnknownAndedToTrue_returnsUnknown(String source) throws Exception { + // TODO: Implement exhaustive eval + assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -536,9 +565,14 @@ public void trace_shortCircuitingDisabledWithUnknownAndedToTrue_returnsUnknown(S } }; Cel cel = - CelFactory.standardCelBuilder() + runtimeFlavor + .builder() .addVar("x", SimpleType.BOOL) - .setOptions(CelOptions.current().enableShortCircuiting(false).build()) + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) .build(); CelAbstractSyntaxTree ast = cel.compile(source).getAst(); @@ -552,6 +586,8 @@ public void trace_shortCircuitingDisabledWithUnknownAndedToTrue_returnsUnknown(S public void trace_shortCircuitingDisabled_logicalOrAllBranchesVisited( @TestParameter boolean first, @TestParameter boolean second, @TestParameter boolean third) throws Exception { + // TODO: Implement exhaustive eval + assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); String expression = String.format("%s || %s || %s", first, second, third); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = @@ -561,12 +597,22 @@ public void trace_shortCircuitingDisabled_logicalOrAllBranchesVisited( } }; Cel celWithShortCircuit = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableShortCircuiting(true).build()) + runtimeFlavor + .builder() + .setOptions( + CelOptions.current() + .enableShortCircuiting(true) + .enableHeterogeneousNumericComparisons(true) + .build()) .build(); Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableShortCircuiting(false).build()) + runtimeFlavor + .builder() + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) .build(); CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); @@ -587,6 +633,8 @@ public void trace_shortCircuitingDisabled_logicalOrAllBranchesVisited( @TestParameters("{source: 'x || false || false'}") public void trace_shortCircuitingDisabledWithUnknownsOredToFalse_returnsUnknown(String source) throws Exception { + // TODO: Implement exhaustive eval + assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -596,9 +644,14 @@ public void trace_shortCircuitingDisabledWithUnknownsOredToFalse_returnsUnknown( } }; Cel cel = - CelFactory.standardCelBuilder() + runtimeFlavor + .builder() .addVar("x", SimpleType.BOOL) - .setOptions(CelOptions.current().enableShortCircuiting(false).build()) + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) .build(); CelAbstractSyntaxTree ast = cel.compile(source).getAst(); @@ -614,6 +667,8 @@ public void trace_shortCircuitingDisabledWithUnknownsOredToFalse_returnsUnknown( @TestParameters("{source: 'x || true || true'}") public void trace_shortCircuitingDisabledWithUnknownOredToTrue_returnsTrue(String source) throws Exception { + // TODO: Implement exhaustive eval + assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -627,9 +682,14 @@ public void trace_shortCircuitingDisabledWithUnknownOredToTrue_returnsTrue(Strin } }; Cel cel = - CelFactory.standardCelBuilder() + runtimeFlavor + .builder() .addVar("x", SimpleType.BOOL) - .setOptions(CelOptions.current().enableShortCircuiting(false).build()) + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) .build(); CelAbstractSyntaxTree ast = cel.compile(source).getAst(); @@ -641,6 +701,7 @@ public void trace_shortCircuitingDisabledWithUnknownOredToTrue_returnsTrue(Strin @Test public void trace_shortCircuitingDisabled_ternaryAllBranchesVisited() throws Exception { + assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -649,8 +710,13 @@ public void trace_shortCircuitingDisabled_ternaryAllBranchesVisited() throws Exc } }; Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableShortCircuiting(false).build()) + runtimeFlavor + .builder() + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) .build(); CelAbstractSyntaxTree ast = cel.compile("true ? false : true").getAst(); @@ -665,6 +731,8 @@ public void trace_shortCircuitingDisabled_ternaryAllBranchesVisited() throws Exc @TestParameters("{source: 'true ? x : false'}") @TestParameters("{source: 'x ? true : false'}") public void trace_shortCircuitingDisabled_ternaryWithUnknowns(String source) throws Exception { + // TODO: Implement exhaustive eval + assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -674,9 +742,14 @@ public void trace_shortCircuitingDisabled_ternaryWithUnknowns(String source) thr } }; Cel cel = - CelFactory.standardCelBuilder() + runtimeFlavor + .builder() .addVar("x", SimpleType.BOOL) - .setOptions(CelOptions.current().enableShortCircuiting(false).build()) + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) .build(); CelAbstractSyntaxTree ast = cel.compile(source).getAst(); @@ -697,6 +770,8 @@ public void trace_shortCircuitingDisabled_ternaryWithUnknowns(String source) thr "{expression: 'true ? true : (1 / 0) > 2', firstVisited: true, secondVisited: true}") public void trace_shortCircuitingDisabled_ternaryWithError( String expression, boolean firstVisited, boolean secondVisited) throws Exception { + // TODO: Implement exhaustive eval + assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -705,12 +780,22 @@ public void trace_shortCircuitingDisabled_ternaryWithError( } }; Cel celWithShortCircuit = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableShortCircuiting(true).build()) + runtimeFlavor + .builder() + .setOptions( + CelOptions.current() + .enableShortCircuiting(true) + .enableHeterogeneousNumericComparisons(true) + .build()) .build(); Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableShortCircuiting(false).build()) + runtimeFlavor + .builder() + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) .build(); CelAbstractSyntaxTree ast = cel.compile(expression).getAst(); From 1e1d8ea0c4db9abe689d9b8327b8ace42c496f4c Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 22 Apr 2026 20:11:32 -0700 Subject: [PATCH 060/204] Implement exhaustive eval for planner PiperOrigin-RevId: 904185953 --- .../main/java/dev/cel/runtime/CelRuntime.java | 8 + .../java/dev/cel/runtime/CelRuntimeImpl.java | 11 ++ .../java/dev/cel/runtime/ProgramImpl.java | 9 + .../java/dev/cel/runtime/planner/BUILD.bazel | 46 +++++ .../runtime/planner/EvalExhaustiveAnd.java | 92 +++++++++ .../planner/EvalExhaustiveConditional.java | 68 +++++++ .../cel/runtime/planner/EvalExhaustiveOr.java | 92 +++++++++ .../runtime/planner/PlannedInterpretable.java | 3 +- .../cel/runtime/planner/ProgramPlanner.java | 12 +- .../src/test/java/dev/cel/runtime/BUILD.bazel | 1 + .../java/dev/cel/runtime/CelRuntimeTest.java | 174 +++++++++++++++--- 11 files changed, 485 insertions(+), 31 deletions(-) create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveAnd.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveConditional.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveOr.java diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntime.java b/runtime/src/main/java/dev/cel/runtime/CelRuntime.java index 416bca132..1e7fdcac8 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntime.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntime.java @@ -90,6 +90,14 @@ Object trace( CelEvaluationListener listener) throws CelEvaluationException; + /** + * Trace evaluates a compiled program using {@code partialVars} as the source of input variables + * and unknown attribute patterns. The listener is invoked as evaluation progresses through the + * AST. + */ + Object trace(PartialVars partialVars, CelEvaluationListener listener) + throws CelEvaluationException; + /** * Advance evaluation based on the current unknown context. * diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index 4cc738b6d..ed203d612 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -222,6 +222,17 @@ public Object trace( .trace(Activation.copyOf(mapValue), lateBoundFunctionResolver, null, listener); } + @Override + public Object trace(PartialVars partialVars, CelEvaluationListener listener) + throws CelEvaluationException { + return ((PlannedProgram) program) + .trace( + (name) -> partialVars.resolver().find(name).orElse(null), + EMPTY_FUNCTION_RESOLVER, + partialVars, + listener); + } + @Override public Object advanceEvaluation(UnknownContext context) throws CelEvaluationException { throw new UnsupportedOperationException("Unsupported operation."); diff --git a/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java b/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java index c9f4d083b..2543a9525 100644 --- a/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java @@ -110,6 +110,15 @@ public Object trace( return evalInternal(Activation.copyOf(mapValue), lateBoundFunctionResolver, listener); } + @Override + public Object trace(PartialVars partialVars, CelEvaluationListener listener) + throws CelEvaluationException { + return evalInternal( + UnknownContext.create(partialVars.resolver(), partialVars.unknowns()), + /* lateBoundFunctionResolver= */ Optional.empty(), + Optional.of(listener)); + } + @Override public Object advanceEvaluation(UnknownContext context) throws CelEvaluationException { return evalInternal(context, Optional.empty(), Optional.empty()); diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index c13d5857f..801e56d73 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -24,6 +24,9 @@ java_library( ":eval_create_list", ":eval_create_map", ":eval_create_struct", + ":eval_exhaustive_and", + ":eval_exhaustive_conditional", + ":eval_exhaustive_or", ":eval_fold", ":eval_late_bound_call", ":eval_optional_or", @@ -446,6 +449,7 @@ java_library( "//runtime:evaluation_listener", "//runtime:function_resolver", "//runtime:interpretable", + "//runtime:interpreter_util", "//runtime:partial_vars", "//runtime:resolved_overload", "@maven//:com_google_errorprone_error_prone_annotations", @@ -498,6 +502,48 @@ java_library( ], ) +java_library( + name = "eval_exhaustive_and", + srcs = ["EvalExhaustiveAnd.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "eval_exhaustive_or", + srcs = ["EvalExhaustiveOr.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "eval_exhaustive_conditional", + srcs = ["EvalExhaustiveConditional.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//runtime:accumulated_unknowns", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + java_library( name = "eval_block", srcs = ["EvalBlock.java"], diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveAnd.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveAnd.java new file mode 100644 index 000000000..ac3d07200 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveAnd.java @@ -0,0 +1,92 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.GlobalResolver; + +/** + * Implementation of logical AND with exhaustive evaluation (non-short-circuiting). + * + *

It evaluates all arguments, but prioritizes a false result over unknowns and errors to + * maintain semantic consistency with short-circuiting evaluation. + */ +@Immutable +final class EvalExhaustiveAnd extends PlannedInterpretable { + + @SuppressWarnings("Immutable") + private final PlannedInterpretable[] args; + + @Override + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { + AccumulatedUnknowns accumulatedUnknowns = null; + ErrorValue errorValue = null; + boolean hasFalse = false; + + for (PlannedInterpretable arg : args) { + Object argVal = evalNonstrictly(arg, resolver, frame); + if (argVal instanceof Boolean) { + if (!((boolean) argVal)) { + hasFalse = true; + } + } + + // If we already encountered a false, we do not need to accumulate unknowns or errors + // from subsequent terms because the final result will be false anyway. + if (hasFalse) { + continue; + } + + if (argVal instanceof AccumulatedUnknowns) { + accumulatedUnknowns = + accumulatedUnknowns == null + ? (AccumulatedUnknowns) argVal + : accumulatedUnknowns.merge((AccumulatedUnknowns) argVal); + } else if (argVal instanceof ErrorValue) { + if (errorValue == null) { + errorValue = (ErrorValue) argVal; + } + } + } + + if (hasFalse) { + return false; + } + + if (accumulatedUnknowns != null) { + return accumulatedUnknowns; + } + + if (errorValue != null) { + return errorValue; + } + + return true; + } + + static EvalExhaustiveAnd create(CelExpr expr, PlannedInterpretable[] args) { + return new EvalExhaustiveAnd(expr, args); + } + + private EvalExhaustiveAnd(CelExpr expr, PlannedInterpretable[] args) { + super(expr); + this.args = args; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveConditional.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveConditional.java new file mode 100644 index 000000000..01e242c0f --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveConditional.java @@ -0,0 +1,68 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; +import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.GlobalResolver; + +/** + * Implementation of conditional operator (ternary) with exhaustive evaluation + * (non-short-circuiting). + * + *

It evaluates all three arguments (condition, truthy, and falsy branches) but returns the + * result based on the condition, maintaining semantic consistency with short-circuiting evaluation. + */ +@Immutable +final class EvalExhaustiveConditional extends PlannedInterpretable { + + @SuppressWarnings("Immutable") + private final PlannedInterpretable[] args; + + @Override + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + PlannedInterpretable condition = args[0]; + PlannedInterpretable truthy = args[1]; + PlannedInterpretable falsy = args[2]; + + Object condResult = condition.eval(resolver, frame); + Object truthyVal = evalNonstrictly(truthy, resolver, frame); + Object falsyVal = evalNonstrictly(falsy, resolver, frame); + + if (condResult instanceof AccumulatedUnknowns) { + return condResult; + } + + if (!(condResult instanceof Boolean)) { + throw new IllegalArgumentException( + String.format("Expected boolean value, found :%s", condResult)); + } + + return (boolean) condResult ? truthyVal : falsyVal; + } + + static EvalExhaustiveConditional create(CelExpr expr, PlannedInterpretable[] args) { + return new EvalExhaustiveConditional(expr, args); + } + + private EvalExhaustiveConditional(CelExpr expr, PlannedInterpretable[] args) { + super(expr); + this.args = args; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveOr.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveOr.java new file mode 100644 index 000000000..07164f8c7 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalExhaustiveOr.java @@ -0,0 +1,92 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.GlobalResolver; + +/** + * Implementation of logical OR with exhaustive evaluation (non-short-circuiting). + * + *

It evaluates all arguments, but prioritizes a true result over unknowns and errors to maintain + * semantic consistency with short-circuiting evaluation. + */ +@Immutable +final class EvalExhaustiveOr extends PlannedInterpretable { + + @SuppressWarnings("Immutable") + private final PlannedInterpretable[] args; + + @Override + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { + AccumulatedUnknowns accumulatedUnknowns = null; + ErrorValue errorValue = null; + boolean hasTrue = false; + + for (PlannedInterpretable arg : args) { + Object argVal = evalNonstrictly(arg, resolver, frame); + if (argVal instanceof Boolean) { + if ((boolean) argVal) { + hasTrue = true; + } + } + + // If we already encountered a true, we do not need to accumulate unknowns or errors + // from subsequent terms because the final result will be true anyway. + if (hasTrue) { + continue; + } + + if (argVal instanceof AccumulatedUnknowns) { + accumulatedUnknowns = + accumulatedUnknowns == null + ? (AccumulatedUnknowns) argVal + : accumulatedUnknowns.merge((AccumulatedUnknowns) argVal); + } else if (argVal instanceof ErrorValue) { + if (errorValue == null) { + errorValue = (ErrorValue) argVal; + } + } + } + + if (hasTrue) { + return true; + } + + if (accumulatedUnknowns != null) { + return accumulatedUnknowns; + } + + if (errorValue != null) { + return errorValue; + } + + return false; + } + + static EvalExhaustiveOr create(CelExpr expr, PlannedInterpretable[] args) { + return new EvalExhaustiveOr(expr, args); + } + + private EvalExhaustiveOr(CelExpr expr, PlannedInterpretable[] args) { + super(expr); + this.args = args; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java b/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java index 8fa52db97..6bdeaf1df 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java @@ -19,6 +19,7 @@ import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelEvaluationListener; import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.InterpreterUtil; @Immutable abstract class PlannedInterpretable { @@ -29,7 +30,7 @@ final Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvalu Object result = evalInternal(resolver, frame); CelEvaluationListener listener = frame.getListener(); if (listener != null) { - listener.callback(expr, result); + listener.callback(expr, InterpreterUtil.maybeAdaptToCelUnknownSet(result)); } return result; } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index affe64381..e38d08f8f 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -259,11 +259,17 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { if (operator != null) { switch (operator) { case LOGICAL_OR: - return EvalOr.create(expr, evaluatedArgs); + return options.enableShortCircuiting() + ? EvalOr.create(expr, evaluatedArgs) + : EvalExhaustiveOr.create(expr, evaluatedArgs); case LOGICAL_AND: - return EvalAnd.create(expr, evaluatedArgs); + return options.enableShortCircuiting() + ? EvalAnd.create(expr, evaluatedArgs) + : EvalExhaustiveAnd.create(expr, evaluatedArgs); case CONDITIONAL: - return EvalConditional.create(expr, evaluatedArgs); + return options.enableShortCircuiting() + ? EvalConditional.create(expr, evaluatedArgs) + : EvalExhaustiveConditional.create(expr, evaluatedArgs); default: // fall-through } diff --git a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel index 9461e5e6a..3200a80e0 100644 --- a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel @@ -75,6 +75,7 @@ java_library( "//runtime:late_function_binding", "//runtime:lite_runtime", "//runtime:lite_runtime_factory", + "//runtime:partial_vars", "//runtime:proto_message_activation_factory", "//runtime:proto_message_runtime_equality", "//runtime:proto_message_runtime_helpers", diff --git a/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java b/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java index 3e29a00db..13d5dd550 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java @@ -16,8 +16,8 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; -import static org.junit.Assume.assumeTrue; +import com.google.api.expr.v1alpha1.CheckedExpr; import com.google.api.expr.v1alpha1.Constant; import com.google.api.expr.v1alpha1.Expr; import com.google.api.expr.v1alpha1.Type.PrimitiveType; @@ -36,7 +36,10 @@ import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; +import dev.cel.common.CelOverloadDecl; import dev.cel.common.CelProtoV1Alpha1AbstractSyntaxTree; import dev.cel.common.CelSource; import dev.cel.common.ast.CelConstant; @@ -104,8 +107,8 @@ public void evaluate_anyPackedEqualityUsingProtoDifferencer_success() throws Exc public void evaluate_v1alpha1CheckedExpr() throws Exception { // Note: v1alpha1 proto support exists only to help migrate existing consumers. // New users of CEL should use the canonical protos instead (I.E: dev.cel.expr) - com.google.api.expr.v1alpha1.CheckedExpr checkedExpr = - com.google.api.expr.v1alpha1.CheckedExpr.newBuilder() + CheckedExpr checkedExpr = + CheckedExpr.newBuilder() .setExpr( Expr.newBuilder() .setId(1) @@ -469,8 +472,6 @@ public void trace_withVariableResolver() throws Exception { public void trace_shortCircuitingDisabled_logicalAndAllBranchesVisited( @TestParameter boolean first, @TestParameter boolean second, @TestParameter boolean third) throws Exception { - // TODO: Implement exhaustive eval - assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); String expression = String.format("%s && %s && %s", first, second, third); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = @@ -516,8 +517,6 @@ public void trace_shortCircuitingDisabled_logicalAndAllBranchesVisited( @TestParameters("{source: 'x && false && false'}") public void trace_shortCircuitingDisabledWithUnknownsAndedToFalse_returnsFalse(String source) throws Exception { - // TODO: Implement exhaustive eval - assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -542,7 +541,8 @@ public void trace_shortCircuitingDisabledWithUnknownsAndedToFalse_returnsFalse(S .build(); CelAbstractSyntaxTree ast = cel.compile(source).getAst(); - boolean result = (boolean) cel.createProgram(ast).trace(listener); + PartialVars partialVars = PartialVars.of(CelAttributePattern.create("x")); + boolean result = (boolean) cel.createProgram(ast).trace(partialVars, listener); assertThat(result).isFalse(); assertThat(branchResults.build()).containsExactly(false, false, "x"); @@ -554,8 +554,6 @@ public void trace_shortCircuitingDisabledWithUnknownsAndedToFalse_returnsFalse(S @TestParameters("{source: 'x && true && true'}") public void trace_shortCircuitingDisabledWithUnknownAndedToTrue_returnsUnknown(String source) throws Exception { - // TODO: Implement exhaustive eval - assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -576,7 +574,8 @@ public void trace_shortCircuitingDisabledWithUnknownAndedToTrue_returnsUnknown(S .build(); CelAbstractSyntaxTree ast = cel.compile(source).getAst(); - Object unknownResult = cel.createProgram(ast).trace(listener); + PartialVars partialVars = PartialVars.of(CelAttributePattern.create("x")); + Object unknownResult = cel.createProgram(ast).trace(partialVars, listener); assertThat(InterpreterUtil.isUnknown(unknownResult)).isTrue(); assertThat(branchResults.build()).containsExactly(true, true, unknownResult); @@ -586,8 +585,6 @@ public void trace_shortCircuitingDisabledWithUnknownAndedToTrue_returnsUnknown(S public void trace_shortCircuitingDisabled_logicalOrAllBranchesVisited( @TestParameter boolean first, @TestParameter boolean second, @TestParameter boolean third) throws Exception { - // TODO: Implement exhaustive eval - assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); String expression = String.format("%s || %s || %s", first, second, third); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = @@ -633,8 +630,6 @@ public void trace_shortCircuitingDisabled_logicalOrAllBranchesVisited( @TestParameters("{source: 'x || false || false'}") public void trace_shortCircuitingDisabledWithUnknownsOredToFalse_returnsUnknown(String source) throws Exception { - // TODO: Implement exhaustive eval - assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -655,7 +650,8 @@ public void trace_shortCircuitingDisabledWithUnknownsOredToFalse_returnsUnknown( .build(); CelAbstractSyntaxTree ast = cel.compile(source).getAst(); - Object unknownResult = cel.createProgram(ast).trace(listener); + PartialVars partialVars = PartialVars.of(CelAttributePattern.create("x")); + Object unknownResult = cel.createProgram(ast).trace(partialVars, listener); assertThat(InterpreterUtil.isUnknown(unknownResult)).isTrue(); assertThat(branchResults.build()).containsExactly(false, false, unknownResult); @@ -667,8 +663,6 @@ public void trace_shortCircuitingDisabledWithUnknownsOredToFalse_returnsUnknown( @TestParameters("{source: 'x || true || true'}") public void trace_shortCircuitingDisabledWithUnknownOredToTrue_returnsTrue(String source) throws Exception { - // TODO: Implement exhaustive eval - assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -693,7 +687,8 @@ public void trace_shortCircuitingDisabledWithUnknownOredToTrue_returnsTrue(Strin .build(); CelAbstractSyntaxTree ast = cel.compile(source).getAst(); - boolean result = (boolean) cel.createProgram(ast).trace(listener); + PartialVars partialVars = PartialVars.of(CelAttributePattern.create("x")); + boolean result = (boolean) cel.createProgram(ast).trace(partialVars, listener); assertThat(result).isTrue(); assertThat(branchResults.build()).containsExactly(true, true, "x"); @@ -701,7 +696,6 @@ public void trace_shortCircuitingDisabledWithUnknownOredToTrue_returnsTrue(Strin @Test public void trace_shortCircuitingDisabled_ternaryAllBranchesVisited() throws Exception { - assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -731,8 +725,6 @@ public void trace_shortCircuitingDisabled_ternaryAllBranchesVisited() throws Exc @TestParameters("{source: 'true ? x : false'}") @TestParameters("{source: 'x ? true : false'}") public void trace_shortCircuitingDisabled_ternaryWithUnknowns(String source) throws Exception { - // TODO: Implement exhaustive eval - assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -753,7 +745,8 @@ public void trace_shortCircuitingDisabled_ternaryWithUnknowns(String source) thr .build(); CelAbstractSyntaxTree ast = cel.compile(source).getAst(); - Object unknownResult = cel.createProgram(ast).trace(listener); + PartialVars partialVars = PartialVars.of(CelAttributePattern.create("x")); + Object unknownResult = cel.createProgram(ast).trace(partialVars, listener); assertThat(InterpreterUtil.isUnknown(unknownResult)).isTrue(); assertThat(branchResults.build()).containsExactly(false, unknownResult, true); @@ -770,8 +763,6 @@ public void trace_shortCircuitingDisabled_ternaryWithUnknowns(String source) thr "{expression: 'true ? true : (1 / 0) > 2', firstVisited: true, secondVisited: true}") public void trace_shortCircuitingDisabled_ternaryWithError( String expression, boolean firstVisited, boolean secondVisited) throws Exception { - // TODO: Implement exhaustive eval - assumeTrue(runtimeFlavor != CelRuntimeFlavor.PLANNER); ImmutableList.Builder branchResults = ImmutableList.builder(); CelEvaluationListener listener = (expr, res) -> { @@ -810,6 +801,55 @@ public void trace_shortCircuitingDisabled_ternaryWithError( assertThat(branchResults.build()).containsExactly(firstVisited, secondVisited).inOrder(); } + @Test + public void trace_shortCircuitingDisabled_ternaryWithSelectedError() throws Exception { + Cel cel = + runtimeFlavor + .builder() + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile("true ? (1 / 0) : 2").getAst(); + CelRuntime.Program program = cel.createProgram(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> program.eval()); + assertThat(e).hasMessageThat().contains("evaluation error at :10: / by zero"); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.DIVIDE_BY_ZERO); + } + + @Test + public void trace_shortCircuitingDisabled_ternaryWithCustomError() throws Exception { + Cel cel = + runtimeFlavor + .builder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "error_func", + CelOverloadDecl.newGlobalOverload( + "error_func_overload", SimpleType.BOOL, ImmutableList.of()))) + .addFunctionBindings( + CelFunctionBinding.from( + "error_func_overload", + ImmutableList.of(), + args -> { + throw new IllegalArgumentException("custom error"); + })) + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile("true ? error_func() : false").getAst(); + CelRuntime.Program program = cel.createProgram(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> program.eval()); + assertThat(e).hasCauseThat().hasMessageThat().contains("custom error"); + } + @Test public void standardEnvironmentDisabledForRuntime_throws() throws Exception { CelCompiler celCompiler = @@ -817,11 +857,91 @@ public void standardEnvironmentDisabledForRuntime_throws() throws Exception { CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().setStandardEnvironmentEnabled(false).build(); CelAbstractSyntaxTree ast = celCompiler.compile("size('hello')").getAst(); + CelRuntime.Program program = celRuntime.createProgram(ast); - CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> celRuntime.createProgram(ast).eval()); + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> program.eval()); assertThat(e) .hasMessageThat() .contains("No matching overload for function 'size'. Overload candidates: size_string"); } + + @Test + public void trace_shortCircuitingDisabled_logicalAndPrefersFirstError() throws Exception { + Cel cel = + runtimeFlavor + .builder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "error_1", + CelOverloadDecl.newGlobalOverload( + "error_1_overload", SimpleType.BOOL, ImmutableList.of())), + CelFunctionDecl.newFunctionDeclaration( + "error_2", + CelOverloadDecl.newGlobalOverload( + "error_2_overload", SimpleType.BOOL, ImmutableList.of()))) + .addFunctionBindings( + CelFunctionBinding.from( + "error_1_overload", + ImmutableList.of(), + args -> { + throw new IllegalArgumentException("error 1"); + }), + CelFunctionBinding.from( + "error_2_overload", + ImmutableList.of(), + args -> { + throw new IllegalArgumentException("error 2"); + })) + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile("error_1() && error_2()").getAst(); + CelRuntime.Program program = cel.createProgram(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> program.eval()); + assertThat(e).hasCauseThat().hasMessageThat().contains("error 1"); + } + + @Test + public void trace_shortCircuitingDisabled_logicalOrPrefersFirstError() throws Exception { + Cel cel = + runtimeFlavor + .builder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "error_1", + CelOverloadDecl.newGlobalOverload( + "error_1_overload", SimpleType.BOOL, ImmutableList.of())), + CelFunctionDecl.newFunctionDeclaration( + "error_2", + CelOverloadDecl.newGlobalOverload( + "error_2_overload", SimpleType.BOOL, ImmutableList.of()))) + .addFunctionBindings( + CelFunctionBinding.from( + "error_1_overload", + ImmutableList.of(), + args -> { + throw new IllegalArgumentException("error 1"); + }), + CelFunctionBinding.from( + "error_2_overload", + ImmutableList.of(), + args -> { + throw new IllegalArgumentException("error 2"); + })) + .setOptions( + CelOptions.current() + .enableShortCircuiting(false) + .enableHeterogeneousNumericComparisons(true) + .build()) + .build(); + CelAbstractSyntaxTree ast = cel.compile("error_1() || error_2()").getAst(); + CelRuntime.Program program = cel.createProgram(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> program.eval()); + assertThat(e).hasCauseThat().hasMessageThat().contains("error 1"); + } } From cd71e408cea826ff4bfd08e9d677f9ba4f100061 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 23 Apr 2026 18:48:18 -0700 Subject: [PATCH 061/204] Promote planner runtime builders to the main factories PiperOrigin-RevId: 904740634 --- bundle/BUILD.bazel | 8 --- .../src/main/java/dev/cel/bundle/BUILD.bazel | 19 ------- .../cel/bundle/CelExperimentalFactory.java | 57 ------------------- .../main/java/dev/cel/bundle/CelFactory.java | 25 ++++++++ .../test/java/dev/cel/extensions/BUILD.bazel | 1 - .../extensions/CelOptionalLibraryTest.java | 3 +- publish/BUILD.bazel | 2 - runtime/BUILD.bazel | 8 --- .../src/main/java/dev/cel/runtime/BUILD.bazel | 12 ---- .../CelRuntimeExperimentalFactory.java | 48 ---------------- .../dev/cel/runtime/CelRuntimeFactory.java | 19 +++++++ .../java/dev/cel/runtime/CelRuntimeImpl.java | 3 +- .../src/test/java/dev/cel/runtime/BUILD.bazel | 1 - .../cel/runtime/PlannerInterpreterTest.java | 2 +- .../src/main/java/dev/cel/testing/BUILD.bazel | 2 - .../dev/cel/testing/CelRuntimeFlavor.java | 3 +- 16 files changed, 49 insertions(+), 164 deletions(-) delete mode 100644 bundle/src/main/java/dev/cel/bundle/CelExperimentalFactory.java delete mode 100644 runtime/src/main/java/dev/cel/runtime/CelRuntimeExperimentalFactory.java diff --git a/bundle/BUILD.bazel b/bundle/BUILD.bazel index 11e0b8a6d..1eaf0bec8 100644 --- a/bundle/BUILD.bazel +++ b/bundle/BUILD.bazel @@ -13,14 +13,6 @@ java_library( ], ) -java_library( - name = "cel_experimental_factory", - visibility = ["//:internal"], - exports = [ - "//bundle/src/main/java/dev/cel/bundle:cel_experimental_factory", - ], -) - java_library( name = "environment", exports = ["//bundle/src/main/java/dev/cel/bundle:environment"], diff --git a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel index 822511e4c..742f718f1 100644 --- a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel +++ b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel @@ -35,7 +35,6 @@ java_library( "@cel_spec//proto/cel/expr:checked_java_proto", "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", ], ) @@ -54,22 +53,6 @@ java_library( "//compiler:compiler_builder", "//parser", "//runtime", - ], -) - -java_library( - name = "cel_experimental_factory", - srcs = ["CelExperimentalFactory.java"], - tags = [ - ], - deps = [ - ":cel", - ":cel_impl", - "//checker", - "//common:options", - "//common/annotations", - "//compiler", - "//parser", "//runtime:runtime_planner_impl", ], ) @@ -117,7 +100,6 @@ java_library( tags = [ ], deps = [ - ":cel_factory", ":environment_exception", ":required_fields_checker", "//:auto_value", @@ -190,7 +172,6 @@ java_library( "//common:options", "//common/internal:env_visitor", "//common/types:cel_proto_types", - "//common/types:cel_types", "//common/types:type_providers", "//compiler:compiler_builder", "//extensions", diff --git a/bundle/src/main/java/dev/cel/bundle/CelExperimentalFactory.java b/bundle/src/main/java/dev/cel/bundle/CelExperimentalFactory.java deleted file mode 100644 index 9a3e95dd8..000000000 --- a/bundle/src/main/java/dev/cel/bundle/CelExperimentalFactory.java +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dev.cel.bundle; - -import dev.cel.checker.CelCheckerLegacyImpl; -import dev.cel.common.CelOptions; -import dev.cel.common.annotations.Beta; -import dev.cel.compiler.CelCompilerImpl; -import dev.cel.parser.CelParserImpl; -import dev.cel.runtime.CelRuntimeImpl; - -/** - * Experimental helper class to configure the entire CEL stack in a common interface, backed by the - * new {@code ProgramPlanner} architecture. - * - *

All APIs and behaviors surfaced here are subject to change. - */ -@Beta -public final class CelExperimentalFactory { - - /** - * Creates a builder for configuring CEL for the parsing, optional type-checking, and evaluation - * of expressions using the Program Planner. - * - *

The {@code ProgramPlanner} architecture provides key benefits over the legacy runtime: - * - *

    - *
  • Performance: Programs can be cached for improving evaluation speed. - *
  • Parsed-only expression evaluation: Unlike the traditional stack which required - * supplying type-checked expressions, this architecture handles both parsed-only and - * type-checked expressions. - *
- */ - public static CelBuilder plannerCelBuilder() { - return CelImpl.newBuilder( - CelCompilerImpl.newBuilder( - CelParserImpl.newBuilder(), - CelCheckerLegacyImpl.newBuilder().setStandardEnvironmentEnabled(true)), - CelRuntimeImpl.newBuilder()) - // CEL-Internal-2 - .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()); - } - - private CelExperimentalFactory() {} -} diff --git a/bundle/src/main/java/dev/cel/bundle/CelFactory.java b/bundle/src/main/java/dev/cel/bundle/CelFactory.java index 6cc6d8192..ac589cfe6 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelFactory.java +++ b/bundle/src/main/java/dev/cel/bundle/CelFactory.java @@ -20,6 +20,7 @@ import dev.cel.compiler.CelCompilerImpl; import dev.cel.parser.CelParserImpl; import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeImpl; import dev.cel.runtime.CelRuntimeLegacyImpl; /** Helper class to configure the entire CEL stack in a common interface. */ @@ -44,6 +45,30 @@ public static CelBuilder standardCelBuilder() { .setStandardEnvironmentEnabled(true); } + /** + * Creates a builder for configuring CEL for the parsing, optional type-checking, and evaluation + * of expressions using the Program Planner. + * + *

The {@code ProgramPlanner} architecture provides key benefits over the {@link + * #standardCelBuilder()}: + * + *

    + *
  • Performance: Programs can be cached for improving evaluation speed. + *
  • Parsed-only expression evaluation: Unlike the traditional stack which required + * supplying type-checked expressions, this architecture handles both parsed-only and + * type-checked expressions. + *
+ */ + public static CelBuilder plannerCelBuilder() { + return CelImpl.newBuilder( + CelCompilerImpl.newBuilder( + CelParserImpl.newBuilder(), + CelCheckerLegacyImpl.newBuilder().setStandardEnvironmentEnabled(true)), + CelRuntimeImpl.newBuilder()) + // CEL-Internal-2 + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()); + } + /** Combines a prebuilt {@link CelCompiler} and {@link CelRuntime} into {@link Cel}. */ public static Cel combine(CelCompiler celCompiler, CelRuntime celRuntime) { return CelImpl.combine(celCompiler, celRuntime); diff --git a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel index eed240317..f926e25b6 100644 --- a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel @@ -10,7 +10,6 @@ java_library( deps = [ "//:java_truth", "//bundle:cel", - "//bundle:cel_experimental_factory", "//common:cel_ast", "//common:cel_exception", "//common:compiler_common", diff --git a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java index 4f348c12a..34c7f89f9 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java @@ -26,7 +26,6 @@ import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.bundle.Cel; import dev.cel.bundle.CelBuilder; -import dev.cel.bundle.CelExperimentalFactory; import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; @@ -113,7 +112,7 @@ private CelBuilder newCelBuilder(int version) { switch (testMode) { case PLANNER_PARSE_ONLY: case PLANNER_CHECKED: - celBuilder = CelExperimentalFactory.plannerCelBuilder(); + celBuilder = CelFactory.plannerCelBuilder(); break; case LEGACY_CHECKED: celBuilder = CelFactory.standardCelBuilder(); diff --git a/publish/BUILD.bazel b/publish/BUILD.bazel index d905edc4b..69622aada 100644 --- a/publish/BUILD.bazel +++ b/publish/BUILD.bazel @@ -32,7 +32,6 @@ RUNTIME_TARGETS = [ "//runtime/src/main/java/dev/cel/runtime:base", "//runtime/src/main/java/dev/cel/runtime:interpreter", "//runtime/src/main/java/dev/cel/runtime:late_function_binding", - "//runtime/src/main/java/dev/cel/runtime:runtime_experimental_factory", "//runtime/src/main/java/dev/cel/runtime:runtime_factory", "//runtime/src/main/java/dev/cel/runtime:runtime_helpers", "//runtime/src/main/java/dev/cel/runtime:runtime_legacy_impl", @@ -125,7 +124,6 @@ EXTENSION_TARGETS = [ # keep sorted BUNDLE_TARGETS = [ "//bundle/src/main/java/dev/cel/bundle:cel", - "//bundle/src/main/java/dev/cel/bundle:cel_experimental_factory", "//bundle/src/main/java/dev/cel/bundle:environment", "//bundle/src/main/java/dev/cel/bundle:environment_yaml_parser", ] diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index 3e183d236..f4a150ff3 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -29,14 +29,6 @@ java_library( ], ) -java_library( - name = "runtime_experimental_factory", - visibility = ["//:internal"], - exports = [ - "//runtime/src/main/java/dev/cel/runtime:runtime_experimental_factory", - ], -) - java_library( name = "runtime_legacy_impl", visibility = ["//:internal"], diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 0da68d548..5178ae27c 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -916,20 +916,8 @@ java_library( deps = [ ":runtime", ":runtime_legacy_impl", - "//common:options", - ], -) - -java_library( - name = "runtime_experimental_factory", - srcs = ["CelRuntimeExperimentalFactory.java"], - tags = [ - ], - deps = [ - ":runtime", ":runtime_planner_impl", "//common:options", - "//common/annotations", ], ) diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeExperimentalFactory.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeExperimentalFactory.java deleted file mode 100644 index 743f90669..000000000 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeExperimentalFactory.java +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dev.cel.runtime; - -import dev.cel.common.CelOptions; -import dev.cel.common.annotations.Beta; - -/** - * Experimental helper class to construct new {@code CelRuntime} instances backed by the new {@code - * ProgramPlanner} architecture. - * - *

All APIs and behaviors surfaced here are subject to change. - */ -@Beta -public final class CelRuntimeExperimentalFactory { - - /** - * Create a new builder for constructing a {@code CelRuntime} instance. - * - *

The {@code ProgramPlanner} architecture provides key benefits over the legacy runtime: - * - *

    - *
  • Performance: Programs can be cached for improving evaluation speed. - *
  • Parsed-only expression evaluation: Unlike the traditional legacy runtime, which - * only supported evaluating type-checked expressions, this architecture handles both - * parsed-only and type-checked expressions. - *
- */ - public static CelRuntimeBuilder plannerRuntimeBuilder() { - return CelRuntimeImpl.newBuilder() - // CEL-Internal-2 - .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()); - } - - private CelRuntimeExperimentalFactory() {} -} diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeFactory.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeFactory.java index 322985b22..6615b59e0 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeFactory.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeFactory.java @@ -32,5 +32,24 @@ public static CelRuntimeBuilder standardCelRuntimeBuilder() { .setStandardEnvironmentEnabled(true); } + /** + * Create a new builder for constructing a {@code CelRuntime} instance. + * + *

The {@code ProgramPlanner} architecture provides key benefits over the {@link + * #standardCelRuntimeBuilder()}: + * + *

    + *
  • Performance: Programs can be cached for improving evaluation speed. + *
  • Parsed-only expression evaluation: Unlike the runtime returned by {@link + * #standardCelRuntimeBuilder()}, which only supported evaluating type-checked expressions, + * this architecture handles both parsed-only and type-checked expressions. + *
+ */ + public static CelRuntimeBuilder plannerRuntimeBuilder() { + return CelRuntimeImpl.newBuilder() + // CEL-Internal-2 + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()); + } + private CelRuntimeFactory() {} } diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index ed203d612..dcdf3be52 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -474,7 +474,8 @@ private static CelDescriptorPool newDescriptorPool( @Override public CelRuntime build() { - assertAllowedCelOptions(options()); + CelOptions options = options(); + assertAllowedCelOptions(options); CelDescriptors celDescriptors = CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(fileDescriptorsBuilder().build()); diff --git a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel index 3200a80e0..e886c3d8a 100644 --- a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel @@ -146,7 +146,6 @@ java_library( "//extensions", "//runtime", "//runtime:function_binding", - "//runtime:runtime_experimental_factory", "//runtime:unknown_attributes", "//testing:base_interpreter_test", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", diff --git a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java index c0b0f76c4..9ae8590d5 100644 --- a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java +++ b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java @@ -42,7 +42,7 @@ public class PlannerInterpreterTest extends BaseInterpreterTest { @Override protected CelRuntimeBuilder newBaseRuntimeBuilder(CelOptions celOptions) { - return CelRuntimeExperimentalFactory.plannerRuntimeBuilder() + return CelRuntimeFactory.plannerRuntimeBuilder() .addLateBoundFunctions("record") .setOptions(celOptions) .addLibraries(CelExtensions.optional()) diff --git a/testing/src/main/java/dev/cel/testing/BUILD.bazel b/testing/src/main/java/dev/cel/testing/BUILD.bazel index b52026ec4..69765b549 100644 --- a/testing/src/main/java/dev/cel/testing/BUILD.bazel +++ b/testing/src/main/java/dev/cel/testing/BUILD.bazel @@ -102,7 +102,6 @@ java_library( "@maven//:com_google_protobuf_protobuf_java", "@maven//:com_google_protobuf_protobuf_java_util", "@maven//:junit_junit", - "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) @@ -113,6 +112,5 @@ java_library( ], deps = [ "//bundle:cel", - "//bundle:cel_experimental_factory", ], ) diff --git a/testing/src/main/java/dev/cel/testing/CelRuntimeFlavor.java b/testing/src/main/java/dev/cel/testing/CelRuntimeFlavor.java index 576e0c1d3..66ce8d802 100644 --- a/testing/src/main/java/dev/cel/testing/CelRuntimeFlavor.java +++ b/testing/src/main/java/dev/cel/testing/CelRuntimeFlavor.java @@ -15,7 +15,6 @@ package dev.cel.testing; import dev.cel.bundle.CelBuilder; -import dev.cel.bundle.CelExperimentalFactory; import dev.cel.bundle.CelFactory; /** Enumeration of supported CEL runtime environments for testing. */ @@ -29,7 +28,7 @@ public CelBuilder builder() { PLANNER { @Override public CelBuilder builder() { - return CelExperimentalFactory.plannerCelBuilder(); + return CelFactory.plannerCelBuilder(); } }; From d699e28a52a6239deb887c3fa28897e56d3feb0d Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 23 Apr 2026 20:31:22 -0700 Subject: [PATCH 062/204] Add CombinedCelValueConverter PiperOrigin-RevId: 904770100 --- .../java/dev/cel/common/values/BUILD.bazel | 44 ++++++- .../cel/common/values/CelValueConverter.java | 73 +++++------- .../values/CombinedCelValueConverter.java | 84 +++++++++++++ .../values/CombinedCelValueProvider.java | 9 ++ .../java/dev/cel/common/values/BUILD.bazel | 1 + .../values/CombinedCelValueConverterTest.java | 112 ++++++++++++++++++ common/values/BUILD.bazel | 12 ++ .../java/dev/cel/runtime/CelRuntimeImpl.java | 12 +- 8 files changed, 298 insertions(+), 49 deletions(-) create mode 100644 common/src/main/java/dev/cel/common/values/CombinedCelValueConverter.java create mode 100644 common/src/test/java/dev/cel/common/values/CombinedCelValueConverterTest.java diff --git a/common/src/main/java/dev/cel/common/values/BUILD.bazel b/common/src/main/java/dev/cel/common/values/BUILD.bazel index 0d1d5431f..d572bb2bc 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -78,10 +78,14 @@ cel_android_library( java_library( name = "combined_cel_value_provider", - srcs = ["CombinedCelValueProvider.java"], + srcs = [ + "CombinedCelValueProvider.java", + ], tags = [ ], deps = [ + ":combined_cel_value_converter", + ":values", "//common/values:cel_value_provider", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -90,16 +94,52 @@ java_library( cel_android_library( name = "combined_cel_value_provider_android", - srcs = ["CombinedCelValueProvider.java"], + srcs = [ + "CombinedCelValueProvider.java", + ], tags = [ ], deps = [ + ":combined_cel_value_converter_android", + ":values_android", "//common/values:cel_value_provider_android", "@maven//:com_google_errorprone_error_prone_annotations", "@maven_android//:com_google_guava_guava", ], ) +java_library( + name = "combined_cel_value_converter", + srcs = [ + "CombinedCelValueConverter.java", + ], + tags = [ + ], + deps = [ + ":values", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + +cel_android_library( + name = "combined_cel_value_converter_android", + srcs = [ + "CombinedCelValueConverter.java", + ], + tags = [ + ], + deps = [ + ":values_android", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "values", srcs = CEL_VALUES_SOURCES, diff --git a/common/src/main/java/dev/cel/common/values/CelValueConverter.java b/common/src/main/java/dev/cel/common/values/CelValueConverter.java index 70d04acc8..89f5ab100 100644 --- a/common/src/main/java/dev/cel/common/values/CelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/CelValueConverter.java @@ -21,8 +21,8 @@ import dev.cel.common.annotations.Internal; import java.util.Collection; import java.util.Map; -import java.util.Map.Entry; import java.util.Optional; +import java.util.function.Function; /** * {@code CelValueConverter} handles bidirectional conversion between native Java objects to {@link @@ -37,6 +37,12 @@ public class CelValueConverter { private static final CelValueConverter DEFAULT_INSTANCE = new CelValueConverter(); + @SuppressWarnings("Immutable") // Method reference is immutable + private final Function maybeUnwrapFunction; + + @SuppressWarnings("Immutable") // Method reference is immutable + private final Function toRuntimeValueFunction; + public static CelValueConverter getDefaultInstance() { return DEFAULT_INSTANCE; } @@ -51,14 +57,26 @@ public Object maybeUnwrap(Object value) { return unwrap((CelValue) value); } + Object mapped = mapContainer(value, maybeUnwrapFunction); + if (mapped != value) { + return mapped; + } + + return value; + } + + /** + * Maps a container (Collection or Map) by applying the provided mapper function to its elements. + * Returns the original value if it's not a supported container. + */ + protected Object mapContainer(Object value, Function mapper) { if (value instanceof Collection) { Collection collection = (Collection) value; ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(collection.size()); for (Object element : collection) { - builder.add(maybeUnwrap(element)); + builder.add(mapper.apply(element)); } - return builder.build(); } @@ -67,19 +85,14 @@ public Object maybeUnwrap(Object value) { ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(map.size()); for (Map.Entry entry : map.entrySet()) { - builder.put(maybeUnwrap(entry.getKey()), maybeUnwrap(entry.getValue())); + builder.put(mapper.apply(entry.getKey()), mapper.apply(entry.getValue())); } - return builder.buildOrThrow(); } return value; } - /** - * Canonicalizes an inbound {@code value} into a suitable Java object representation for - * evaluation. - */ public Object toRuntimeValue(Object value) { Preconditions.checkNotNull(value); @@ -87,14 +100,15 @@ public Object toRuntimeValue(Object value) { return value; } - if (value instanceof Collection) { - return toListValue((Collection) value); - } else if (value instanceof Map) { - return toMapValue((Map) value); - } else if (value instanceof Optional) { + Object mapped = mapContainer(value, toRuntimeValueFunction); + if (mapped != value) { + return mapped; + } + + if (value instanceof Optional) { Optional optionalValue = (Optional) value; return optionalValue - .map(this::toRuntimeValue) + .map(toRuntimeValueFunction) .map(OptionalValue::create) .orElse(OptionalValue.EMPTY); } @@ -136,31 +150,8 @@ private Object unwrap(CelValue celValue) { return celValue.value(); } - private ImmutableList toListValue(Collection iterable) { - Preconditions.checkNotNull(iterable); - - ImmutableList.Builder listBuilder = - ImmutableList.builderWithExpectedSize(iterable.size()); - for (Object entry : iterable) { - listBuilder.add(toRuntimeValue(entry)); - } - - return listBuilder.build(); - } - - private ImmutableMap toMapValue(Map map) { - Preconditions.checkNotNull(map); - - ImmutableMap.Builder mapBuilder = - ImmutableMap.builderWithExpectedSize(map.size()); - for (Entry entry : map.entrySet()) { - Object mapKey = toRuntimeValue(entry.getKey()); - Object mapValue = toRuntimeValue(entry.getValue()); - mapBuilder.put(mapKey, mapValue); - } - - return mapBuilder.buildOrThrow(); + protected CelValueConverter() { + this.maybeUnwrapFunction = this::maybeUnwrap; + this.toRuntimeValueFunction = this::toRuntimeValue; } - - protected CelValueConverter() {} } diff --git a/common/src/main/java/dev/cel/common/values/CombinedCelValueConverter.java b/common/src/main/java/dev/cel/common/values/CombinedCelValueConverter.java new file mode 100644 index 000000000..46e5fc3f1 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/CombinedCelValueConverter.java @@ -0,0 +1,84 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.values; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import dev.cel.common.annotations.Internal; +import org.jspecify.annotations.Nullable; + +/** + * {@code CombinedCelValueConverter} delegates value conversion to a list of underlying {@link + * CelValueConverter}s. + */ +@Internal +public final class CombinedCelValueConverter extends CelValueConverter { + private final ImmutableList converters; + + public static CombinedCelValueConverter combine(ImmutableList converters) { + return new CombinedCelValueConverter(converters); + } + + private CombinedCelValueConverter(ImmutableList converters) { + this.converters = checkNotNull(converters); + } + + @Override + public @Nullable Object toRuntimeValue(Object value) { + if (value == null) { + return null; + } + + // Let the base class handle CelValues, Optionals, Collections, Maps, and primitives. + Object baseResult = super.toRuntimeValue(value); + if (baseResult != value) { + return baseResult; + } + + // If the base class left the object unchanged (e.g. a raw POJO), try the delegates. + for (CelValueConverter converter : converters) { + Object result = converter.toRuntimeValue(value); + if (result != value) { + return result; + } + } + + return value; + } + + @Override + public @Nullable Object maybeUnwrap(Object value) { + if (value == null) { + return null; + } + + // Let the base class handle standard unwrapping and container unrolling. + Object baseResult = super.maybeUnwrap(value); + if (baseResult != value) { + return baseResult; + } + + // Try delegates for specialized unwrapping. + for (CelValueConverter converter : converters) { + Object result = converter.maybeUnwrap(value); + if (result != value) { + return result; + } + } + + return value; + } +} diff --git a/common/src/main/java/dev/cel/common/values/CombinedCelValueProvider.java b/common/src/main/java/dev/cel/common/values/CombinedCelValueProvider.java index 8fe62cb7b..d51c3afce 100644 --- a/common/src/main/java/dev/cel/common/values/CombinedCelValueProvider.java +++ b/common/src/main/java/dev/cel/common/values/CombinedCelValueProvider.java @@ -16,6 +16,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableList.toImmutableList; import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; @@ -49,6 +50,14 @@ public Optional newValue(String structType, Map fields) return Optional.empty(); } + @Override + public CelValueConverter celValueConverter() { + return CombinedCelValueConverter.combine( + celValueProviders.stream() + .map(CelValueProvider::celValueConverter) + .collect(toImmutableList())); + } + /** Returns the underlying {@link CelValueProvider}s in order. */ public ImmutableList valueProviders() { return celValueProviders; diff --git a/common/src/test/java/dev/cel/common/values/BUILD.bazel b/common/src/test/java/dev/cel/common/values/BUILD.bazel index ab7eae8dd..bf151fcb7 100644 --- a/common/src/test/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/values/BUILD.bazel @@ -24,6 +24,7 @@ java_library( "//common/values", "//common/values:cel_byte_string", "//common/values:cel_value_provider", + "//common/values:combined_cel_value_converter", "//common/values:combined_cel_value_provider", "//common/values:proto_message_lite_value", "//common/values:proto_message_lite_value_provider", diff --git a/common/src/test/java/dev/cel/common/values/CombinedCelValueConverterTest.java b/common/src/test/java/dev/cel/common/values/CombinedCelValueConverterTest.java new file mode 100644 index 000000000..8574587bc --- /dev/null +++ b/common/src/test/java/dev/cel/common/values/CombinedCelValueConverterTest.java @@ -0,0 +1,112 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.values; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import java.util.Map; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class CombinedCelValueConverterTest { + + @Test + public void toRuntimeValue_delegatesToUnderlyingConverters() { + CustomConverter converter1 = new CustomConverter("target1", "replacement1"); + CustomConverter converter2 = new CustomConverter("target2", "replacement2"); + CelValueConverter combined = + CombinedCelValueConverter.combine(ImmutableList.of(converter1, converter2)); + + assertThat(combined.toRuntimeValue("target1")).isEqualTo("replacement1"); + assertThat(combined.toRuntimeValue("target2")).isEqualTo("replacement2"); + assertThat(combined.toRuntimeValue("unhandled")).isEqualTo("unhandled"); + } + + @Test + public void maybeUnwrap_delegatesToUnderlyingConverters() { + CustomConverter converter1 = new CustomConverter("target1", "replacement1"); + CustomConverter converter2 = new CustomConverter("target2", "replacement2"); + CelValueConverter combined = + CombinedCelValueConverter.combine(ImmutableList.of(converter1, converter2)); + + assertThat(combined.maybeUnwrap("replacement1")).isEqualTo("target1"); + assertThat(combined.maybeUnwrap("replacement2")).isEqualTo("target2"); + assertThat(combined.maybeUnwrap("unhandled")).isEqualTo("unhandled"); + } + + @Test + public void combinedCelValueProvider_returnsCombinedConverter() { + CustomConverter converter1 = new CustomConverter("target1", "replacement1"); + CustomConverter converter2 = new CustomConverter("target2", "replacement2"); + CustomProvider provider1 = new CustomProvider(converter1); + CustomProvider provider2 = new CustomProvider(converter2); + + CombinedCelValueProvider combinedProvider = + CombinedCelValueProvider.combine(provider1, provider2); + CelValueConverter combinedConverter = combinedProvider.celValueConverter(); + + assertThat(combinedConverter).isInstanceOf(CombinedCelValueConverter.class); + assertThat(combinedConverter.toRuntimeValue("target1")).isEqualTo("replacement1"); + assertThat(combinedConverter.toRuntimeValue("target2")).isEqualTo("replacement2"); + } + + private static class CustomConverter extends CelValueConverter { + private final String target; + private final String replacement; + + private CustomConverter(String target, String replacement) { + this.target = target; + this.replacement = replacement; + } + + @Override + public Object toRuntimeValue(Object value) { + if (value.equals(target)) { + return replacement; + } + return value; + } + + @Override + public Object maybeUnwrap(Object value) { + if (value.equals(replacement)) { + return target; + } + return value; + } + } + + private static class CustomProvider implements CelValueProvider { + private final CelValueConverter converter; + + private CustomProvider(CelValueConverter converter) { + this.converter = converter; + } + + @Override + public Optional newValue(String structType, Map fields) { + return Optional.empty(); + } + + @Override + public CelValueConverter celValueConverter() { + return converter; + } + } +} diff --git a/common/values/BUILD.bazel b/common/values/BUILD.bazel index 74bfa9e0f..9853289a9 100644 --- a/common/values/BUILD.bazel +++ b/common/values/BUILD.bazel @@ -37,6 +37,18 @@ cel_android_library( exports = ["//common/src/main/java/dev/cel/common/values:combined_cel_value_provider_android"], ) +java_library( + name = "combined_cel_value_converter", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/values:combined_cel_value_converter"], +) + +cel_android_library( + name = "combined_cel_value_converter_android", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/values:combined_cel_value_converter_android"], +) + java_library( name = "values", exports = ["//common/src/main/java/dev/cel/common/values"], diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index dcdf3be52..adfba967b 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -487,12 +487,6 @@ public CelRuntime build() { DynamicProto dynamicProto = DynamicProto.create(defaultMessageFactory); CelValueProvider protoMessageValueProvider = ProtoMessageValueProvider.newInstance(options(), dynamicProto); - CelValueConverter celValueConverter = protoMessageValueProvider.celValueConverter(); - if (valueProvider() != null) { - protoMessageValueProvider = - CombinedCelValueProvider.combine(protoMessageValueProvider, valueProvider()); - } - RuntimeEquality runtimeEquality = ProtoMessageRuntimeEquality.create(dynamicProto, options()); ImmutableSet runtimeLibraries = runtimeLibrariesBuilder().build(); // Add libraries, such as extensions @@ -505,6 +499,12 @@ public CelRuntime build() { } } + if (valueProvider() != null) { + protoMessageValueProvider = + CombinedCelValueProvider.combine(protoMessageValueProvider, valueProvider()); + } + CelValueConverter celValueConverter = protoMessageValueProvider.celValueConverter(); + CelTypeProvider messageTypeProvider = ProtoMessageTypeProvider.newBuilder() .setCelDescriptors(celDescriptors) From f5664501d9401930481276523fcb9681cf6bda1f Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 23 Apr 2026 22:53:50 -0700 Subject: [PATCH 063/204] Add value type parameter in StructValue PiperOrigin-RevId: 904815361 --- .../common/values/ProtoMessageLiteValue.java | 2 +- .../cel/common/values/ProtoMessageValue.java | 2 +- .../dev/cel/common/values/StructValue.java | 16 +++- .../cel/common/values/OptionalValueTest.java | 2 +- .../cel/common/values/StructValueTest.java | 88 +++++++++++++------ .../cel/runtime/planner/EvalCreateStruct.java | 3 +- 6 files changed, 76 insertions(+), 37 deletions(-) diff --git a/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java index 52f0f1594..2e4d980c7 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java +++ b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java @@ -35,7 +35,7 @@ */ @AutoValue @Immutable -public abstract class ProtoMessageLiteValue extends StructValue { +public abstract class ProtoMessageLiteValue extends StructValue { @Override public abstract MessageLite value(); diff --git a/common/src/main/java/dev/cel/common/values/ProtoMessageValue.java b/common/src/main/java/dev/cel/common/values/ProtoMessageValue.java index 12d47c253..627bd2c1d 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoMessageValue.java +++ b/common/src/main/java/dev/cel/common/values/ProtoMessageValue.java @@ -28,7 +28,7 @@ /** ProtoMessageValue is a struct value with protobuf support. */ @AutoValue @Immutable -public abstract class ProtoMessageValue extends StructValue { +public abstract class ProtoMessageValue extends StructValue { @Override public abstract Message value(); diff --git a/common/src/main/java/dev/cel/common/values/StructValue.java b/common/src/main/java/dev/cel/common/values/StructValue.java index 8775ef5c4..aa44ec420 100644 --- a/common/src/main/java/dev/cel/common/values/StructValue.java +++ b/common/src/main/java/dev/cel/common/values/StructValue.java @@ -19,13 +19,21 @@ /** * StructValue is a representation of a structured object with typed properties. * - *

Users may extend from this class to provide a custom struct that CEL can understand (ex: - * POJOs). Custom struct implementations must provide all functionalities denoted in the CEL - * specification, such as field selection, presence testing and new object creation. + *

Users may extend from this class to provide a custom struct that CEL can understand by + * wrapping a native Java object (e.g., a POJO or a Map). Custom struct implementations must provide + * all functionalities denoted in the CEL specification, such as field selection, presence testing + * and new object creation. * *

For an expression `e` selecting a field `f`, `e.f` must throw an exception if `f` does not * exist in the struct (i.e: hasField returns false). If the field exists but is not set, the * implementation should return an appropriate default value based on the struct's semantics. + * + * @param The type of the field identifier. Only {@code String} is supported for now, but we may + * extend support to other types in the future. + * @param The type of the wrapped native object. */ @Immutable -public abstract class StructValue extends CelValue implements SelectableValue {} +public abstract class StructValue extends CelValue implements SelectableValue { + @Override + public abstract V value(); +} diff --git a/common/src/test/java/dev/cel/common/values/OptionalValueTest.java b/common/src/test/java/dev/cel/common/values/OptionalValueTest.java index 24b3ea30b..f00954e3d 100644 --- a/common/src/test/java/dev/cel/common/values/OptionalValueTest.java +++ b/common/src/test/java/dev/cel/common/values/OptionalValueTest.java @@ -141,7 +141,7 @@ public void celTypeTest() { } @SuppressWarnings("Immutable") // Test only - private static class CelCustomStruct extends StructValue { + private static class CelCustomStruct extends StructValue { private final long data; @Override diff --git a/common/src/test/java/dev/cel/common/values/StructValueTest.java b/common/src/test/java/dev/cel/common/values/StructValueTest.java index b8d6371a8..f25db8e87 100644 --- a/common/src/test/java/dev/cel/common/values/StructValueTest.java +++ b/common/src/test/java/dev/cel/common/values/StructValueTest.java @@ -59,18 +59,34 @@ public Optional findType(String typeName) { }; private static final CelValueProvider CUSTOM_STRUCT_VALUE_PROVIDER = - (structType, fields) -> { - if (structType.equals(CUSTOM_STRUCT_TYPE.name())) { - return Optional.of(new CelCustomStructValue(fields)); + new CelValueProvider() { + @Override + public Optional newValue(String structType, Map fields) { + if (structType.equals(CUSTOM_STRUCT_TYPE.name())) { + return Optional.of(new CelCustomStructValue(fields)); + } + return Optional.empty(); + } + + @Override + public CelValueConverter celValueConverter() { + return new CelValueConverter() { + @Override + public Object toRuntimeValue(Object value) { + if (value instanceof CustomPojo) { + return new CelCustomStructValue((CustomPojo) value); + } + return super.toRuntimeValue(value); + } + }; } - return Optional.empty(); }; @Test public void emptyStruct() { CelCustomStructValue celCustomStruct = new CelCustomStructValue(0); - assertThat(celCustomStruct.value()).isEqualTo(celCustomStruct); + assertThat(celCustomStruct.value().getData()).isEqualTo(0L); assertThat(celCustomStruct.isZeroValue()).isTrue(); } @@ -78,7 +94,7 @@ public void emptyStruct() { public void constructStruct() { CelCustomStructValue celCustomStruct = new CelCustomStructValue(5); - assertThat(celCustomStruct.value()).isEqualTo(celCustomStruct); + assertThat(celCustomStruct.value().getData()).isEqualTo(5L); assertThat(celCustomStruct.isZeroValue()).isFalse(); } @@ -115,41 +131,41 @@ public void celTypeTest() { @Test public void evaluate_usingCustomClass_createNewStruct() throws Exception { Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableCelValue(true).build()) + CelFactory.plannerCelBuilder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) .setTypeProvider(CUSTOM_STRUCT_TYPE_PROVIDER) .setValueProvider(CUSTOM_STRUCT_VALUE_PROVIDER) .build(); CelAbstractSyntaxTree ast = cel.compile("custom_struct{data: 50}").getAst(); - CelCustomStructValue result = (CelCustomStructValue) cel.createProgram(ast).eval(); + CustomPojo result = (CustomPojo) cel.createProgram(ast).eval(); - assertThat(result.data).isEqualTo(50); + assertThat(result.getData()).isEqualTo(50); } @Test public void evaluate_usingCustomClass_asVariable() throws Exception { Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableCelValue(true).build()) + CelFactory.plannerCelBuilder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) .addVar("a", CUSTOM_STRUCT_TYPE) .setTypeProvider(CUSTOM_STRUCT_TYPE_PROVIDER) .setValueProvider(CUSTOM_STRUCT_VALUE_PROVIDER) .build(); CelAbstractSyntaxTree ast = cel.compile("a").getAst(); - CelCustomStructValue result = - (CelCustomStructValue) + CustomPojo result = + (CustomPojo) cel.createProgram(ast).eval(ImmutableMap.of("a", new CelCustomStructValue(10))); - assertThat(result.data).isEqualTo(10); + assertThat(result.getData()).isEqualTo(10); } @Test public void evaluate_usingCustomClass_asVariableSelectField() throws Exception { Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableCelValue(true).build()) + CelFactory.plannerCelBuilder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) .addVar("a", CUSTOM_STRUCT_TYPE) .setTypeProvider(CUSTOM_STRUCT_TYPE_PROVIDER) .setValueProvider(CUSTOM_STRUCT_VALUE_PROVIDER) @@ -163,8 +179,8 @@ public void evaluate_usingCustomClass_asVariableSelectField() throws Exception { @Test public void evaluate_usingCustomClass_selectField() throws Exception { Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableCelValue(true).build()) + CelFactory.plannerCelBuilder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) .setTypeProvider(CUSTOM_STRUCT_TYPE_PROVIDER) .setValueProvider(CUSTOM_STRUCT_VALUE_PROVIDER) .build(); @@ -178,8 +194,8 @@ public void evaluate_usingCustomClass_selectField() throws Exception { @Test public void evaluate_usingMultipleProviders_selectFieldFromCustomClass() throws Exception { Cel cel = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().enableCelValue(true).build()) + CelFactory.plannerCelBuilder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) .setTypeProvider(CUSTOM_STRUCT_TYPE_PROVIDER) .setValueProvider( CombinedCelValueProvider.combine( @@ -197,19 +213,31 @@ public void evaluate_usingMultipleProviders_selectFieldFromCustomClass() throws // TODO: Bring back evaluate_usingMultipleProviders_selectFieldFromProtobufMessage // once planner is exposed from factory + private static class CustomPojo { + private final long data; + + CustomPojo(long data) { + this.data = data; + } + + long getData() { + return data; + } + } + @SuppressWarnings("Immutable") // Test only - private static class CelCustomStructValue extends StructValue { + private static class CelCustomStructValue extends StructValue { - private final long data; + private final CustomPojo pojo; @Override - public CelCustomStructValue value() { - return this; + public CustomPojo value() { + return pojo; } @Override public boolean isZeroValue() { - return data == 0; + return pojo.getData() == 0; } @Override @@ -226,7 +254,7 @@ public Object select(String field) { @Override public Optional find(String field) { if (field.equals("data")) { - return Optional.of(value().data); + return Optional.of(pojo.getData()); } return Optional.empty(); @@ -237,7 +265,11 @@ private CelCustomStructValue(Map fields) { } private CelCustomStructValue(long data) { - this.data = data; + this.pojo = new CustomPojo(data); + } + + private CelCustomStructValue(CustomPojo pojo) { + this.pojo = pojo; } } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java index 36485d5be..a2e8a9da6 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java @@ -87,9 +87,8 @@ Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { .newValue(structType.name(), Collections.unmodifiableMap(fieldValues)) .orElseThrow( () -> new IllegalArgumentException("Type name not found: " + structType.name())); - if (value instanceof StructValue) { - return ((StructValue) value).value(); + return ((StructValue) value).value(); } return value; From 21c3318538393114b9237bcc1c806a999b07f79c Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 28 Apr 2026 16:32:14 -0700 Subject: [PATCH 064/204] Policy nested rule fix PiperOrigin-RevId: 907239115 --- .../src/main/java/dev/cel/policy/BUILD.bazel | 4 +- .../java/dev/cel/policy/RuleComposer.java | 284 +++++++++++++----- .../cel/policy/CelPolicyCompilerImplTest.java | 29 +- .../java/dev/cel/policy/PolicyTestHelper.java | 28 +- .../src/test/resources/policy/k8s/tests.yaml | 31 +- .../test/resources/policy/limits/tests.yaml | 48 +-- .../resources/policy/nested_rule/tests.yaml | 39 +-- .../resources/policy/nested_rule2/tests.yaml | 68 +++-- .../resources/policy/nested_rule3/tests.yaml | 68 +++-- .../resources/policy/nested_rule4/config.yaml | 19 ++ .../resources/policy/nested_rule4/policy.yaml | 24 ++ .../resources/policy/nested_rule4/tests.yaml | 30 ++ .../resources/policy/nested_rule5/config.yaml | 19 ++ .../resources/policy/nested_rule5/policy.yaml | 30 ++ .../resources/policy/nested_rule5/tests.yaml | 42 +++ .../resources/policy/nested_rule6/config.yaml | 19 ++ .../resources/policy/nested_rule6/policy.yaml | 28 ++ .../resources/policy/nested_rule6/tests.yaml | 24 ++ .../resources/policy/nested_rule7/config.yaml | 19 ++ .../resources/policy/nested_rule7/policy.yaml | 29 ++ .../resources/policy/nested_rule7/tests.yaml | 42 +++ .../src/test/resources/policy/pb/tests.yaml | 35 +-- .../policy/required_labels/tests.yaml | 115 +++---- .../policy/restricted_destinations/tests.yaml | 200 ++++++------ 24 files changed, 893 insertions(+), 381 deletions(-) create mode 100644 testing/src/test/resources/policy/nested_rule4/config.yaml create mode 100644 testing/src/test/resources/policy/nested_rule4/policy.yaml create mode 100644 testing/src/test/resources/policy/nested_rule4/tests.yaml create mode 100644 testing/src/test/resources/policy/nested_rule5/config.yaml create mode 100644 testing/src/test/resources/policy/nested_rule5/policy.yaml create mode 100644 testing/src/test/resources/policy/nested_rule5/tests.yaml create mode 100644 testing/src/test/resources/policy/nested_rule6/config.yaml create mode 100644 testing/src/test/resources/policy/nested_rule6/policy.yaml create mode 100644 testing/src/test/resources/policy/nested_rule6/tests.yaml create mode 100644 testing/src/test/resources/policy/nested_rule7/config.yaml create mode 100644 testing/src/test/resources/policy/nested_rule7/policy.yaml create mode 100644 testing/src/test/resources/policy/nested_rule7/tests.yaml diff --git a/policy/src/main/java/dev/cel/policy/BUILD.bazel b/policy/src/main/java/dev/cel/policy/BUILD.bazel index 916f16f9b..a7bb90ffe 100644 --- a/policy/src/main/java/dev/cel/policy/BUILD.bazel +++ b/policy/src/main/java/dev/cel/policy/BUILD.bazel @@ -247,19 +247,19 @@ java_library( visibility = ["//visibility:private"], deps = [ ":compiled_rule", - "//:auto_value", "//bundle:cel", "//common:cel_ast", "//common:compiler_common", "//common:mutable_ast", + "//common:mutable_source", "//common:operator", "//common/ast", + "//common/ast:mutable_expr", "//common/formats:value_string", "//common/navigation:mutable_navigation", "//extensions:optional_library", "//optimizer:ast_optimizer", "//optimizer:mutable_ast", - "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], ) diff --git a/policy/src/main/java/dev/cel/policy/RuleComposer.java b/policy/src/main/java/dev/cel/policy/RuleComposer.java index 7bbde7685..cafef4b7c 100644 --- a/policy/src/main/java/dev/cel/policy/RuleComposer.java +++ b/policy/src/main/java/dev/cel/policy/RuleComposer.java @@ -18,15 +18,17 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static java.util.stream.Collectors.toCollection; -import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import dev.cel.bundle.Cel; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelMutableAst; +import dev.cel.common.CelMutableSource; import dev.cel.common.CelValidationException; import dev.cel.common.Operator; +import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import dev.cel.common.ast.CelMutableExpr; import dev.cel.common.formats.ValueString; import dev.cel.common.navigation.CelNavigableMutableAst; import dev.cel.common.navigation.CelNavigableMutableExpr; @@ -48,22 +50,11 @@ final class RuleComposer implements CelAstOptimizer { @Override public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) { - RuleOptimizationResult result = optimizeRule(cel, compiledRule); - return OptimizationResult.create(result.ast().toParsedAst()); + Step result = optimizeRule(cel, compiledRule); + return OptimizationResult.create(result.expr.toParsedAst()); } - @AutoValue - abstract static class RuleOptimizationResult { - abstract CelMutableAst ast(); - - abstract boolean isOptionalResult(); - - static RuleOptimizationResult create(CelMutableAst ast, boolean isOptionalResult) { - return new AutoValue_RuleComposer_RuleOptimizationResult(ast, isOptionalResult); - } - } - - private RuleOptimizationResult optimizeRule(Cel cel, CelCompiledRule compiledRule) { + private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) { cel = cel.toCelBuilder() .addVarDeclarations( @@ -72,81 +63,57 @@ private RuleOptimizationResult optimizeRule(Cel cel, CelCompiledRule compiledRul .collect(toImmutableList())) .build(); - CelMutableAst matchAst = astMutator.newGlobalCall(Function.OPTIONAL_NONE.getFunction()); - boolean isOptionalResult = true; - // Keep track of the last output ID that might cause type-check failure while attempting to - // compose the subgraphs. + Step output = null; + // If the rule has an optional output, the last result in the ternary should return + // `optional.none`. This output is implicit and created here to reflect the desired + // last possible output of this type of rule. + if (compiledRule.hasOptionalOutput()) { + output = + Step.newUnconditionalOptionalStep( + newTrueLiteral(), astMutator.newGlobalCall(Function.OPTIONAL_NONE.getFunction())); + } + long lastOutputId = 0; for (CelCompiledMatch match : Lists.reverse(compiledRule.matches())) { CelAbstractSyntaxTree conditionAst = match.condition(); - // If the condition is trivially true, none of the matches in the rule causes the result - // to become optional, and the rule is not the last match, then this will introduce - // unreachable outputs or rules. boolean isTriviallyTrue = match.isConditionTriviallyTrue(); + CelMutableAst condAst = CelMutableAst.fromCelAst(conditionAst); switch (match.result().kind()) { - // For the match's output, determine whether the output should be wrapped - // into an optional value, a conditional, or both. case OUTPUT: + // If the match has an output, then it is considered a non-optional output since + // it is explicitly stated. If the rule itself is optional, then the base case value + // of output being optional.none() will convert the non-optional value to an optional + // one. OutputValue matchOutput = match.result().output(); CelMutableAst outAst = CelMutableAst.fromCelAst(matchOutput.ast()); - if (isTriviallyTrue) { - matchAst = outAst; - isOptionalResult = false; - lastOutputId = matchOutput.sourceId(); - continue; - } - if (isOptionalResult) { - outAst = astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), outAst); - } - - matchAst = - astMutator.newGlobalCall( - Operator.CONDITIONAL.getFunction(), - CelMutableAst.fromCelAst(conditionAst), - outAst, - matchAst); + Step step = Step.newNonOptionalStep(!isTriviallyTrue, condAst, outAst); + output = combine(astMutator, step, output); + assertComposedAstIsValid( cel, - matchAst, + output.expr, "conflicting output types found.", matchOutput.sourceId(), lastOutputId); lastOutputId = matchOutput.sourceId(); - continue; + break; case RULE: // If the match has a nested rule, then compute the rule and whether it has // an optional return value. CelCompiledRule matchNestedRule = match.result().rule(); - RuleOptimizationResult nestedRule = optimizeRule(cel, matchNestedRule); + Step nestedRule = optimizeRule(cel, matchNestedRule); boolean nestedHasOptional = matchNestedRule.hasOptionalOutput(); - CelMutableAst nestedRuleAst = nestedRule.ast(); - if (isOptionalResult && !nestedHasOptional) { - nestedRuleAst = - astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), nestedRuleAst); - } - if (!isOptionalResult && nestedHasOptional) { - matchAst = astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), matchAst); - isOptionalResult = true; - } - // If either the nested rule or current condition output are optional then - // use optional.or() to specify the combination of the first and second results - // Note, the argument order is reversed due to the traversal of matches in - // reverse order. - if (isOptionalResult && isTriviallyTrue) { - matchAst = astMutator.newMemberCall(nestedRuleAst, Function.OR.getFunction(), matchAst); - } else { - matchAst = - astMutator.newGlobalCall( - Operator.CONDITIONAL.getFunction(), - CelMutableAst.fromCelAst(conditionAst), - nestedRuleAst, - matchAst); - } + + Step ruleStep = + nestedHasOptional + ? Step.newOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr) + : Step.newNonOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr); + output = combine(astMutator, ruleStep, output); assertComposedAstIsValid( cel, - matchAst, + output.expr, String.format( "failed composing the subrule '%s' due to conflicting output types.", matchNestedRule.ruleId().map(ValueString::value).orElse("")), @@ -155,11 +122,124 @@ private RuleOptimizationResult optimizeRule(Cel cel, CelCompiledRule compiledRul } } - CelMutableAst result = inlineCompiledVariables(matchAst, compiledRule.variables()); + CelMutableAst resultExpr = output.expr; + resultExpr = inlineCompiledVariables(resultExpr, compiledRule.variables()); + resultExpr = astMutator.renumberIdsConsecutively(resultExpr); + + return output.isOptional + ? Step.newUnconditionalOptionalStep(newTrueLiteral(), resultExpr) + : Step.newUnconditionalNonOptionalStep(newTrueLiteral(), resultExpr); + } + + static RuleComposer newInstance( + CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) { + return new RuleComposer(compiledRule, variablePrefix, iterationLimit); + } + + // Assembles two output expressions into a single output step. + private Step combine(AstMutator astMutator, Step currentStep, Step accumulatedStep) { + if (accumulatedStep == null) { + return currentStep; + } + CelMutableAst trueCondition = newTrueLiteral(); + + if (currentStep.isOptional) { + return combineWhenCurrentIsOptional(currentStep, accumulatedStep, astMutator, trueCondition); + } else { + return combineWhenCurrentIsNonOptional( + currentStep, accumulatedStep, astMutator, trueCondition); + } + } + + private Step combineWhenCurrentIsOptional( + Step currentStep, Step accumulatedStep, AstMutator astMutator, CelMutableAst trueCondition) { + // optional.combine(optional) // optional + // (optional && conditional).combine(non-optional) // optional + // (optional && unconditional).combine(non-optional) // non-optional + if (accumulatedStep.isOptional) { + if (currentStep.isConditional) { + return Step.newUnconditionalOptionalStep( + trueCondition, + astMutator.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + currentStep.cond, + currentStep.expr, + accumulatedStep.expr)); + } else { + if (!isOptionalNone(accumulatedStep.expr)) { + // If either the nested rule or current condition output are optional then + // use optional.or() to specify the combination of the first and second results + // Note, the argument order is reversed due to the traversal of matches in + // reverse order. + return Step.newUnconditionalOptionalStep( + trueCondition, + astMutator.newMemberCall(currentStep.expr, "or", accumulatedStep.expr)); + } + return currentStep; + } + } else { // accumulatedStep is non-optional + if (currentStep.isConditional) { + return Step.newUnconditionalOptionalStep( + trueCondition, + astMutator.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + currentStep.cond, + currentStep.expr, + astMutator.newGlobalCall( + Function.OPTIONAL_OF.getFunction(), accumulatedStep.expr))); + } else { + return Step.newUnconditionalNonOptionalStep( + trueCondition, + astMutator.newMemberCall(currentStep.expr, "orValue", accumulatedStep.expr)); + } + } + } + + private Step combineWhenCurrentIsNonOptional( + Step currentStep, Step accumulatedStep, AstMutator astMutator, CelMutableAst trueCondition) { + // non-optional.combine(non-optional) // non-optional + // (non-optional && conditional).combine(optional) // optional + // (non-optional && unconditional).combine(optional) // non-optional + // + // The last combination case is unusual, but effectively it means that the non-optional value + // prunes away + // the potential optional output. + if (accumulatedStep.isOptional) { + if (currentStep.isConditional) { + return Step.newUnconditionalOptionalStep( + trueCondition, + astMutator.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + currentStep.cond, + astMutator.newGlobalCall(Function.OPTIONAL_OF.getFunction(), currentStep.expr), + accumulatedStep.expr)); + } else { + // If the condition is trivially true, none of the matches in the rule causes the result + // to become optional, and the rule is not the last match, then this will introduce + // unreachable outputs or rules (pruning away 'accumulatedStep'). + return currentStep; + } + } else { // accumulatedStep is non-optional + return Step.newUnconditionalNonOptionalStep( + trueCondition, + astMutator.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + currentStep.cond, + currentStep.expr, + accumulatedStep.expr)); + } + } - result = astMutator.renumberIdsConsecutively(result); + private static boolean isOptionalNone(CelMutableAst ast) { + CelMutableExpr expr = ast.expr(); + return expr.getKind().equals(Kind.CALL) + && expr.call().function().equals("optional.none") + && expr.call().args().isEmpty(); + } - return RuleOptimizationResult.create(result, isOptionalResult); + private static CelMutableAst newTrueLiteral() { + return CelMutableAst.of( + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), CelMutableSource.newInstance()); } private CelMutableAst inlineCompiledVariables( @@ -186,11 +266,6 @@ private CelMutableAst inlineCompiledVariables( return mutatedAst; } - static RuleComposer newInstance( - CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) { - return new RuleComposer(compiledRule, variablePrefix, iterationLimit); - } - private void assertComposedAstIsValid( Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) { assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids)); @@ -206,10 +281,55 @@ private void assertComposedAstIsValid( } } - private RuleComposer(CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) { - this.compiledRule = checkNotNull(compiledRule); - this.variablePrefix = variablePrefix; - this.astMutator = AstMutator.newInstance(iterationLimit); + // Step represents an intermediate stage of rule and match expression composition. + // + // The CelCompiledRule and CelCompiledMatch types are meant to represent standalone tuples of + // condition and output expressions, and have no notion of how the order of combination would + // impact composition since composition rules may vary based on the policy execution semantic, + // e.g. first-match versus logical-or, logical-and, or accumulation. + private static class Step { + /** + * Indicates whether the output step has an optional result. Individual conditional attributes + * are not optional; however, rules and subrules can have optional output. + */ + private final boolean isOptional; + + /** True if the condition expression is not trivially true. */ + private final boolean isConditional; + + /** The condition associated with the output. */ + private final CelMutableAst cond; + + /** The output expression for the step. */ + private final CelMutableAst expr; + + private Step( + boolean isOptional, boolean isConditional, CelMutableAst cond, CelMutableAst expr) { + this.isOptional = isOptional; + this.isConditional = isConditional; + this.cond = cond; + this.expr = expr; + } + + private static Step newOptionalStep( + boolean isConditional, CelMutableAst cond, CelMutableAst expr) { + return new Step(/* isOptional= */ true, isConditional, cond, expr); + } + + private static Step newNonOptionalStep( + boolean isConditional, CelMutableAst cond, CelMutableAst expr) { + return new Step(/* isOptional= */ false, isConditional, cond, expr); + } + + private static Step newUnconditionalOptionalStep( + CelMutableAst trueCondition, CelMutableAst expr) { + return new Step(/* isOptional= */ true, /* isConditional= */ false, trueCondition, expr); + } + + private static Step newUnconditionalNonOptionalStep( + CelMutableAst trueCondition, CelMutableAst expr) { + return new Step(/* isOptional= */ false, /* isConditional= */ false, trueCondition, expr); + } } static final class RuleCompositionException extends RuntimeException { @@ -225,4 +345,10 @@ private RuleCompositionException( this.compileException = e; } } + + private RuleComposer(CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) { + this.compiledRule = checkNotNull(compiledRule); + this.variablePrefix = variablePrefix; + this.astMutator = AstMutator.newInstance(iterationLimit); + } } diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java index d4ca76324..35e249407 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java @@ -214,7 +214,30 @@ public void evaluateYamlPolicy_withCanonicalTestData( // Read the policy source String policySource = testData.yamlPolicy.readPolicyYamlContent(); CelPolicy policy = POLICY_PARSER.parse(policySource); - CelAbstractSyntaxTree expectedOutputAst = cel.compile(testData.testCase.getOutput()).getAst(); + Object outputObj = testData.testCase.getOutput(); + String exprToCompile; + if (outputObj instanceof String) { + exprToCompile = (String) outputObj; + } else if (outputObj instanceof Map) { + @SuppressWarnings("unchecked") // Test only + Map outputMap = (Map) outputObj; + if (outputMap.containsKey("value")) { + Object value = outputMap.get("value"); + if (value instanceof String) { + String escapedValue = ((String) value).replace("\"", "\\\""); + exprToCompile = "\"" + escapedValue + "\""; // Quote string literals + } else { + exprToCompile = String.valueOf(value); + } + } else if (outputMap.containsKey("expr")) { + exprToCompile = (String) outputMap.get("expr"); + } else { + throw new IllegalArgumentException("Invalid output format: " + outputObj); + } + } else { + throw new IllegalArgumentException("Invalid output format: " + outputObj); + } + CelAbstractSyntaxTree expectedOutputAst = cel.compile(exprToCompile).getAst(); Object expectedOutput = cel.createProgram(expectedOutputAst).eval(); // Act @@ -266,8 +289,8 @@ public void evaluateYamlPolicy_nestedRuleProducesOptionalOutput() throws Excepti CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy); Optional evalResult = (Optional) cel.createProgram(compiledPolicyAst).eval(); - // Result is Optional> - assertThat(evalResult).hasValue(Optional.of(true)); + // Result is Optional containing true + assertThat(evalResult).hasValue(true); } @Test diff --git a/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java b/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java index 18d5ffc69..59647f4d9 100644 --- a/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java +++ b/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java @@ -40,11 +40,11 @@ final class PolicyTestHelper { enum TestYamlPolicy { NESTED_RULE( "nested_rule", - true, + false, "cel.@block([resource.origin, @index0 in [\"us\", \"uk\", \"es\"], {\"banned\": true}]," + " ((@index0 in {\"us\": false, \"ru\": false, \"ir\": false} && !@index1) ?" - + " optional.of(@index2) : optional.none()).or(optional.of(@index1 ? {\"banned\":" - + " false} : @index2)))"), + + " optional.of(@index2) : optional.none()).orValue(@index1 ? {\"banned\":" + + " false} : @index2))"), NESTED_RULE2( "nested_rule2", false, @@ -61,6 +61,22 @@ enum TestYamlPolicy { + " false, \"ru\": false, \"ir\": false} && @index1) ? {\"banned\":" + " \"restricted_region\"} : {\"banned\": \"bad_actor\"}) : (@index1 ?" + " optional.of({\"banned\": \"unconfigured_region\"}) : optional.none()))"), + NESTED_RULE4("nested_rule4", false, "(x > 0) ? true : false"), + NESTED_RULE5( + "nested_rule5", + true, + "cel.@block([optional.of(true), optional.none()], (x > 0) ? ((x > 2) ? @index0 : @index1) :" + + " ((x > 1) ? ((x >= 2) ? @index0 : @index1) : optional.of(false)))"), + NESTED_RULE6( + "nested_rule6", + false, + "cel.@block([optional.of(true), optional.none()], ((x > 2) ? @index0 : @index1).orValue(((x" + + " > 3) ? @index0 : @index1).orValue(false)))"), + NESTED_RULE7( + "nested_rule7", + true, + "cel.@block([optional.of(true), optional.none()], ((x > 2) ? @index0 : @index1).or(((x > 3)" + + " ? @index0 : @index1).or((x > 1) ? optional.of(false) : @index1)))"), REQUIRED_LABELS( "required_labels", true, @@ -198,7 +214,7 @@ public List getTests() { public static final class PolicyTestCase { private String name; private Map input; - private String output; + private Object output; public void setName(String name) { this.name = name; @@ -208,7 +224,7 @@ public void setInput(Map input) { this.input = input; } - public void setOutput(String output) { + public void setOutput(Object output) { this.output = output; } @@ -220,7 +236,7 @@ public Map getInput() { return input; } - public String getOutput() { + public Object getOutput() { return output; } diff --git a/testing/src/test/resources/policy/k8s/tests.yaml b/testing/src/test/resources/policy/k8s/tests.yaml index 8585c5efb..f3e7de790 100644 --- a/testing/src/test/resources/policy/k8s/tests.yaml +++ b/testing/src/test/resources/policy/k8s/tests.yaml @@ -14,18 +14,19 @@ description: K8s admission control tests section: -- name: "invalid" - tests: - - name: "restricted_container" - input: - resource.namespace: - value: "dev.cel" - resource.labels: - value: - environment: "staging" - resource.containers: - value: - - staging.dev.cel.container1 - - staging.dev.cel.container2 - - preprod.dev.cel.container3 - output: "'only staging containers are allowed in namespace dev.cel'" + - name: "invalid" + tests: + - name: "restricted_container" + input: + resource.namespace: + value: "dev.cel" + resource.labels: + value: + environment: "staging" + resource.containers: + value: + - staging.dev.cel.container1 + - staging.dev.cel.container2 + - preprod.dev.cel.container3 + output: + value: "only staging containers are allowed in namespace dev.cel" diff --git a/testing/src/test/resources/policy/limits/tests.yaml b/testing/src/test/resources/policy/limits/tests.yaml index fe6daa61d..88772e075 100644 --- a/testing/src/test/resources/policy/limits/tests.yaml +++ b/testing/src/test/resources/policy/limits/tests.yaml @@ -14,25 +14,29 @@ description: Limits related tests section: -- name: "now_after_hours" - tests: - - name: "7pm" - input: - now: - expr: "timestamp('2024-07-30T00:30:00Z')" - output: "'hello, me'" - - name: "8pm" - input: - now: - expr: "timestamp('2024-07-30T20:30:00Z')" - output: "'goodbye, me!'" - - name: "9pm" - input: - now: - expr: "timestamp('2024-07-30T21:30:00Z')" - output: "'goodbye, me!!'" - - name: "11pm" - input: - now: - expr: "timestamp('2024-07-30T23:30:00Z')" - output: "'goodbye, me!!!'" \ No newline at end of file + - name: "now_after_hours" + tests: + - name: "7pm" + input: + now: + expr: "timestamp('2024-07-30T00:30:00Z')" + output: + value: "hello, me" + - name: "8pm" + input: + now: + expr: "timestamp('2024-07-30T20:30:00Z')" + output: + value: "goodbye, me!" + - name: "9pm" + input: + now: + expr: "timestamp('2024-07-30T21:30:00Z')" + output: + value: "goodbye, me!!" + - name: "11pm" + input: + now: + expr: "timestamp('2024-07-30T23:30:00Z')" + output: + value: "goodbye, me!!!" diff --git a/testing/src/test/resources/policy/nested_rule/tests.yaml b/testing/src/test/resources/policy/nested_rule/tests.yaml index a9807c376..3f9f63437 100644 --- a/testing/src/test/resources/policy/nested_rule/tests.yaml +++ b/testing/src/test/resources/policy/nested_rule/tests.yaml @@ -16,23 +16,26 @@ description: Nested rule conformance tests section: - name: "banned" tests: - - name: "restricted_origin" - input: - resource: - value: - origin: "ir" - output: "{'banned': true}" - - name: "by_default" - input: - resource: - value: - origin: "de" - output: "{'banned': true}" + - name: "restricted_origin" + input: + resource: + value: + origin: "ir" + output: + expr: "{'banned': true}" + - name: "by_default" + input: + resource: + value: + origin: "de" + output: + expr: "{'banned': true}" - name: "permitted" tests: - - name: "valid_origin" - input: - resource: - value: - origin: "uk" - output: "{'banned': false}" + - name: "valid_origin" + input: + resource: + value: + origin: "uk" + output: + expr: "{'banned': false}" diff --git a/testing/src/test/resources/policy/nested_rule2/tests.yaml b/testing/src/test/resources/policy/nested_rule2/tests.yaml index b5fbba745..0e1a9ca69 100644 --- a/testing/src/test/resources/policy/nested_rule2/tests.yaml +++ b/testing/src/test/resources/policy/nested_rule2/tests.yaml @@ -14,35 +14,39 @@ description: Nested rule conformance tests section: -- name: "banned" - tests: - - name: "restricted_origin" - input: - resource: - value: - user: "bad-user" - origin: "ir" - output: "{'banned': 'restricted_region'}" - - name: "by_default" - input: - resource: - value: - user: "bad-user" - origin: "de" - output: "{'banned': 'bad_actor'}" - - name: "unconfigured_region" - input: - resource: - value: - user: "good-user" - origin: "de" - output: "{'banned': 'unconfigured_region'}" -- name: "permitted" - tests: - - name: "valid_origin" - input: - resource: - value: - user: "good-user" - origin: "uk" - output: "{}" \ No newline at end of file + - name: "banned" + tests: + - name: "restricted_origin" + input: + resource: + value: + user: "bad-user" + origin: "ir" + output: + expr: "{'banned': 'restricted_region'}" + - name: "by_default" + input: + resource: + value: + user: "bad-user" + origin: "de" + output: + expr: "{'banned': 'bad_actor'}" + - name: "unconfigured_region" + input: + resource: + value: + user: "good-user" + origin: "de" + output: + expr: "{'banned': 'unconfigured_region'}" + - name: "permitted" + tests: + - name: "valid_origin" + input: + resource: + value: + user: "good-user" + origin: "uk" + output: + expr: "{}" diff --git a/testing/src/test/resources/policy/nested_rule3/tests.yaml b/testing/src/test/resources/policy/nested_rule3/tests.yaml index b10785d0c..9d993c65f 100644 --- a/testing/src/test/resources/policy/nested_rule3/tests.yaml +++ b/testing/src/test/resources/policy/nested_rule3/tests.yaml @@ -14,35 +14,39 @@ description: Nested rule conformance tests section: -- name: "banned" - tests: - - name: "restricted_origin" - input: - resource: - value: - user: "bad-user" - origin: "ir" - output: "{'banned': 'restricted_region'}" - - name: "by_default" - input: - resource: - value: - user: "bad-user" - origin: "de" - output: "{'banned': 'bad_actor'}" - - name: "unconfigured_region" - input: - resource: - value: - user: "good-user" - origin: "de" - output: "{'banned': 'unconfigured_region'}" -- name: "permitted" - tests: - - name: "valid_origin" - input: - resource: - value: - user: "good-user" - origin: "uk" - output: "optional.none()" \ No newline at end of file + - name: "banned" + tests: + - name: "restricted_origin" + input: + resource: + value: + user: "bad-user" + origin: "ir" + output: + expr: "{'banned': 'restricted_region'}" + - name: "by_default" + input: + resource: + value: + user: "bad-user" + origin: "de" + output: + expr: "{'banned': 'bad_actor'}" + - name: "unconfigured_region" + input: + resource: + value: + user: "good-user" + origin: "de" + output: + expr: "{'banned': 'unconfigured_region'}" + - name: "permitted" + tests: + - name: "valid_origin" + input: + resource: + value: + user: "good-user" + origin: "uk" + output: + expr: "optional.none()" diff --git a/testing/src/test/resources/policy/nested_rule4/config.yaml b/testing/src/test/resources/policy/nested_rule4/config.yaml new file mode 100644 index 000000000..5afb8c587 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule4/config.yaml @@ -0,0 +1,19 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "nested_rule4" +variables: + - name: x + type: + type_name: int diff --git a/testing/src/test/resources/policy/nested_rule4/policy.yaml b/testing/src/test/resources/policy/nested_rule4/policy.yaml new file mode 100644 index 000000000..ea53bfb25 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule4/policy.yaml @@ -0,0 +1,24 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: nested_rule4 +rule: + match: + - condition: x > 0 + rule: + match: + - rule: + match: + - output: "true" + - output: "false" diff --git a/testing/src/test/resources/policy/nested_rule4/tests.yaml b/testing/src/test/resources/policy/nested_rule4/tests.yaml new file mode 100644 index 000000000..006eddb88 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule4/tests.yaml @@ -0,0 +1,30 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +description: "Nested rule tests which explore optional vs non-optional returns" +section: + - name: "valid" + tests: + - name: "x=0" + input: + x: + value: 0 + output: + value: false + - name: "x=2" + input: + x: + value: 2 + output: + value: true diff --git a/testing/src/test/resources/policy/nested_rule5/config.yaml b/testing/src/test/resources/policy/nested_rule5/config.yaml new file mode 100644 index 000000000..499450090 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule5/config.yaml @@ -0,0 +1,19 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "nested_rule5" +variables: + - name: x + type: + type_name: int diff --git a/testing/src/test/resources/policy/nested_rule5/policy.yaml b/testing/src/test/resources/policy/nested_rule5/policy.yaml new file mode 100644 index 000000000..e43dce188 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule5/policy.yaml @@ -0,0 +1,30 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: nested_rule5 +rule: + match: + - condition: x > 0 + rule: + match: + - rule: + match: + - condition: "x > 2" + output: "true" + - condition: x > 1 + rule: + match: + - condition: "x >= 2" + output: "true" + - output: "false" diff --git a/testing/src/test/resources/policy/nested_rule5/tests.yaml b/testing/src/test/resources/policy/nested_rule5/tests.yaml new file mode 100644 index 000000000..8cd794051 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule5/tests.yaml @@ -0,0 +1,42 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +description: "Nested rule tests which explore optional vs non-optional returns" +section: + - name: "valid" + tests: + - name: "x=0" + input: + x: + value: 0 + output: + value: false + - name: "x=1" + input: + x: + value: 1 + output: + expr: "optional.none()" + - name: "x=2" + input: + x: + value: 2 + output: + expr: "optional.none()" + - name: "x=3" + input: + x: + value: 3 + output: + value: true diff --git a/testing/src/test/resources/policy/nested_rule6/config.yaml b/testing/src/test/resources/policy/nested_rule6/config.yaml new file mode 100644 index 000000000..a5b1ee16b --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule6/config.yaml @@ -0,0 +1,19 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "nested_rule6" +variables: + - name: x + type: + type_name: int diff --git a/testing/src/test/resources/policy/nested_rule6/policy.yaml b/testing/src/test/resources/policy/nested_rule6/policy.yaml new file mode 100644 index 000000000..a3360e7c1 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule6/policy.yaml @@ -0,0 +1,28 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: nested_rule6 +rule: + match: + - rule: + match: + - rule: + match: + - condition: "x > 2" + output: "true" + - rule: + match: + - condition: "x > 3" + output: "true" + - output: "false" diff --git a/testing/src/test/resources/policy/nested_rule6/tests.yaml b/testing/src/test/resources/policy/nested_rule6/tests.yaml new file mode 100644 index 000000000..fef586df0 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule6/tests.yaml @@ -0,0 +1,24 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +description: "Nested rule tests which explore optional vs non-optional returns" +section: + - name: "valid" + tests: + - name: "x=0" + input: + x: + value: 0 + output: + value: false diff --git a/testing/src/test/resources/policy/nested_rule7/config.yaml b/testing/src/test/resources/policy/nested_rule7/config.yaml new file mode 100644 index 000000000..74d4d8c2d --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule7/config.yaml @@ -0,0 +1,19 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "nested_rule7" +variables: + - name: x + type: + type_name: int diff --git a/testing/src/test/resources/policy/nested_rule7/policy.yaml b/testing/src/test/resources/policy/nested_rule7/policy.yaml new file mode 100644 index 000000000..fcacd017e --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule7/policy.yaml @@ -0,0 +1,29 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: nested_rule7 +rule: + match: + - rule: + match: + - rule: + match: + - condition: "x > 2" + output: "true" + - rule: + match: + - condition: "x > 3" + output: "true" + - condition: "x > 1" + output: "false" diff --git a/testing/src/test/resources/policy/nested_rule7/tests.yaml b/testing/src/test/resources/policy/nested_rule7/tests.yaml new file mode 100644 index 000000000..ec2896878 --- /dev/null +++ b/testing/src/test/resources/policy/nested_rule7/tests.yaml @@ -0,0 +1,42 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +description: "Nested rule tests which explore optional vs non-optional returns" +section: + - name: "valid" + tests: + - name: "x=1" + input: + x: + value: 1 + output: + expr: "optional.none()" + - name: "x=2" + input: + x: + value: 2 + output: + value: false + - name: "x=3" + input: + x: + value: 3 + output: + value: true + - name: "x=4" + input: + x: + value: 4 + output: + value: true diff --git a/testing/src/test/resources/policy/pb/tests.yaml b/testing/src/test/resources/policy/pb/tests.yaml index 82dd6b11b..71cd56b57 100644 --- a/testing/src/test/resources/policy/pb/tests.yaml +++ b/testing/src/test/resources/policy/pb/tests.yaml @@ -14,20 +14,21 @@ description: "Protobuf input tests" section: -- name: "valid" - tests: - - name: "good spec" - input: - spec: - expr: > - TestAllTypes{single_int32: 10} - output: "optional.none()" -- name: "invalid" - tests: - - name: "bad spec" - input: - spec: - expr: > - TestAllTypes{single_int32: 11} - output: > - "invalid spec, got single_int32=11, wanted <= 10" + - name: "valid" + tests: + - name: "good spec" + input: + spec: + expr: > + TestAllTypes{single_int32: 10} + output: + expr: "optional.none()" + - name: "invalid" + tests: + - name: "bad spec" + input: + spec: + expr: > + TestAllTypes{single_int32: 11} + output: + value: "invalid spec, got single_int32=11, wanted <= 10" diff --git a/testing/src/test/resources/policy/required_labels/tests.yaml b/testing/src/test/resources/policy/required_labels/tests.yaml index 67681ef46..4296c6914 100644 --- a/testing/src/test/resources/policy/required_labels/tests.yaml +++ b/testing/src/test/resources/policy/required_labels/tests.yaml @@ -16,64 +16,65 @@ description: "Required labels conformance tests" section: - name: "valid" tests: - - name: "matching" - input: - spec: - value: - labels: - env: prod - experiment: "group b" - resource: - value: - labels: - env: prod - experiment: "group b" - release: "v0.1.0" - output: "optional.none()" + - name: "matching" + input: + spec: + value: + labels: + env: prod + experiment: "group b" + resource: + value: + labels: + env: prod + experiment: "group b" + release: "v0.1.0" + output: + expr: "optional.none()" - name: "missing" tests: - - name: "env" - input: - spec: - value: - labels: - env: prod - experiment: "group b" - resource: - value: - labels: - experiment: "group b" - release: "v0.1.0" - output: > - "missing one or more required labels: [\"env\"]" - - name: "experiment" - input: - spec: - value: - labels: - env: prod - experiment: "group b" - resource: - value: - labels: - env: staging - release: "v0.1.0" - output: > - "missing one or more required labels: [\"experiment\"]" + - name: "env" + input: + spec: + value: + labels: + env: prod + experiment: "group b" + resource: + value: + labels: + experiment: "group b" + release: "v0.1.0" + output: + value: "missing one or more required labels: [\"env\"]" + - name: "experiment" + input: + spec: + value: + labels: + env: prod + experiment: "group b" + resource: + value: + labels: + env: staging + release: "v0.1.0" + output: + value: "missing one or more required labels: [\"experiment\"]" - name: "invalid" tests: - - name: "env" - input: - spec: - value: - labels: - env: prod - experiment: "group b" - resource: - value: - labels: - env: staging - experiment: "group b" - release: "v0.1.0" - output: > - "invalid values provided on one or more labels: [\"env\"]" + - name: "env" + input: + spec: + value: + labels: + env: prod + experiment: "group b" + resource: + value: + labels: + env: staging + experiment: "group b" + release: "v0.1.0" + output: + value: "invalid values provided on one or more labels: [\"env\"]" diff --git a/testing/src/test/resources/policy/restricted_destinations/tests.yaml b/testing/src/test/resources/policy/restricted_destinations/tests.yaml index c0feeb202..f7ae36550 100644 --- a/testing/src/test/resources/policy/restricted_destinations/tests.yaml +++ b/testing/src/test/resources/policy/restricted_destinations/tests.yaml @@ -16,103 +16,107 @@ description: Restricted destinations conformance tests. section: - name: "valid" tests: - - name: "ip_allowed" - input: - "spec.origin": - value: "us" - "spec.restricted_destinations": - value: - - "cu" - - "ir" - - "kp" - - "sd" - - "sy" - "destination.ip": - value: "10.0.0.1" - "origin.ip": - value: "10.0.0.1" - request: - value: - auth: - claims: {} - resource: - value: - name: "/company/acme/secrets/doomsday-device" - labels: - location: "us" - output: "false" # false means unrestricted - - name: "nationality_allowed" - input: - "spec.origin": - value: "us" - "spec.restricted_destinations": - value: - - "cu" - - "ir" - - "kp" - - "sd" - - "sy" - "destination.ip": - value: "10.0.0.1" - request: - value: - auth: - claims: - nationality: "us" - resource: - value: - name: "/company/acme/secrets/doomsday-device" - labels: - location: "us" - output: "false" + - name: "ip_allowed" + input: + spec.origin: + value: "us" + spec.restricted_destinations: + value: + - "cu" + - "ir" + - "kp" + - "sd" + - "sy" + destination.ip: + value: "10.0.0.1" + origin.ip: + value: "10.0.0.1" + request: + value: + auth: + claims: {} + resource: + value: + name: "/company/acme/secrets/doomsday-device" + labels: + location: "us" + output: + value: false # false means unrestricted + - name: "nationality_allowed" + input: + spec.origin: + value: "us" + spec.restricted_destinations: + value: + - "cu" + - "ir" + - "kp" + - "sd" + - "sy" + destination.ip: + value: "10.0.0.1" + request: + value: + auth: + claims: + nationality: "us" + resource: + value: + name: "/company/acme/secrets/doomsday-device" + labels: + location: "us" + output: + value: false - name: "invalid" tests: - - name: "destination_ip_prohibited" - input: - "spec.origin": - value: "us" - "spec.restricted_destinations": - value: - - "cu" - - "ir" - - "kp" - - "sd" - - "sy" - "destination.ip": - value: "123.123.123.123" - "origin.ip": - value: "10.0.0.1" - request: - value: - auth: - claims: {} - resource: - value: - name: "/company/acme/secrets/doomsday-device" - labels: - location: "us" - output: "true" # true means restricted - - name: "resource_nationality_prohibited" - input: - "spec.origin": - value: "us" - "spec.restricted_destinations": - value: - - "cu" - - "ir" - - "kp" - - "sd" - - "sy" - "destination.ip": - value: "10.0.0.1" - request: - value: - auth: - claims: - nationality: "us" - resource: - value: - name: "/company/acme/secrets/doomsday-device" - labels: - location: "cu" - output: "true" + - name: "destination_ip_prohibited" + input: + spec.origin: + value: "us" + spec.restricted_destinations: + value: + - "cu" + - "ir" + - "kp" + - "sd" + - "sy" + destination.ip: + value: "123.123.123.123" + origin.ip: + value: "10.0.0.1" + request: + value: + auth: + claims: {} + resource: + value: + name: "/company/acme/secrets/doomsday-device" + labels: + location: "us" + output: + value: true # true means restricted + - name: "resource_nationality_prohibited" + input: + spec.origin: + value: "us" + spec.restricted_destinations: + value: + - "cu" + - "ir" + - "kp" + - "sd" + - "sy" + destination.ip: + value: "10.0.0.1" + request: + value: + auth: + claims: + nationality: "us" + resource: + value: + name: "/company/acme/secrets/doomsday-device" + labels: + location: "cu" + output: + value: true From 9ff1f233efd1a9e2f4d8b22e24e13e35cc83fb06 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 1 May 2026 11:54:57 -0700 Subject: [PATCH 065/204] Introduce CEL Policy Conformance Test Runner for Java PiperOrigin-RevId: 908834020 --- .../dev/cel/conformance/policy/BUILD.bazel | 25 +++ .../policy/PolicyConformanceTest.java | 72 +++++++ .../policy/PolicyConformanceTestRunner.java | 190 ++++++++++++++++++ .../policy/PolicyConformanceTests.java | 21 ++ .../policy/cel_policy_conformance_test.bzl | 50 +++++ 5 files changed, 358 insertions(+) create mode 100644 conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel create mode 100644 conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java create mode 100644 conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTestRunner.java create mode 100644 conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTests.java create mode 100644 conformance/src/test/java/dev/cel/conformance/policy/cel_policy_conformance_test.bzl diff --git a/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel new file mode 100644 index 000000000..9a9b14f74 --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel @@ -0,0 +1,25 @@ +load("@rules_java//java:defs.bzl", "java_library") +load(":cel_policy_conformance_test.bzl", "cel_policy_conformance_test_java") + +package( + default_applicable_licenses = ["//:license"], + default_testonly = True, +) + +java_library( + name = "run", + srcs = glob(["*.java"]), + deps = [ + "//:auto_value", + "//bundle:cel", + "//testing/testrunner:cel_expression_source", + "//testing/testrunner:cel_test_context", + "//testing/testrunner:cel_test_suite", + "//testing/testrunner:cel_test_suite_text_proto_parser", + "//testing/testrunner:cel_test_suite_yaml_parser", + "//testing/testrunner:test_runner_library", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:junit_junit", + ], +) diff --git a/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java new file mode 100644 index 000000000..700539927 --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java @@ -0,0 +1,72 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.conformance.policy; + +import com.google.protobuf.ListValue; +import com.google.protobuf.Struct; +import com.google.protobuf.Value; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelFactory; +import dev.cel.testing.testrunner.CelExpressionSource; +import dev.cel.testing.testrunner.CelTestContext; +import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase; +import dev.cel.testing.testrunner.TestRunnerLibrary; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.runners.model.Statement; + +/** Statement representing a single CEL policy conformance test case. */ +public final class PolicyConformanceTest extends Statement { + + private static final Cel CEL = CelFactory.standardCelBuilder().build(); + + private final String name; + private final CelTestCase testCase; + private final String dirPath; + + public PolicyConformanceTest(String name, CelTestCase testCase, String dirPath) { + this.name = name; + this.testCase = testCase; + this.dirPath = dirPath; + } + + public String getName() { + return name; + } + + @Override + public void evaluate() throws Throwable { + String policyFile = Paths.get(dirPath, "policy.yaml").toString(); + + CelTestContext.Builder contextBuilder = + CelTestContext.newBuilder() + .setCelExpression(CelExpressionSource.fromSource(policyFile)) + .setCel(CEL) + .addMessageTypes( + Struct.getDescriptor(), Value.getDescriptor(), ListValue.getDescriptor()); + + Path yamlConfigPath = Paths.get(dirPath, "config.yaml"); + Path textprotoConfigPath = Paths.get(dirPath, "config.textproto"); + + if (Files.exists(yamlConfigPath)) { + contextBuilder.setConfigFile(yamlConfigPath.toString()); + } else if (Files.exists(textprotoConfigPath)) { + contextBuilder.setConfigFile(textprotoConfigPath.toString()); + } + + TestRunnerLibrary.runTest(testCase, contextBuilder.build()); + } +} diff --git a/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTestRunner.java b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTestRunner.java new file mode 100644 index 000000000..6d1d86e47 --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTestRunner.java @@ -0,0 +1,190 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.conformance.policy; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Files; +import com.google.protobuf.ListValue; +import com.google.protobuf.Struct; +import com.google.protobuf.TypeRegistry; +import com.google.protobuf.Value; +import dev.cel.testing.testrunner.CelTestSuite; +import dev.cel.testing.testrunner.CelTestSuite.CelTestSection; +import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase; +import dev.cel.testing.testrunner.CelTestSuiteTextProtoParser; +import dev.cel.testing.testrunner.CelTestSuiteYamlParser; +import java.io.File; +import java.util.Arrays; +import java.util.List; +import org.junit.runner.Description; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.ParentRunner; +import org.junit.runners.model.InitializationError; + +/** Custom JUnit runner for CEL policy conformance tests. */ +public final class PolicyConformanceTestRunner extends ParentRunner { + + private static final Splitter SPLITTER = Splitter.on(",").omitEmptyStrings(); + private static final String TESTS_YAML_FILE_NAME = "tests.yaml"; + private static final String TESTS_TEXTPROTO_FILE_NAME = "tests.textproto"; + private static final TypeRegistry TYPE_REGISTRY = + TypeRegistry.newBuilder() + .add(Struct.getDescriptor()) + .add(Value.getDescriptor()) + .add(ListValue.getDescriptor()) + .build(); + + private static final String TEST_DIRS_PROP = + System.getProperty("dev.cel.policy.conformance.tests"); + private static final String TESTDATA_DIR = + System.getProperty("dev.cel.policy.conformance.testdata_dir", "testdata"); + private static final String SKIP_TESTS_PROP = + System.getProperty("dev.cel.policy.conformance.skip_tests"); + + private static final ImmutableList TESTS_TO_SKIP = + Strings.isNullOrEmpty(SKIP_TESTS_PROP) + ? ImmutableList.of() + : ImmutableList.copyOf(SPLITTER.splitToList(SKIP_TESTS_PROP)); + + private static final ImmutableList TEST_DIRS = + Strings.isNullOrEmpty(TEST_DIRS_PROP) + ? discoverTestDirs(TESTDATA_DIR) + : ImmutableList.copyOf(SPLITTER.splitToList(TEST_DIRS_PROP)); + + private static ImmutableList discoverTestDirs(String testdataDir) { + File dir = new File(testdataDir); + if (!dir.exists() || !dir.isDirectory()) { + return ImmutableList.of(); + } + String[] directories = dir.list((current, name) -> new File(current, name).isDirectory()); + if (directories == null) { + return ImmutableList.of(); + } + Arrays.sort(directories); + return ImmutableList.copyOf(directories); + } + + private final ImmutableList tests; + + private ImmutableList loadTests() { + if (TEST_DIRS.isEmpty()) { + return ImmutableList.of(); + } + + ImmutableList.Builder testsBuilder = ImmutableList.builder(); + + for (String dir : TEST_DIRS) { + String fullDirPath = TESTDATA_DIR + "/" + dir; + try { + ImmutableList suites = readTestSuites(fullDirPath); + for (CelTestSuiteContext namedSuite : suites) { + for (CelTestSection section : namedSuite.testSuite().sections()) { + for (CelTestCase testCase : section.tests()) { + String baseName = String.format("%s/%s/%s", dir, section.name(), testCase.name()); + String displayName = baseName + namedSuite.formatSuffix(); + if (!shouldSkipTest(baseName, TESTS_TO_SKIP)) { + testsBuilder.add(new PolicyConformanceTest(displayName, testCase, fullDirPath)); + } + } + } + } + } catch (Exception e) { + throw new RuntimeException("Failed to load test suite in " + fullDirPath, e); + } + } + return testsBuilder.build(); + } + + private static boolean shouldSkipTest(String name, List testsToSkip) { + for (String testToSkip : testsToSkip) { + if (name.startsWith(testToSkip)) { + String consumedName = name.substring(testToSkip.length()); + if (consumedName.isEmpty() || consumedName.startsWith("/")) { + return true; + } + } + } + return false; + } + + private static ImmutableList readTestSuites(String dirPath) + throws Exception { + File dir = new File(dirPath); + File yamlFile = new File(dir, TESTS_YAML_FILE_NAME); + File textprotoFile = new File(dir, TESTS_TEXTPROTO_FILE_NAME); + + boolean bothExist = yamlFile.exists() && textprotoFile.exists(); + ImmutableList.Builder suitesBuilder = ImmutableList.builder(); + + if (yamlFile.exists()) { + suitesBuilder.add( + CelTestSuiteContext.create( + CelTestSuiteYamlParser.newInstance() + .parse(Files.asCharSource(yamlFile, UTF_8).read()), + bothExist ? " (yaml)" : "")); + } + if (textprotoFile.exists()) { + suitesBuilder.add( + CelTestSuiteContext.create( + CelTestSuiteTextProtoParser.newInstance() + .parse(Files.asCharSource(textprotoFile, UTF_8).read(), TYPE_REGISTRY), + bothExist ? " (textproto)" : "")); + } + + ImmutableList suites = suitesBuilder.build(); + if (suites.isEmpty()) { + throw new IllegalArgumentException( + String.format( + "No %s or %s found in %s", TESTS_YAML_FILE_NAME, TESTS_TEXTPROTO_FILE_NAME, dirPath)); + } + return suites; + } + + @Override + protected ImmutableList getChildren() { + return tests; + } + + @Override + protected Description describeChild(PolicyConformanceTest child) { + return Description.createTestDescription(getTestClass().getJavaClass(), child.getName()); + } + + @Override + protected void runChild(PolicyConformanceTest child, RunNotifier notifier) { + runLeaf(child, describeChild(child), notifier); + } + + public PolicyConformanceTestRunner(Class clazz) throws InitializationError { + super(clazz); + this.tests = loadTests(); + } + + @AutoValue + abstract static class CelTestSuiteContext { + abstract CelTestSuite testSuite(); + + abstract String formatSuffix(); + + static CelTestSuiteContext create(CelTestSuite testSuite, String formatSuffix) { + return new AutoValue_PolicyConformanceTestRunner_CelTestSuiteContext(testSuite, formatSuffix); + } + } +} diff --git a/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTests.java b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTests.java new file mode 100644 index 000000000..46596763e --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTests.java @@ -0,0 +1,21 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.conformance.policy; + +import org.junit.runner.RunWith; + +/** Main test class for CEL policy conformance tests. */ +@RunWith(PolicyConformanceTestRunner.class) +public class PolicyConformanceTests {} diff --git a/conformance/src/test/java/dev/cel/conformance/policy/cel_policy_conformance_test.bzl b/conformance/src/test/java/dev/cel/conformance/policy/cel_policy_conformance_test.bzl new file mode 100644 index 000000000..3e3720ec5 --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/policy/cel_policy_conformance_test.bzl @@ -0,0 +1,50 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Macro to run CEL policy conformance tests.""" + +load("@rules_java//java:defs.bzl", "java_test") + +def cel_policy_conformance_test_java( + name, + testdata, + test_cases = [], + skip_tests = [], + **kwargs): + """Macro to run CEL policy conformance tests for Java. + + Args: + name: The name of the test target. + testdata: Testdata filegroup target. + test_cases: (optional) List of test case names (directory names) to run. + skip_tests: (optional) List of test case names (directory names) to skip. + **kwargs: Other standard Bazel target attributes. + """ + + lbl = native.package_relative_label(testdata) + testdata_dir = lbl.package + "/" + lbl.name + + java_test( + name = name, + jvm_flags = [ + "-Ddev.cel.policy.conformance.tests=" + ",".join(test_cases), + "-Ddev.cel.policy.conformance.testdata_dir=" + testdata_dir, + "-Ddev.cel.policy.conformance.skip_tests=" + ",".join(skip_tests), + ], + data = [testdata], + size = "small", + test_class = "dev.cel.conformance.policy.PolicyConformanceTests", + runtime_deps = [Label(":run")], + **kwargs + ) From fc24bfde5a9a6f82e1830abdec98f169a761576f Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 1 May 2026 15:07:48 -0700 Subject: [PATCH 066/204] Internal Changes PiperOrigin-RevId: 908917795 --- .../dev/cel/conformance/policy/BUILD.bazel | 1 + .../policy/PolicyConformanceTest.java | 21 ++++++++++++++++++- .../testrunner/CelTestSuiteYamlParser.java | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel index 9a9b14f74..236bacd93 100644 --- a/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel @@ -12,6 +12,7 @@ java_library( deps = [ "//:auto_value", "//bundle:cel", + "//runtime:function_binding", "//testing/testrunner:cel_expression_source", "//testing/testrunner:cel_test_context", "//testing/testrunner:cel_test_suite", diff --git a/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java index 700539927..5d6c84dcd 100644 --- a/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java +++ b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java @@ -19,6 +19,7 @@ import com.google.protobuf.Value; import dev.cel.bundle.Cel; import dev.cel.bundle.CelFactory; +import dev.cel.runtime.CelFunctionBinding; import dev.cel.testing.testrunner.CelExpressionSource; import dev.cel.testing.testrunner.CelTestContext; import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase; @@ -31,7 +32,25 @@ /** Statement representing a single CEL policy conformance test case. */ public final class PolicyConformanceTest extends Statement { - private static final Cel CEL = CelFactory.standardCelBuilder().build(); + private static final Cel CEL = + CelFactory.standardCelBuilder() + .addFunctionBindings( + CelFunctionBinding.fromOverloads( + "locationCode", + CelFunctionBinding.from( + "locationCode_string", + String.class, + (ip) -> { + switch (ip) { + case "10.0.0.1": + return "us"; + case "10.0.0.2": + return "de"; + default: + return "ir"; + } + }))) + .build(); private final String name; private final CelTestCase testCase; diff --git a/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteYamlParser.java b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteYamlParser.java index 71c4b9231..2340bf229 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteYamlParser.java +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteYamlParser.java @@ -90,7 +90,7 @@ private CelTestSuite parseYaml(String celTestSuiteYamlContent, String descriptio } private CelTestSuite.Builder parseTestSuite(ParserContext ctx, Node node) { - CelTestSuite.Builder builder = CelTestSuite.newBuilder(); + CelTestSuite.Builder builder = CelTestSuite.newBuilder().setName("").setDescription(""); long id = ctx.collectMetadata(node); if (!assertYamlType(ctx, id, node, YamlNodeType.MAP)) { ctx.reportError(id, "Unknown test suite type: " + node.getTag()); From d08c4243a85b4394d0e929c76b912763f170eec2 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 4 May 2026 15:29:11 -0700 Subject: [PATCH 067/204] Internal Changes PiperOrigin-RevId: 910276432 --- .../src/test/java/dev/cel/conformance/policy/BUILD.bazel | 1 + .../dev/cel/conformance/policy/PolicyConformanceTest.java | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel index 236bacd93..ca34530fb 100644 --- a/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel @@ -19,6 +19,7 @@ java_library( "//testing/testrunner:cel_test_suite_text_proto_parser", "//testing/testrunner:cel_test_suite_yaml_parser", "//testing/testrunner:test_runner_library", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", "@maven//:junit_junit", diff --git a/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java index 5d6c84dcd..cd24339c0 100644 --- a/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java +++ b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java @@ -14,11 +14,10 @@ package dev.cel.conformance.policy; -import com.google.protobuf.ListValue; import com.google.protobuf.Struct; -import com.google.protobuf.Value; import dev.cel.bundle.Cel; import dev.cel.bundle.CelFactory; +import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.runtime.CelFunctionBinding; import dev.cel.testing.testrunner.CelExpressionSource; import dev.cel.testing.testrunner.CelTestContext; @@ -74,8 +73,9 @@ public void evaluate() throws Throwable { CelTestContext.newBuilder() .setCelExpression(CelExpressionSource.fromSource(policyFile)) .setCel(CEL) - .addMessageTypes( - Struct.getDescriptor(), Value.getDescriptor(), ListValue.getDescriptor()); + .addFileTypes( + TestAllTypes.getDescriptor().getFile(), + Struct.getDescriptor().getFile()); Path yamlConfigPath = Paths.get(dirPath, "config.yaml"); Path textprotoConfigPath = Paths.get(dirPath, "config.textproto"); From 5fc176681ecf09b360f5b503228d916e9e89d6fc Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 5 May 2026 14:45:32 -0700 Subject: [PATCH 068/204] Implement context_variable support in YAML environment PiperOrigin-RevId: 910924913 --- .../src/main/java/dev/cel/bundle/BUILD.bazel | 1 + .../java/dev/cel/bundle/CelEnvironment.java | 23 +++++++++++++ .../cel/bundle/CelEnvironmentYamlParser.java | 34 +++++++++++++++++++ .../dev/cel/testing/testrunner/BUILD.bazel | 1 + .../testing/testrunner/CelTestContext.java | 22 ++++++------ .../testing/testrunner/TestRunnerLibrary.java | 33 +++++++++++++----- .../testrunner/TestRunnerLibraryTest.java | 2 +- 7 files changed, 95 insertions(+), 21 deletions(-) diff --git a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel index 742f718f1..716442849 100644 --- a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel +++ b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel @@ -104,6 +104,7 @@ java_library( ":required_fields_checker", "//:auto_value", "//bundle:cel", + "//checker:proto_type_mask", "//checker:standard_decl", "//common:compiler_common", "//common:container", diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java index b85f16cb1..ccbaef61b 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java @@ -30,6 +30,7 @@ import dev.cel.checker.CelStandardDeclarations; import dev.cel.checker.CelStandardDeclarations.StandardFunction; import dev.cel.checker.CelStandardDeclarations.StandardOverload; +import dev.cel.checker.ProtoTypeMask; import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; @@ -134,6 +135,9 @@ public abstract class CelEnvironment { /** Limits to set in the environment. */ public abstract ImmutableSet limits(); + /** Context variable to enable in the environment. */ + public abstract Optional contextVariable(); + /** Builder for {@link CelEnvironment}. */ @AutoValue.Builder public abstract static class Builder { @@ -199,6 +203,8 @@ public Builder setLimits(Limit... limits) { public abstract Builder setLimits(ImmutableSet limits); + public abstract Builder setContextVariable(ContextVariable contextVariable); + abstract CelEnvironment autoBuild(); @CheckReturnValue @@ -258,6 +264,12 @@ public CelCompiler extend(CelCompiler celCompiler, CelOptions celOptions) applyStandardLibrarySubset(compilerBuilder); + contextVariable() + .ifPresent( + cv -> + compilerBuilder.addProtoTypeMasks( + ProtoTypeMask.ofAllFields(cv.typeName()).withFieldsAsVariableDeclarations())); + return compilerBuilder.build(); } catch (RuntimeException e) { throw new CelEnvironmentException(e.getMessage(), e); @@ -406,6 +418,17 @@ private static CanonicalCelExtension getExtensionOrThrow(String extensionName) { return extension; } + /** Represents a context variable declaration. */ + @AutoValue + public abstract static class ContextVariable { + /** Fully qualified type name of the context variable. */ + public abstract String typeName(); + + public static ContextVariable create(String typeName) { + return new AutoValue_CelEnvironment_ContextVariable(typeName); + } + } + /** Represents a policy variable declaration. */ @AutoValue public abstract static class VariableDecl { diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java index f129d9f5d..14f1c93d8 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java @@ -28,6 +28,7 @@ import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CanIgnoreReturnValue; import dev.cel.bundle.CelEnvironment.Alias; +import dev.cel.bundle.CelEnvironment.ContextVariable; import dev.cel.bundle.CelEnvironment.ExtensionConfig; import dev.cel.bundle.CelEnvironment.FunctionDecl; import dev.cel.bundle.CelEnvironment.LibrarySubset; @@ -320,6 +321,36 @@ private ImmutableSet parseAbbreviations(ParserContext ctx, Node no return builder.build(); } + private ContextVariable parseContextVariable(ParserContext ctx, Node node) { + long id = ctx.collectMetadata(node); + if (!assertYamlType(ctx, id, node, YamlNodeType.MAP)) { + return ContextVariable.create(""); + } + + MappingNode mapNode = (MappingNode) node; + String typeName = ""; + for (NodeTuple nodeTuple : mapNode.getValue()) { + Node keyNode = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(keyNode); + Node valueNode = nodeTuple.getValueNode(); + String keyName = ((ScalarNode) keyNode).getValue(); + switch (keyName) { + case "type_name": + typeName = newString(ctx, valueNode); + break; + default: + ctx.reportError(keyId, String.format("Unsupported context_variable tag: %s", keyName)); + break; + } + } + + if (typeName.isEmpty()) { + ctx.reportError(id, "Missing required attribute(s): type_name"); + } + + return ContextVariable.create(typeName); + } + private ImmutableSet parseVariables(ParserContext ctx, Node node) { long valueId = ctx.collectMetadata(node); ImmutableSet.Builder variableSetBuilder = ImmutableSet.builder(); @@ -900,6 +931,9 @@ private CelEnvironment.Builder parseConfig(ParserContext ctx, Node node) { case "limits": builder.setLimits(parseLimits(ctx, valueNode)); break; + case "context_variable": + builder.setContextVariable(parseContextVariable(ctx, valueNode)); + break; default: ctx.reportError(id, "Unknown config tag: " + fieldName); // continue handling the rest of the nodes diff --git a/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel b/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel index d0fed9bea..677884a8a 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel +++ b/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel @@ -161,6 +161,7 @@ java_library( deps = [ ":cel_expression_source", ":default_result_matcher", + ":registry_utils", ":result_matcher", "//:auto_value", "//bundle:cel", diff --git a/testing/src/main/java/dev/cel/testing/testrunner/CelTestContext.java b/testing/src/main/java/dev/cel/testing/testrunner/CelTestContext.java index 1be0bab25..6ef988a44 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/CelTestContext.java +++ b/testing/src/main/java/dev/cel/testing/testrunner/CelTestContext.java @@ -140,21 +140,21 @@ public Optional celDescriptors() { return Optional.empty(); } + /** Returns a unified set of {@link CelDescriptors} combined from all descriptor sources. */ @Memoized - public Optional typeRegistry() { + public Optional mergedDescriptors() { if (fileTypes().isEmpty() && !fileDescriptorSetPath().isPresent()) { return Optional.empty(); } - TypeRegistry.Builder builder = TypeRegistry.newBuilder(); - if (!fileTypes().isEmpty()) { - builder.add( - CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(fileTypes()) - .messageTypeDescriptors()); - } - if (celDescriptors().isPresent()) { - builder.add(celDescriptors().get().messageTypeDescriptors()); - } - return Optional.of(builder.build()); + ImmutableSet.Builder allFiles = + ImmutableSet.builder().addAll(fileTypes()); + celDescriptors().ifPresent(d -> allFiles.addAll(d.fileDescriptors())); + return Optional.of(CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(allFiles.build())); + } + + @Memoized + public Optional typeRegistry() { + return mergedDescriptors().map(RegistryUtils::getTypeRegistry); } public abstract Optional extensionRegistry(); diff --git a/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java b/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java index 69c365972..1d3e49fbe 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java +++ b/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java @@ -360,18 +360,33 @@ private static Object getEvaluationResultWithMessage( } private static Message unpackAny(Any any, CelTestContext celTestContext) throws IOException { - if (!celTestContext.fileDescriptorSetPath().isPresent()) { - throw new IllegalArgumentException( - "Proto descriptors are required for unpacking Any messages."); + TypeRegistry typeRegistry = + celTestContext + .typeRegistry() + .orElseThrow( + () -> + new IllegalArgumentException( + "Proto descriptors or type registry are required for unpacking Any" + + " messages.")); + + Descriptor descriptor = typeRegistry.getDescriptorForTypeUrl(any.getTypeUrl()); + if (descriptor == null) { + throw new IllegalArgumentException("Descriptor not found for type URL: " + any.getTypeUrl()); } - Descriptor descriptor = - RegistryUtils.getTypeRegistry(celTestContext.celDescriptors().get()) - .getDescriptorForTypeUrl(any.getTypeUrl()); + + ExtensionRegistry extensionRegistry = + celTestContext + .extensionRegistry() + .orElseGet( + () -> + celTestContext + .mergedDescriptors() + .map(RegistryUtils::getExtensionRegistry) + .orElseGet(ExtensionRegistry::getEmptyRegistry)); + return DynamicMessage.getDefaultInstance(descriptor) .getParserForType() - .parseFrom( - any.getValue(), - RegistryUtils.getExtensionRegistry(celTestContext.celDescriptors().get())); + .parseFrom(any.getValue(), extensionRegistry); } private static Message getEvaluatedContextExpr( diff --git a/testing/src/test/java/dev/cel/testing/testrunner/TestRunnerLibraryTest.java b/testing/src/test/java/dev/cel/testing/testrunner/TestRunnerLibraryTest.java index b83375b35..112ef1f82 100644 --- a/testing/src/test/java/dev/cel/testing/testrunner/TestRunnerLibraryTest.java +++ b/testing/src/test/java/dev/cel/testing/testrunner/TestRunnerLibraryTest.java @@ -262,7 +262,7 @@ public void runTest_missingProtoDescriptors_failure() throws Exception { assertThat(thrown) .hasMessageThat() - .contains("Proto descriptors are required for unpacking Any messages."); + .contains("Proto descriptors or type registry are required for unpacking Any messages"); } @Test From f89d7e5100ed62b94640697e87245cee5ff6c703 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 6 May 2026 12:44:24 -0700 Subject: [PATCH 069/204] Internal Changes PiperOrigin-RevId: 911493152 --- .../dev/cel/conformance/policy/BUILD.bazel | 2 + .../policy/PolicyConformanceTest.java | 9 ++ .../main/java/dev/cel/policy/CelPolicy.java | 2 +- .../java/dev/cel/policy/testing/BUILD.bazel | 27 ++++ .../dev/cel/policy/testing/K8sTagHandler.java | 117 ++++++++++++++++++ .../src/test/java/dev/cel/policy/BUILD.bazel | 2 +- .../cel/policy/CelPolicyCompilerImplTest.java | 2 +- .../cel/policy/CelPolicyYamlParserTest.java | 2 +- .../java/dev/cel/policy/PolicyTestHelper.java | 108 ---------------- policy/testing/BUILD.bazel | 12 ++ 10 files changed, 171 insertions(+), 112 deletions(-) create mode 100644 policy/src/main/java/dev/cel/policy/testing/BUILD.bazel create mode 100644 policy/src/main/java/dev/cel/policy/testing/K8sTagHandler.java create mode 100644 policy/testing/BUILD.bazel diff --git a/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel index ca34530fb..27853f29b 100644 --- a/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel @@ -12,6 +12,8 @@ java_library( deps = [ "//:auto_value", "//bundle:cel", + "//policy:parser_factory", + "//policy/testing:k8s_test_tag_handler", "//runtime:function_binding", "//testing/testrunner:cel_expression_source", "//testing/testrunner:cel_test_context", diff --git a/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java index cd24339c0..4f1c643c2 100644 --- a/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java +++ b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java @@ -18,6 +18,8 @@ import dev.cel.bundle.Cel; import dev.cel.bundle.CelFactory; import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.policy.CelPolicyParserFactory; +import dev.cel.policy.testing.K8sTagHandler; import dev.cel.runtime.CelFunctionBinding; import dev.cel.testing.testrunner.CelExpressionSource; import dev.cel.testing.testrunner.CelTestContext; @@ -77,6 +79,13 @@ public void evaluate() throws Throwable { TestAllTypes.getDescriptor().getFile(), Struct.getDescriptor().getFile()); + // Scopes the custom Kubernetes tag visitor exclusively to k8s tests to prevent non-standard + // grammar leakage. + if (name.startsWith("k8s/")) { + contextBuilder.setCelPolicyParser( + CelPolicyParserFactory.newYamlParserBuilder().addTagVisitor(new K8sTagHandler()).build()); + } + Path yamlConfigPath = Paths.get(dirPath, "config.yaml"); Path textprotoConfigPath = Paths.get(dirPath, "config.textproto"); diff --git a/policy/src/main/java/dev/cel/policy/CelPolicy.java b/policy/src/main/java/dev/cel/policy/CelPolicy.java index 9e442a2e7..19f6631d0 100644 --- a/policy/src/main/java/dev/cel/policy/CelPolicy.java +++ b/policy/src/main/java/dev/cel/policy/CelPolicy.java @@ -252,7 +252,7 @@ public abstract static class Builder implements RequiredFieldsChecker { abstract Optional id(); - abstract Optional result(); + public abstract Optional result(); abstract Optional explanation(); diff --git a/policy/src/main/java/dev/cel/policy/testing/BUILD.bazel b/policy/src/main/java/dev/cel/policy/testing/BUILD.bazel new file mode 100644 index 000000000..3a8a4950b --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/testing/BUILD.bazel @@ -0,0 +1,27 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = [ + "//:license", + ], + default_testonly = True, + default_visibility = [ + "//policy/testing:__pkg__", + ], +) + +java_library( + name = "k8s_tag_handler", + srcs = ["K8sTagHandler.java"], + tags = [ + ], + deps = [ + "//common/formats:value_string", + "//common/formats:yaml_helper", + "//policy", + "//policy:parser", + "//policy:policy_parser_context", + "@maven//:com_google_guava_guava", + "@maven//:org_yaml_snakeyaml", + ], +) diff --git a/policy/src/main/java/dev/cel/policy/testing/K8sTagHandler.java b/policy/src/main/java/dev/cel/policy/testing/K8sTagHandler.java new file mode 100644 index 000000000..04635e054 --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/testing/K8sTagHandler.java @@ -0,0 +1,117 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.policy.testing; + +import com.google.common.annotations.VisibleForTesting; +import dev.cel.common.formats.ValueString; +import dev.cel.common.formats.YamlHelper; +import dev.cel.common.formats.YamlHelper.YamlNodeType; +import dev.cel.policy.CelPolicy; +import dev.cel.policy.CelPolicy.Match; +import dev.cel.policy.CelPolicyParser.TagVisitor; +import dev.cel.policy.PolicyParserContext; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.SequenceNode; + +/** + * K8sTagHandler is a {@link TagVisitor} implementation to support parsing Kubernetes + * ValidatingAdmissionPolicy structures in testing and conformance environments. + */ +@VisibleForTesting +public final class K8sTagHandler implements TagVisitor { + + @Override + public void visitPolicyTag( + PolicyParserContext ctx, + long id, + String tagName, + Node node, + CelPolicy.Builder policyBuilder) { + switch (tagName) { + case "kind": + policyBuilder.putMetadata("kind", ctx.newYamlString(node).value()); + break; + case "metadata": + YamlHelper.assertYamlType(ctx, id, node, YamlNodeType.MAP); + break; + case "spec": + CelPolicy.Rule spec = ctx.parseRule(ctx, policyBuilder, node); + policyBuilder.setRule(spec); + break; + default: + TagVisitor.super.visitPolicyTag(ctx, id, tagName, node, policyBuilder); + break; + } + } + + @Override + public void visitRuleTag( + PolicyParserContext ctx, + long id, + String tagName, + Node node, + CelPolicy.Builder policyBuilder, + CelPolicy.Rule.Builder ruleBuilder) { + switch (tagName) { + case "failurePolicy": + policyBuilder.putMetadata(tagName, ctx.newYamlString(node).value()); + break; + case "matchConstraints": + YamlHelper.assertYamlType(ctx, id, node, YamlNodeType.MAP); + break; + case "validations": + if (!YamlHelper.assertYamlType(ctx, id, node, YamlNodeType.LIST)) { + return; + } + SequenceNode seqNode = (SequenceNode) node; + for (Node valNode : seqNode.getValue()) { + ruleBuilder.addMatches(ctx.parseMatch(ctx, policyBuilder, valNode)); + } + break; + default: + TagVisitor.super.visitRuleTag(ctx, id, tagName, node, policyBuilder, ruleBuilder); + break; + } + } + + @Override + public void visitMatchTag( + PolicyParserContext ctx, + long id, + String tagName, + Node node, + CelPolicy.Builder policyBuilder, + CelPolicy.Match.Builder matchBuilder) { + if (!matchBuilder.result().isPresent()) { + matchBuilder.setResult( + Match.Result.ofOutput(ValueString.of(ctx.nextId(), "'invalid admission request'"))); + } + switch (tagName) { + case "expression": + // The K8s expression to validate must return false in order to generate a violation + // message. + ValueString condition = ctx.newSourceString(node); + String invertedCondition = "!(" + condition.value() + ")"; + matchBuilder.setCondition(ValueString.of(condition.id(), invertedCondition)); + break; + case "messageExpression": + matchBuilder.setResult(Match.Result.ofOutput(ctx.newSourceString(node))); + break; + default: + TagVisitor.super.visitMatchTag(ctx, id, tagName, node, policyBuilder, matchBuilder); + break; + } + } +} diff --git a/policy/src/test/java/dev/cel/policy/BUILD.bazel b/policy/src/test/java/dev/cel/policy/BUILD.bazel index 8a28caee1..3089a3849 100644 --- a/policy/src/test/java/dev/cel/policy/BUILD.bazel +++ b/policy/src/test/java/dev/cel/policy/BUILD.bazel @@ -30,9 +30,9 @@ java_library( "//policy:compiler_factory", "//policy:parser", "//policy:parser_factory", - "//policy:policy_parser_context", "//policy:source", "//policy:validation_exception", + "//policy/testing:k8s_test_tag_handler", "//runtime", "//runtime:function_binding", "//testing:cel_runtime_flavor", diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java index 35e249407..416e3b95f 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java @@ -37,12 +37,12 @@ import dev.cel.extensions.CelOptionalLibrary; import dev.cel.parser.CelStandardMacro; import dev.cel.parser.CelUnparserFactory; -import dev.cel.policy.PolicyTestHelper.K8sTagHandler; import dev.cel.policy.PolicyTestHelper.PolicyTestSuite; import dev.cel.policy.PolicyTestHelper.PolicyTestSuite.PolicyTestSection; import dev.cel.policy.PolicyTestHelper.PolicyTestSuite.PolicyTestSection.PolicyTestCase; import dev.cel.policy.PolicyTestHelper.PolicyTestSuite.PolicyTestSection.PolicyTestCase.PolicyTestInput; import dev.cel.policy.PolicyTestHelper.TestYamlPolicy; +import dev.cel.policy.testing.K8sTagHandler; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelLateFunctionBindings; import dev.cel.testing.CelRuntimeFlavor; diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java index 22aec6746..2a2c47a98 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java @@ -22,8 +22,8 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.common.formats.ValueString; import dev.cel.policy.CelPolicy.Import; -import dev.cel.policy.PolicyTestHelper.K8sTagHandler; import dev.cel.policy.PolicyTestHelper.TestYamlPolicy; +import dev.cel.policy.testing.K8sTagHandler; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java b/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java index 59647f4d9..6e918286b 100644 --- a/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java +++ b/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java @@ -19,11 +19,6 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Ascii; import com.google.common.io.Resources; -import dev.cel.common.formats.ValueString; -import dev.cel.policy.CelPolicy.Match; -import dev.cel.policy.CelPolicy.Match.Result; -import dev.cel.policy.CelPolicy.Rule; -import dev.cel.policy.CelPolicyParser.TagVisitor; import java.io.IOException; import java.net.URL; import java.util.List; @@ -31,8 +26,6 @@ import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.Constructor; -import org.yaml.snakeyaml.nodes.Node; -import org.yaml.snakeyaml.nodes.SequenceNode; /** Package-private class to assist with policy testing. */ final class PolicyTestHelper { @@ -273,106 +266,5 @@ private static String readFile(String path) throws IOException { return Resources.toString(getResource(path), UTF_8); } - static class K8sTagHandler implements TagVisitor { - - @Override - public void visitPolicyTag( - PolicyParserContext ctx, - long id, - String tagName, - Node node, - CelPolicy.Builder policyBuilder) { - switch (tagName) { - case "kind": - policyBuilder.putMetadata("kind", ctx.newYamlString(node)); - break; - case "metadata": - long metadataId = ctx.collectMetadata(node); - if (!node.getTag().getValue().equals("tag:yaml.org,2002:map")) { - ctx.reportError( - metadataId, - String.format( - "invalid 'metadata' type, expected map got: %s", node.getTag().getValue())); - } - break; - case "spec": - Rule rule = ctx.parseRule(ctx, policyBuilder, node); - policyBuilder.setRule(rule); - break; - default: - TagVisitor.super.visitPolicyTag(ctx, id, tagName, node, policyBuilder); - break; - } - } - - @Override - public void visitRuleTag( - PolicyParserContext ctx, - long id, - String tagName, - Node node, - CelPolicy.Builder policyBuilder, - Rule.Builder ruleBuilder) { - switch (tagName) { - case "failurePolicy": - policyBuilder.putMetadata(tagName, ctx.newYamlString(node)); - break; - case "matchConstraints": - long matchConstraintsId = ctx.collectMetadata(node); - if (!node.getTag().getValue().equals("tag:yaml.org,2002:map")) { - ctx.reportError( - matchConstraintsId, - String.format( - "invalid 'matchConstraints' type, expected map got: %s", - node.getTag().getValue())); - } - break; - case "validations": - long validationId = ctx.collectMetadata(node); - if (!node.getTag().getValue().equals("tag:yaml.org,2002:seq")) { - ctx.reportError( - validationId, - String.format( - "invalid 'validations' type, expected list got: %s", node.getTag().getValue())); - } - - SequenceNode validationNodes = (SequenceNode) node; - for (Node element : validationNodes.getValue()) { - ruleBuilder.addMatches(ctx.parseMatch(ctx, policyBuilder, element)); - } - break; - default: - TagVisitor.super.visitRuleTag(ctx, id, tagName, node, policyBuilder, ruleBuilder); - break; - } - } - - @Override - public void visitMatchTag( - PolicyParserContext ctx, - long id, - String tagName, - Node node, - CelPolicy.Builder policyBuilder, - Match.Builder matchBuilder) { - switch (tagName) { - case "expression": - // The K8s expression to validate must return false in order to generate a violation - // message. - ValueString conditionValue = ctx.newYamlString(node); - conditionValue = - conditionValue.toBuilder().setValue("!(" + conditionValue.value() + ")").build(); - matchBuilder.setCondition(conditionValue); - break; - case "messageExpression": - matchBuilder.setResult(Result.ofOutput(ctx.newYamlString(node))); - break; - default: - TagVisitor.super.visitMatchTag(ctx, id, tagName, node, policyBuilder, matchBuilder); - break; - } - } - } - private PolicyTestHelper() {} } diff --git a/policy/testing/BUILD.bazel b/policy/testing/BUILD.bazel new file mode 100644 index 000000000..898368c3c --- /dev/null +++ b/policy/testing/BUILD.bazel @@ -0,0 +1,12 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_testonly = True, + default_visibility = ["//:internal"], +) + +java_library( + name = "k8s_test_tag_handler", + exports = ["//policy/src/main/java/dev/cel/policy/testing:k8s_tag_handler"], +) From 966b0cf4238191c91c291aa0a9b60571231f2c07 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 6 May 2026 15:47:37 -0700 Subject: [PATCH 070/204] Add support for running policy error cases in conformance tests PiperOrigin-RevId: 911591805 --- .../dev/cel/conformance/policy/BUILD.bazel | 2 + .../policy/PolicyConformanceTest.java | 16 +++++++- .../policy/PolicyConformanceTestRunner.java | 37 +++++++++++++++++-- .../java/dev/cel/policy/RuleComposer.java | 4 +- .../expected_errors.baseline | 4 +- .../expected_errors.baseline | 2 +- 6 files changed, 55 insertions(+), 10 deletions(-) diff --git a/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel index 27853f29b..0326b6f15 100644 --- a/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel @@ -11,8 +11,10 @@ java_library( srcs = glob(["*.java"]), deps = [ "//:auto_value", + "//:java_truth", "//bundle:cel", "//policy:parser_factory", + "//policy:validation_exception", "//policy/testing:k8s_test_tag_handler", "//runtime:function_binding", "//testing/testrunner:cel_expression_source", diff --git a/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java index 4f1c643c2..d7851bb72 100644 --- a/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java +++ b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java @@ -14,11 +14,14 @@ package dev.cel.conformance.policy; +import static com.google.common.truth.Truth.assertThat; + import com.google.protobuf.Struct; import dev.cel.bundle.Cel; import dev.cel.bundle.CelFactory; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.policy.CelPolicyParserFactory; +import dev.cel.policy.CelPolicyValidationException; import dev.cel.policy.testing.K8sTagHandler; import dev.cel.runtime.CelFunctionBinding; import dev.cel.testing.testrunner.CelExpressionSource; @@ -28,6 +31,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Locale; import org.junit.runners.model.Statement; /** Statement representing a single CEL policy conformance test case. */ @@ -95,6 +99,16 @@ public void evaluate() throws Throwable { contextBuilder.setConfigFile(textprotoConfigPath.toString()); } - TestRunnerLibrary.runTest(testCase, contextBuilder.build()); + try { + TestRunnerLibrary.runTest(testCase, contextBuilder.build()); + } catch (CelPolicyValidationException e) { + if (testCase.output().kind() == CelTestCase.Output.Kind.EVAL_ERROR) { + String expectedError = testCase.output().evalError().get(0).toString(); + assertThat(e.getMessage().toLowerCase(Locale.US)) + .contains(expectedError.toLowerCase(Locale.US)); + } else { + throw e; + } + } } } diff --git a/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTestRunner.java b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTestRunner.java index 6d1d86e47..62812b124 100644 --- a/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTestRunner.java +++ b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTestRunner.java @@ -44,6 +44,7 @@ public final class PolicyConformanceTestRunner extends ParentRunner discoverTestDirs(String testdataDir) { if (!dir.exists() || !dir.isDirectory()) { return ImmutableList.of(); } - String[] directories = dir.list((current, name) -> new File(current, name).isDirectory()); - if (directories == null) { + File[] topLevelDirs = dir.listFiles(File::isDirectory); + if (topLevelDirs == null) { return ImmutableList.of(); } - Arrays.sort(directories); - return ImmutableList.copyOf(directories); + + ImmutableList.Builder testDirsBuilder = ImmutableList.builder(); + Arrays.sort(topLevelDirs); + for (File topLevelDir : topLevelDirs) { + if (hasTestSuite(topLevelDir)) { + testDirsBuilder.add(topLevelDir.getName()); + continue; + } + + // Check one level deeper to support nested tests like compile_errors/unreachable + File[] subDirs = topLevelDir.listFiles(File::isDirectory); + if (subDirs == null) { + continue; + } + + Arrays.sort(subDirs); + for (File subDir : subDirs) { + if (hasTestSuite(subDir)) { + testDirsBuilder.add(topLevelDir.getName() + "/" + subDir.getName()); + } + } + } + + return testDirsBuilder.build(); + } + + private static boolean hasTestSuite(File dir) { + return (new File(dir, TESTS_YAML_FILE_NAME).exists() + || new File(dir, TESTS_TEXTPROTO_FILE_NAME).exists()) + && new File(dir, POLICY_YAML_FILE_NAME).exists(); } private final ImmutableList tests; diff --git a/policy/src/main/java/dev/cel/policy/RuleComposer.java b/policy/src/main/java/dev/cel/policy/RuleComposer.java index cafef4b7c..5fa0957f5 100644 --- a/policy/src/main/java/dev/cel/policy/RuleComposer.java +++ b/policy/src/main/java/dev/cel/policy/RuleComposer.java @@ -93,7 +93,7 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) { assertComposedAstIsValid( cel, output.expr, - "conflicting output types found.", + "incompatible output types found.", matchOutput.sourceId(), lastOutputId); lastOutputId = matchOutput.sourceId(); @@ -115,7 +115,7 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) { cel, output.expr, String.format( - "failed composing the subrule '%s' due to conflicting output types.", + "failed composing the subrule '%s' due to incompatible output types.", matchNestedRule.ruleId().map(ValueString::value).orElse("")), lastOutputId); break; diff --git a/testing/src/test/resources/policy/compose_errors_conflicting_output/expected_errors.baseline b/testing/src/test/resources/policy/compose_errors_conflicting_output/expected_errors.baseline index 3e2624b64..0facbbe2e 100644 --- a/testing/src/test/resources/policy/compose_errors_conflicting_output/expected_errors.baseline +++ b/testing/src/test/resources/policy/compose_errors_conflicting_output/expected_errors.baseline @@ -1,6 +1,6 @@ -ERROR: compose_errors_conflicting_output/policy.yaml:22:14: conflicting output types found. +ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types found. | output: "false" | .............^ -ERROR: compose_errors_conflicting_output/policy.yaml:23:14: conflicting output types found. +ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types found. | - output: "{'banned': true}" | .............^ \ No newline at end of file diff --git a/testing/src/test/resources/policy/compose_errors_conflicting_subrule/expected_errors.baseline b/testing/src/test/resources/policy/compose_errors_conflicting_subrule/expected_errors.baseline index 559d62e1d..92ddff311 100644 --- a/testing/src/test/resources/policy/compose_errors_conflicting_subrule/expected_errors.baseline +++ b/testing/src/test/resources/policy/compose_errors_conflicting_subrule/expected_errors.baseline @@ -1,3 +1,3 @@ -ERROR: compose_errors_conflicting_subrule/policy.yaml:36:14: failed composing the subrule 'banned regions' due to conflicting output types. +ERROR: compose_errors_conflicting_subrule/policy.yaml:36:14: failed composing the subrule 'banned regions' due to incompatible output types. | output: "{'banned': false}" | .............^ \ No newline at end of file From 14d4c2e39151f2e99e36f9818a9118b01c1d9ed3 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 6 May 2026 17:16:33 -0700 Subject: [PATCH 071/204] Track incompatible output types for accurate error reporting PiperOrigin-RevId: 911634269 --- policy/BUILD.bazel | 6 ++ .../src/main/java/dev/cel/policy/BUILD.bazel | 3 +- .../java/dev/cel/policy/RuleComposer.java | 88 ++++++++++++------- .../src/test/java/dev/cel/policy/BUILD.bazel | 2 + .../cel/policy/CelPolicyCompilerImplTest.java | 19 ++++ .../expected_errors.baseline | 4 +- .../expected_errors.baseline | 3 + 7 files changed, 92 insertions(+), 33 deletions(-) diff --git a/policy/BUILD.bazel b/policy/BUILD.bazel index 5979f1ba7..bce68f001 100644 --- a/policy/BUILD.bazel +++ b/policy/BUILD.bazel @@ -59,3 +59,9 @@ java_library( name = "compiler_builder", exports = ["//policy/src/main/java/dev/cel/policy:compiler_builder"], ) + +java_library( + name = "rule_composer", + visibility = ["//:internal"], + exports = ["//policy/src/main/java/dev/cel/policy:rule_composer"], +) diff --git a/policy/src/main/java/dev/cel/policy/BUILD.bazel b/policy/src/main/java/dev/cel/policy/BUILD.bazel index a7bb90ffe..e0d6af461 100644 --- a/policy/src/main/java/dev/cel/policy/BUILD.bazel +++ b/policy/src/main/java/dev/cel/policy/BUILD.bazel @@ -244,7 +244,6 @@ java_library( java_library( name = "rule_composer", srcs = ["RuleComposer.java"], - visibility = ["//visibility:private"], deps = [ ":compiled_rule", "//bundle:cel", @@ -257,6 +256,8 @@ java_library( "//common/ast:mutable_expr", "//common/formats:value_string", "//common/navigation:mutable_navigation", + "//common/types:cel_types", + "//common/types:type_providers", "//extensions:optional_library", "//optimizer:ast_optimizer", "//optimizer:mutable_ast", diff --git a/policy/src/main/java/dev/cel/policy/RuleComposer.java b/policy/src/main/java/dev/cel/policy/RuleComposer.java index 5fa0957f5..73d31a4ee 100644 --- a/policy/src/main/java/dev/cel/policy/RuleComposer.java +++ b/policy/src/main/java/dev/cel/policy/RuleComposer.java @@ -18,6 +18,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static java.util.stream.Collectors.toCollection; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import dev.cel.bundle.Cel; @@ -32,11 +33,14 @@ import dev.cel.common.formats.ValueString; import dev.cel.common.navigation.CelNavigableMutableAst; import dev.cel.common.navigation.CelNavigableMutableExpr; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypes; import dev.cel.extensions.CelOptionalLibrary.Function; import dev.cel.optimizer.AstMutator; import dev.cel.optimizer.CelAstOptimizer; import dev.cel.policy.CelCompiledRule.CelCompiledMatch; import dev.cel.policy.CelCompiledRule.CelCompiledMatch.OutputValue; +import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result; import dev.cel.policy.CelCompiledRule.CelCompiledVariable; import java.util.ArrayList; import java.util.Arrays; @@ -74,11 +78,15 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) { } long lastOutputId = 0; + // The expected output type of the rule, used to verify that all branches agree on the type. + CelType lastOutputType = null; for (CelCompiledMatch match : Lists.reverse(compiledRule.matches())) { CelAbstractSyntaxTree conditionAst = match.condition(); boolean isTriviallyTrue = match.isConditionTriviallyTrue(); CelMutableAst condAst = CelMutableAst.fromCelAst(conditionAst); + long currentSourceId = lastOutputId; + switch (match.result().kind()) { case OUTPUT: // If the match has an output, then it is considered a non-optional output since @@ -86,42 +94,54 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) { // of output being optional.none() will convert the non-optional value to an optional // one. OutputValue matchOutput = match.result().output(); - CelMutableAst outAst = CelMutableAst.fromCelAst(matchOutput.ast()); - Step step = Step.newNonOptionalStep(!isTriviallyTrue, condAst, outAst); + Step step = + Step.newNonOptionalStep( + !isTriviallyTrue, condAst, CelMutableAst.fromCelAst(matchOutput.ast())); + currentSourceId = matchOutput.sourceId(); + output = combine(astMutator, step, output); - assertComposedAstIsValid( - cel, - output.expr, - "incompatible output types found.", - matchOutput.sourceId(), - lastOutputId); - lastOutputId = matchOutput.sourceId(); + String outputFailureMessage = + String.format( + "incompatible output types: block has output type %s, but previous outputs have" + + " type %s", + lastOutputType == null ? "" : CelTypes.format(lastOutputType), + CelTypes.format(matchOutput.ast().getResultType())); + lastOutputType = + assertComposedAstIsValid( + cel, output.expr, outputFailureMessage, currentSourceId, lastOutputId) + .getResultType(); + break; case RULE: // If the match has a nested rule, then compute the rule and whether it has // an optional return value. CelCompiledRule matchNestedRule = match.result().rule(); Step nestedRule = optimizeRule(cel, matchNestedRule); - boolean nestedHasOptional = matchNestedRule.hasOptionalOutput(); - Step ruleStep = - nestedHasOptional - ? Step.newOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr) - : Step.newNonOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr); + new Step( + matchNestedRule.hasOptionalOutput(), !isTriviallyTrue, condAst, nestedRule.expr); + currentSourceId = getFirstOutputSourceId(matchNestedRule); + output = combine(astMutator, ruleStep, output); - assertComposedAstIsValid( - cel, - output.expr, - String.format( - "failed composing the subrule '%s' due to incompatible output types.", - matchNestedRule.ruleId().map(ValueString::value).orElse("")), - lastOutputId); + lastOutputType = + assertComposedAstIsValid( + cel, + output.expr, + String.format( + "failed composing the subrule '%s' due to incompatible output types.", + matchNestedRule.ruleId().map(ValueString::value).orElse("")), + currentSourceId, + lastOutputId) + .getResultType(); break; } + + lastOutputId = currentSourceId; } + Preconditions.checkState(output != null, "Policy contains no outputs."); CelMutableAst resultExpr = output.expr; resultExpr = inlineCompiledVariables(resultExpr, compiledRule.variables()); resultExpr = astMutator.renumberIdsConsecutively(resultExpr); @@ -266,21 +286,34 @@ private CelMutableAst inlineCompiledVariables( return mutatedAst; } - private void assertComposedAstIsValid( + private CelAbstractSyntaxTree assertComposedAstIsValid( Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) { - assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids)); + return assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids)); } - private void assertComposedAstIsValid( + private CelAbstractSyntaxTree assertComposedAstIsValid( Cel cel, CelMutableAst composedAst, String failureMessage, List ids) { try { - cel.check(composedAst.toParsedAst()).getAst(); + return cel.check(composedAst.toParsedAst()).getAst(); } catch (CelValidationException e) { ids = ids.stream().filter(id -> id > 0).collect(toCollection(ArrayList::new)); throw new RuleCompositionException(failureMessage, e, ids); } } + private static long getFirstOutputSourceId(CelCompiledRule rule) { + for (CelCompiledMatch match : rule.matches()) { + if (match.result().kind() == Result.Kind.OUTPUT) { + return match.result().output().sourceId(); + } else if (match.result().kind() == Result.Kind.RULE) { + return getFirstOutputSourceId(match.result().rule()); + } + } + + // Fallback to the nested rule ID if the policy is invalid and contains no output + return rule.sourceId(); + } + // Step represents an intermediate stage of rule and match expression composition. // // The CelCompiledRule and CelCompiledMatch types are meant to represent standalone tuples of @@ -311,11 +344,6 @@ private Step( this.expr = expr; } - private static Step newOptionalStep( - boolean isConditional, CelMutableAst cond, CelMutableAst expr) { - return new Step(/* isOptional= */ true, isConditional, cond, expr); - } - private static Step newNonOptionalStep( boolean isConditional, CelMutableAst cond, CelMutableAst expr) { return new Step(/* isOptional= */ false, isConditional, cond, expr); diff --git a/policy/src/test/java/dev/cel/policy/BUILD.bazel b/policy/src/test/java/dev/cel/policy/BUILD.bazel index 3089a3849..bc8a5d4b4 100644 --- a/policy/src/test/java/dev/cel/policy/BUILD.bazel +++ b/policy/src/test/java/dev/cel/policy/BUILD.bazel @@ -27,9 +27,11 @@ java_library( "//parser:parser_factory", "//parser:unparser", "//policy", + "//policy:compiled_rule", "//policy:compiler_factory", "//policy:parser", "//policy:parser_factory", + "//policy:rule_composer", "//policy:source", "//policy:validation_exception", "//policy/testing:k8s_test_tag_handler", diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java index 416e3b95f..b4065b60c 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java @@ -31,6 +31,7 @@ import dev.cel.bundle.CelEnvironmentYamlParser; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelOptions; +import dev.cel.common.formats.ValueString; import dev.cel.common.types.OptionalType; import dev.cel.common.types.SimpleType; import dev.cel.expr.conformance.proto3.TestAllTypes; @@ -356,6 +357,24 @@ public void evaluateYamlPolicy_withSimpleVariable() throws Exception { assertThat(evalResult).isFalse(); } + @Test + public void compose_ruleWithNoOutputs_throws() throws Exception { + Cel cel = newCel(); + CelCompiledRule emptyRule = + CelCompiledRule.create( + 1L, + Optional.of(ValueString.of(2L, "empty_rule")), + ImmutableList.of(), + ImmutableList.of(), + cel); + RuleComposer composer = RuleComposer.newInstance(emptyRule, "variables.", 1000); + CelAbstractSyntaxTree ast = cel.compile("true").getAst(); + + IllegalStateException e = + assertThrows(IllegalStateException.class, () -> composer.optimize(ast, cel)); + assertThat(e).hasMessageThat().isEqualTo("Policy contains no outputs."); + } + private static final class EvaluablePolicyTestData { private final TestYamlPolicy yamlPolicy; private final PolicyTestCase testCase; diff --git a/testing/src/test/resources/policy/compose_errors_conflicting_output/expected_errors.baseline b/testing/src/test/resources/policy/compose_errors_conflicting_output/expected_errors.baseline index 0facbbe2e..bc205c2ab 100644 --- a/testing/src/test/resources/policy/compose_errors_conflicting_output/expected_errors.baseline +++ b/testing/src/test/resources/policy/compose_errors_conflicting_output/expected_errors.baseline @@ -1,6 +1,6 @@ -ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types found. +ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool | output: "false" | .............^ -ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types found. +ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool | - output: "{'banned': true}" | .............^ \ No newline at end of file diff --git a/testing/src/test/resources/policy/compose_errors_conflicting_subrule/expected_errors.baseline b/testing/src/test/resources/policy/compose_errors_conflicting_subrule/expected_errors.baseline index 92ddff311..66e48ea57 100644 --- a/testing/src/test/resources/policy/compose_errors_conflicting_subrule/expected_errors.baseline +++ b/testing/src/test/resources/policy/compose_errors_conflicting_subrule/expected_errors.baseline @@ -1,3 +1,6 @@ +ERROR: compose_errors_conflicting_subrule/policy.yaml:34:18: failed composing the subrule 'banned regions' due to incompatible output types. + | output: "true" + | .................^ ERROR: compose_errors_conflicting_subrule/policy.yaml:36:14: failed composing the subrule 'banned regions' due to incompatible output types. | output: "{'banned': false}" | .............^ \ No newline at end of file From bdfe82340650d66e5fc8446780bb539a0c31e144 Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Thu, 14 May 2026 11:54:11 -0700 Subject: [PATCH 072/204] Fix math.round to use HALF_UP to match doc, cel-go/cel-cpp --- .../main/java/dev/cel/extensions/CelMathExtensions.java | 2 +- .../java/dev/cel/extensions/CelMathExtensionsTest.java | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java index 78a0fd51c..63108aa0c 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java @@ -874,7 +874,7 @@ private static double round(double x) { if (isNaN(x) || isInfinite(x)) { return x; } - return DoubleMath.roundToLong(x, RoundingMode.HALF_EVEN); + return DoubleMath.roundToLong(x, RoundingMode.HALF_UP); } private static Number sign(Number x) { diff --git a/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java index 16d5c4c83..68c80dedb 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java @@ -735,6 +735,13 @@ public void floor_invalidArgs_throwsException(String expr) { @TestParameters("{expr: 'math.round(-1.5)' , expectedResult: -2.0}") @TestParameters("{expr: 'math.round(-1.2)' , expectedResult: -1.0}") @TestParameters("{expr: 'math.round(-1.6)' , expectedResult: -2.0}") + // Discriminating tie cases: confirm "ties round away from zero" (HALF_UP), not + // banker's rounding (HALF_EVEN). 1.5/-1.5 above don't distingish the two because + // their nearest-even neighbor (2/-2) is also the away-from-zero neighbor. + @TestParameters("{expr: 'math.round(0.5)' , expectedResult: 1.0}") + @TestParameters("{expr: 'math.round(2.5)' , expectedResult: 3.0}") + @TestParameters("{expr: 'math.round(-0.5)' , expectedResult: -1.0}") + @TestParameters("{expr: 'math.round(-2.5)' , expectedResult: -3.0}") @TestParameters("{expr: 'math.round(0.0/0.0)' , expectedResult: NaN}") @TestParameters("{expr: 'math.round(1.0/0.0)' , expectedResult: Infinity}") @TestParameters("{expr: 'math.round(-1.0/0.0)' , expectedResult: -Infinity}") From 9988a347afbe817614c7817e92f23d52c3791872 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 14 May 2026 00:48:33 +0000 Subject: [PATCH 073/204] Enable policy conformance test suite in OSS PiperOrigin-RevId: 915149734 --- MODULE.bazel | 1 + .../test/java/dev/cel/conformance/policy/BUILD.bazel | 5 +++++ .../policy/cel_policy_conformance_test.bzl | 6 +++++- repositories.bzl | 11 +++++++++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/MODULE.bazel b/MODULE.bazel index 0b67c825c..895715a5f 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -137,3 +137,4 @@ use_repo(maven, "maven", "maven_android", "maven_conformance") non_module_dependencies = use_extension("//:repositories.bzl", "non_module_dependencies") use_repo(non_module_dependencies, "antlr4_jar") use_repo(non_module_dependencies, "bazel_common") +use_repo(non_module_dependencies, "cel_policy") diff --git a/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel index 0326b6f15..e4d80eccf 100644 --- a/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel @@ -29,3 +29,8 @@ java_library( "@maven//:junit_junit", ], ) + +cel_policy_conformance_test_java( + name = "policy_conformance_tests", + testdata = "@cel_policy//conformance:testdata", +) diff --git a/conformance/src/test/java/dev/cel/conformance/policy/cel_policy_conformance_test.bzl b/conformance/src/test/java/dev/cel/conformance/policy/cel_policy_conformance_test.bzl index 3e3720ec5..b53d982bb 100644 --- a/conformance/src/test/java/dev/cel/conformance/policy/cel_policy_conformance_test.bzl +++ b/conformance/src/test/java/dev/cel/conformance/policy/cel_policy_conformance_test.bzl @@ -33,7 +33,11 @@ def cel_policy_conformance_test_java( """ lbl = native.package_relative_label(testdata) - testdata_dir = lbl.package + "/" + lbl.name + + # Under Bzlmod, external repository runfiles are located in sibling directories + # named after their canonical repository name. + repo_prefix = "../" + lbl.workspace_name + "/" if lbl.workspace_name else "" + testdata_dir = repo_prefix + lbl.package + "/" + lbl.name java_test( name = name, diff --git a/repositories.bzl b/repositories.bzl index 8e9a9ba47..88f01019a 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -33,9 +33,20 @@ def bazel_common_dependency(): url = "https://github.com/google/bazel-common/archive/%s.tar.gz" % bazel_common_tag, ) +def cel_policy_dependency(): + cel_policy_tag = "569292f1c4eaa41894c1e37ee94eb146e284bcfa" + cel_policy_sha = "5a68318d906f6ce18492ad6f82b5f8bb083fd9d694cf567d399216c11da03157" + http_archive( + name = "cel_policy", + sha256 = cel_policy_sha, + strip_prefix = "cel-policy-%s" % cel_policy_tag, + url = "https://github.com/cel-expr/cel-policy/archive/%s.tar.gz" % cel_policy_tag, + ) + def _non_module_dependencies_impl(_ctx): antlr4_jar_dependency() bazel_common_dependency() + cel_policy_dependency() non_module_dependencies = module_extension( implementation = _non_module_dependencies_impl, From 558f09e38d9d754de4286c736bdf555b612bd827 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 14 May 2026 15:55:33 -0700 Subject: [PATCH 074/204] Implement Native Extensions Future iterations may introduce an annotation based field mapping (ex: `@CelName('foo')`. PiperOrigin-RevId: 915660684 --- common/internal/BUILD.bazel | 5 + .../java/dev/cel/common/internal/BUILD.bazel | 3 + .../cel/common/internal/ReflectionUtil.java | 15 + extensions/BUILD.bazel | 5 + .../main/java/dev/cel/extensions/BUILD.bazel | 25 + .../dev/cel/extensions/CelExtensions.java | 29 +- .../extensions/CelNativeTypesExtensions.java | 1041 +++++++++++++ .../cel/extensions/CelOptionalLibrary.java | 6 +- .../main/java/dev/cel/extensions/README.md | 55 + .../test/java/dev/cel/extensions/BUILD.bazel | 2 + .../CelNativeTypesExtensionsTest.java | 1349 +++++++++++++++++ .../runtime/planner/NamespacedAttribute.java | 4 +- .../runtime/planner/RelativeAttribute.java | 4 +- 13 files changed, 2529 insertions(+), 14 deletions(-) create mode 100644 extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java create mode 100644 extensions/src/test/java/dev/cel/extensions/CelNativeTypesExtensionsTest.java diff --git a/common/internal/BUILD.bazel b/common/internal/BUILD.bazel index 781566713..7c33e56b9 100644 --- a/common/internal/BUILD.bazel +++ b/common/internal/BUILD.bazel @@ -147,3 +147,8 @@ cel_android_library( name = "date_time_helpers_android", exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers_android"], ) + +java_library( + name = "reflection_util", + exports = ["//common/src/main/java/dev/cel/common/internal:reflection_util"], +) diff --git a/common/src/main/java/dev/cel/common/internal/BUILD.bazel b/common/src/main/java/dev/cel/common/internal/BUILD.bazel index 6b470d98c..58b15b103 100644 --- a/common/src/main/java/dev/cel/common/internal/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/internal/BUILD.bazel @@ -398,8 +398,11 @@ java_library( java_library( name = "reflection_util", srcs = ["ReflectionUtil.java"], + tags = [ + ], deps = [ "//common/annotations", + "@maven//:com_google_guava_guava", ], ) diff --git a/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java b/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java index e513a446b..97bed650f 100644 --- a/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java +++ b/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java @@ -14,9 +14,11 @@ package dev.cel.common.internal; +import com.google.common.reflect.TypeToken; import dev.cel.common.annotations.Internal; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Type; /** * Utility class for invoking Java reflection. @@ -48,5 +50,18 @@ public static Object invoke(Method method, Object object, Object... params) { } } + /** Resolves a generic parameter of a base class from a type token. */ + public static Type resolveGenericParameter(TypeToken token, Class baseClass, int index) { + return token.resolveType(baseClass.getTypeParameters()[index]).getType(); + } + + /** + * Extracts the raw Class from a Type. Handles Class, ParameterizedType, and WildcardType (returns + * upper bound). Returns Object.class as fallback. + */ + public static Class getRawType(Type type) { + return TypeToken.of(type).getRawType(); + } + private ReflectionUtil() {} } diff --git a/extensions/BUILD.bazel b/extensions/BUILD.bazel index c6a029106..dea4cd760 100644 --- a/extensions/BUILD.bazel +++ b/extensions/BUILD.bazel @@ -56,3 +56,8 @@ java_library( name = "comprehensions", exports = ["//extensions/src/main/java/dev/cel/extensions:comprehensions"], ) + +java_library( + name = "native", + exports = ["//extensions/src/main/java/dev/cel/extensions:native"], +) diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel index f8e4bfc8c..73bab08c9 100644 --- a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel @@ -34,6 +34,7 @@ java_library( ":encoders", ":lists", ":math", + ":native", ":optional_library", ":protos", ":regex", @@ -185,6 +186,7 @@ java_library( "//common/types", "//common/values", "//common/values:cel_byte_string", + "//common/values:cel_value", "//compiler:compiler_builder", "//extensions:extension_library", "//parser:macro", @@ -318,3 +320,26 @@ java_library( "@maven//:com_google_guava_guava", ], ) + +java_library( + name = "native", + srcs = ["CelNativeTypesExtensions.java"], + tags = [ + ], + deps = [ + "//checker:checker_builder", + "//common/exceptions:attribute_not_found", + "//common/internal:reflection_util", + "//common/types", + "//common/types:type_providers", + "//common/values", + "//common/values:cel_byte_string", + "//common/values:cel_value", + "//common/values:cel_value_provider", + "//compiler:compiler_builder", + "//runtime", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) diff --git a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java index 8f1770f3f..8adc39384 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java @@ -15,13 +15,13 @@ package dev.cel.extensions; import static com.google.common.collect.ImmutableSet.toImmutableSet; -import static java.util.Arrays.stream; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Streams; import com.google.errorprone.annotations.InlineMe; import dev.cel.common.CelOptions; import dev.cel.extensions.CelMathExtensions.Function; +import java.util.EnumSet; import java.util.Set; /** @@ -350,6 +350,18 @@ public static CelComprehensionsExtensions comprehensions() { return COMPREHENSIONS_EXTENSIONS; } + /** + * Extensions for supporting native Java types (POJOs) in CEL. + * + *

Refer to README.md for details on property discovery, type mapping, and limitations. + * + *

Note: Passing classes with unsupported types or anonymous/local classes will result in an + * {@link IllegalArgumentException} when the runtime is built. + */ + public static CelNativeTypesExtensions nativeTypes(Class... classes) { + return CelNativeTypesExtensions.nativeTypes(classes); + } + /** * Retrieves all function names used by every extension libraries. * @@ -359,18 +371,17 @@ public static CelComprehensionsExtensions comprehensions() { */ public static ImmutableSet getAllFunctionNames() { return Streams.concat( - stream(CelMathExtensions.Function.values()) - .map(CelMathExtensions.Function::getFunction), - stream(CelStringExtensions.Function.values()) + EnumSet.allOf(Function.class).stream().map(CelMathExtensions.Function::getFunction), + EnumSet.allOf(CelStringExtensions.Function.class).stream() .map(CelStringExtensions.Function::getFunction), - stream(SetsFunction.values()).map(SetsFunction::getFunction), - stream(CelEncoderExtensions.Function.values()) + EnumSet.allOf(SetsFunction.class).stream().map(SetsFunction::getFunction), + EnumSet.allOf(CelEncoderExtensions.Function.class).stream() .map(CelEncoderExtensions.Function::getFunction), - stream(CelListsExtensions.Function.values()) + EnumSet.allOf(CelListsExtensions.Function.class).stream() .map(CelListsExtensions.Function::getFunction), - stream(CelRegexExtensions.Function.values()) + EnumSet.allOf(CelRegexExtensions.Function.class).stream() .map(CelRegexExtensions.Function::getFunction), - stream(CelComprehensionsExtensions.Function.values()) + EnumSet.allOf(CelComprehensionsExtensions.Function.class).stream() .map(CelComprehensionsExtensions.Function::getFunction)) .collect(toImmutableSet()); } diff --git a/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java new file mode 100644 index 000000000..fd579a3bc --- /dev/null +++ b/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java @@ -0,0 +1,1041 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.extensions; + +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static java.util.Arrays.stream; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.Primitives; +import com.google.common.primitives.UnsignedLong; +import com.google.common.reflect.TypeToken; +import com.google.errorprone.annotations.Immutable; +import dev.cel.checker.CelCheckerBuilder; +import dev.cel.common.exceptions.CelAttributeNotFoundException; +import dev.cel.common.internal.ReflectionUtil; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.ListType; +import dev.cel.common.types.MapType; +import dev.cel.common.types.OptionalType; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.common.values.CelByteString; +import dev.cel.common.values.CelValue; +import dev.cel.common.values.CelValueConverter; +import dev.cel.common.values.CelValueProvider; +import dev.cel.common.values.StructValue; +import dev.cel.compiler.CelCompilerLibrary; +import dev.cel.runtime.CelRuntimeBuilder; +import dev.cel.runtime.CelRuntimeLibrary; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; +import org.jspecify.annotations.Nullable; + +/** + * Extension for supporting native Java types (POJOs) in CEL. + * + *

This allows seamless plugin and evaluation of message creations and field selections without + * involving protobuf. + */ +@Immutable +public final class CelNativeTypesExtensions implements CelCompilerLibrary, CelRuntimeLibrary { + + private final NativeTypeRegistry registry; + + // Set of all standard java.lang.Object method names. + private static final ImmutableSet OBJECT_METHOD_NAMES = + stream(Object.class.getDeclaredMethods()).map(Method::getName).collect(toImmutableSet()); + + private static final ImmutableMap, CelType> JAVA_TO_CEL_TYPE_MAP = + ImmutableMap., CelType>builder() + .put(boolean.class, SimpleType.BOOL) + .put(Boolean.class, SimpleType.BOOL) + .put(String.class, SimpleType.STRING) + .put(int.class, SimpleType.INT) + .put(Integer.class, SimpleType.INT) + .put(long.class, SimpleType.INT) + .put(Long.class, SimpleType.INT) + .put(UnsignedLong.class, SimpleType.UINT) + .put(float.class, SimpleType.DOUBLE) + .put(Float.class, SimpleType.DOUBLE) + .put(double.class, SimpleType.DOUBLE) + .put(Double.class, SimpleType.DOUBLE) + .put(byte[].class, SimpleType.BYTES) + .put(CelByteString.class, SimpleType.BYTES) + .put(Duration.class, SimpleType.DURATION) + .put(Instant.class, SimpleType.TIMESTAMP) + .put(Object.class, SimpleType.DYN) + .buildOrThrow(); + + private static final ImmutableMap, Object> JAVA_TO_DEFAULT_VALUE_MAP = + ImmutableMap., Object>builder() + .put(boolean.class, false) + .put(Boolean.class, false) + .put(String.class, "") + .put(int.class, 0L) + .put(Integer.class, 0L) + .put(long.class, 0L) + .put(Long.class, 0L) + .put(UnsignedLong.class, UnsignedLong.ZERO) + .put(float.class, 0.0) + .put(Float.class, 0.0) + .put(double.class, 0.0) + .put(Double.class, 0.0) + .put(byte[].class, new byte[0]) + .put(CelByteString.class, CelByteString.EMPTY) + .put(Duration.class, Duration.ZERO) + .put(Instant.class, Instant.EPOCH) + .put(Optional.class, Optional.empty()) + .buildOrThrow(); + + /** Creates a new instance of {@link CelNativeTypesExtensions} for the given classes. */ + static CelNativeTypesExtensions nativeTypes(Class... classes) { + return new CelNativeTypesExtensions(new NativeTypeRegistry(NativeTypeScanner.scan(classes))); + } + + @VisibleForTesting + NativeTypeRegistry getRegistry() { + return registry; + } + + @Override + public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) { + runtimeBuilder.setValueProvider(registry); + runtimeBuilder.setTypeProvider(registry); + } + + @Override + public void setCheckerOptions(CelCheckerBuilder checkerBuilder) { + checkerBuilder.setTypeProvider(registry); + } + + /** + * NativeTypeScanner scans registered Java classes to extract properties and compile accessors. + */ + @VisibleForTesting + static final class NativeTypeScanner { + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + + private NativeTypeScanner() {} + + private static final class ScanResult { + private final ImmutableMap> classMap; + private final ImmutableMap typeMap; + private final ImmutableMap, StructType> classToTypeMap; + private final ImmutableMap, ImmutableMap> accessorMap; + + ScanResult( + ImmutableMap> classMap, + ImmutableMap typeMap, + ImmutableMap, StructType> classToTypeMap, + ImmutableMap, ImmutableMap> accessorMap) { + this.classMap = classMap; + this.typeMap = typeMap; + this.classToTypeMap = classToTypeMap; + this.accessorMap = accessorMap; + } + } + + private static ScanResult scan(Class... classes) { + ImmutableMap.Builder> classMapBuilder = ImmutableMap.builder(); + ImmutableMap.Builder typeMapBuilder = ImmutableMap.builder(); + ImmutableMap.Builder, StructType> classToTypeMapBuilder = ImmutableMap.builder(); + ImmutableMap.Builder, ImmutableMap> accessorMapBuilder = + ImmutableMap.builder(); + + Set> visited = new HashSet<>(); + Queue> queue = new ArrayDeque<>(Arrays.asList(classes)); + + while (!queue.isEmpty()) { + Class clazz = queue.poll(); + if (shouldSkip(clazz, visited)) { + continue; + } + visited.add(clazz); + + String typeName = getCelTypeName(clazz); + classMapBuilder.put(typeName, clazz); + + ImmutableMap accessors = scanProperties(clazz, queue); + accessorMapBuilder.put(clazz, accessors); + } + + ImmutableMap> classMap = classMapBuilder.buildOrThrow(); + ImmutableMap, ImmutableMap> accessorMap = + accessorMapBuilder.buildOrThrow(); + + for (Map.Entry> entry : classMap.entrySet()) { + String typeName = entry.getKey(); + Class clazz = entry.getValue(); + + StructType structType = createStructType(clazz, classMap, accessorMap); + typeMapBuilder.put(typeName, structType); + classToTypeMapBuilder.put(clazz, structType); + } + + ScanResult result = + new ScanResult( + classMap, + typeMapBuilder.buildOrThrow(), + classToTypeMapBuilder.buildOrThrow(), + accessorMap); + + validateRegisteredClasses(result.classToTypeMap, result.classMap, result.accessorMap); + + return result; + } + + private static void validateRegisteredClasses( + ImmutableMap, StructType> classToTypeMap, + ImmutableMap> classMap, + ImmutableMap, ImmutableMap> accessorMap) { + for (Class clazz : classToTypeMap.keySet()) { + for (String prop : getProperties(clazz)) { + try { + getPropertyType(clazz, prop, classMap, accessorMap); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "Unsupported type for property '" + prop + "' in class " + clazz.getName(), e); + } + } + } + } + + private static boolean shouldSkip(Class clazz, Set> visited) { + return clazz == null + || visited.contains(clazz) + || clazz.isInterface() + || isSupportedType(clazz); + } + + private static boolean isSupportedType(Class type) { + return JAVA_TO_CEL_TYPE_MAP.containsKey(type) + || type == Optional.class + || List.class.isAssignableFrom(type) + || Map.class.isAssignableFrom(type) + || type.isArray(); + } + + private static StructType createStructType( + Class clazz, + ImmutableMap> classMap, + ImmutableMap, ImmutableMap> accessorMap) { + return StructType.create( + getCelTypeName(clazz), + getProperties(clazz), + fieldName -> Optional.of(getPropertyType(clazz, fieldName, classMap, accessorMap))); + } + + private static CelType getPropertyType( + Class clazz, + String propertyName, + ImmutableMap> classMap, + ImmutableMap, ImmutableMap> accessorMap) { + ImmutableMap accessors = accessorMap.get(clazz); + if (accessors != null) { + PropertyAccessor accessor = accessors.get(propertyName); + if (accessor != null) { + return mapJavaTypeToCelType(accessor.targetType, accessor.genericTargetType, classMap); + } + } + throw new IllegalArgumentException("No public field or getter for " + propertyName); + } + + private static CelType mapJavaTypeToCelType( + Class type, Type genericType, ImmutableMap> classMap) { + + CelType celType = JAVA_TO_CEL_TYPE_MAP.get(type); + if (celType != null) { + return celType; + } + + if (type.isInterface() + && !List.class.isAssignableFrom(type) + && !Map.class.isAssignableFrom(type)) { + throw new IllegalArgumentException("Unsupported interface type: " + type.getName()); + } + + TypeToken token = TypeToken.of(genericType); + + if (List.class.isAssignableFrom(type)) { + Type elementType = ReflectionUtil.resolveGenericParameter(token, List.class, 0); + return ListType.create( + mapJavaTypeToCelType(ReflectionUtil.getRawType(elementType), elementType, classMap)); + } + + if (Map.class.isAssignableFrom(type)) { + Type keyType = ReflectionUtil.resolveGenericParameter(token, Map.class, 0); + Type valueType = ReflectionUtil.resolveGenericParameter(token, Map.class, 1); + + CelType celKeyType = + mapJavaTypeToCelType(ReflectionUtil.getRawType(keyType), keyType, classMap); + if (celKeyType == SimpleType.DOUBLE) { + throw new IllegalArgumentException("Decimals are not allowed as map keys in CEL."); + } + + return MapType.create( + celKeyType, + mapJavaTypeToCelType(ReflectionUtil.getRawType(valueType), valueType, classMap)); + } + + // Optional is a final class, so reference equality is equivalent to isAssignableFrom + // but slightly more performant than tree traversal. + if (type == Optional.class) { + Type optionalType = ReflectionUtil.resolveGenericParameter(token, Optional.class, 0); + return OptionalType.create( + mapJavaTypeToCelType(ReflectionUtil.getRawType(optionalType), optionalType, classMap)); + } + + String typeName = getCelTypeName(type); + if (classMap.containsKey(typeName)) { + return StructTypeReference.create(typeName); + } + + throw new IllegalArgumentException( + "Unsupported Java type for CEL mapping: " + type.getName()); + } + + private static ImmutableMap scanProperties( + Class clazz, Queue> queue) { + ImmutableMap.Builder builtAccessors = ImmutableMap.builder(); + + for (String propName : getProperties(clazz)) { + buildPropertyAccessor(clazz, propName, queue) + .ifPresent(accessor -> builtAccessors.put(propName, accessor)); + } + + return builtAccessors.buildOrThrow(); + } + + private static Optional buildPropertyAccessor( + Class clazz, String propName, Queue> queue) { + Method getter = findGetter(clazz, propName); + Field field = findField(clazz, propName); + + Class propType = null; + Type genericPropType = null; + Function compiledGetter = null; + BiConsumer compiledSetter = null; + + if (getter != null) { + propType = getter.getReturnType(); + genericPropType = getter.getGenericReturnType(); + discoverCustomTypes(genericPropType, queue); + compiledGetter = compileGetter(getter); + } else if (field != null) { + propType = field.getType(); + genericPropType = field.getGenericType(); + discoverCustomTypes(genericPropType, queue); + compiledGetter = compileFieldGetter(field); + } + + if (propType != null) { + Method setter = findSetter(clazz, propName, propType); + if (setter != null) { + compiledSetter = compileSetter(setter); + } else if (field != null + && !Modifier.isFinal(field.getModifiers()) + && Primitives.wrap(field.getType()) == Primitives.wrap(propType)) { + compiledSetter = compileFieldSetter(field); + } + } + + if (compiledGetter != null) { + return Optional.of( + new PropertyAccessor(compiledGetter, compiledSetter, propType, genericPropType)); + } + + return Optional.empty(); + } + + /** + * Recursively explores a {@link Type} and discovers any transitive, user-defined custom POJO + * classes nested inside multi-level generic collections, lists, maps, or optionals, pushing + * them into the scanning discovery queue. + * + *

"Custom types" are any public non-primitive, non-built-in Java classes that require + * explicit properties reflective scanning and mapping to a CEL StructType schema (as opposed to + * standard built-in types like {@code String}, {@code List}, or {@code Map}). + * + * @param type The Java type token or parameterized collection type to recursively unpack. + * @param queue The central scanning queue where newly discovered custom classes are pushed for + * subsequent properties discovery. + */ + private static void discoverCustomTypes(Type type, Queue> queue) { + Preconditions.checkNotNull(type, "Type to discover cannot be null."); + Preconditions.checkNotNull(queue, "Queue cannot be null."); + TypeToken token = TypeToken.of(type); + Class rawType = token.getRawType(); + + if (List.class.isAssignableFrom(rawType)) { + Type elementType = ReflectionUtil.resolveGenericParameter(token, List.class, 0); + discoverCustomTypes(elementType, queue); + return; + } + + if (Map.class.isAssignableFrom(rawType)) { + Type keyType = ReflectionUtil.resolveGenericParameter(token, Map.class, 0); + Type valueType = ReflectionUtil.resolveGenericParameter(token, Map.class, 1); + discoverCustomTypes(keyType, queue); + discoverCustomTypes(valueType, queue); + return; + } + + if (rawType == Optional.class) { + Type optionalType = ReflectionUtil.resolveGenericParameter(token, Optional.class, 0); + discoverCustomTypes(optionalType, queue); + return; + } + + if (!JAVA_TO_DEFAULT_VALUE_MAP.containsKey(rawType) + && Modifier.isPublic(rawType.getModifiers())) { + queue.add(rawType); + } + } + + private static Function compileGetter(Method getter) { + try { + // Required to unreflect public getters of package-private classes registered from other + // packages. + getter.setAccessible(true); + MethodHandle mh = LOOKUP.unreflect(getter); + return instance -> { + try { + return mh.invoke(instance); + } catch (Throwable t) { + throw new IllegalStateException("Failed to invoke getter for " + getter, t); + } + }; + } catch (IllegalAccessException e) { + throw new IllegalStateException("Failed to unreflect getter", e); + } + } + + private static Function compileFieldGetter(Field field) { + try { + // Required to unreflect public fields of package-private classes registered from other + // packages. + field.setAccessible(true); + MethodHandle mh = LOOKUP.unreflectGetter(field); + return instance -> { + try { + return mh.invoke(instance); + } catch (Throwable t) { + throw new IllegalStateException("Failed to get field " + field, t); + } + }; + } catch (IllegalAccessException e) { + throw new IllegalStateException("Failed to access field " + field, e); + } + } + + private static BiConsumer compileSetter(Method setter) { + try { + setter.setAccessible(true); + MethodHandle mh = LOOKUP.unreflect(setter); + return (instance, value) -> { + try { + mh.invoke(instance, value); + } catch (Throwable t) { + throw new IllegalStateException("Failed to invoke setter for " + setter, t); + } + }; + } catch (IllegalAccessException e) { + throw new IllegalStateException("Failed to unreflect setter", e); + } + } + + private static BiConsumer compileFieldSetter(Field field) { + try { + field.setAccessible(true); + MethodHandle mh = LOOKUP.unreflectSetter(field); + return (instance, value) -> { + try { + mh.invoke(instance, value); + } catch (Throwable t) { + throw new IllegalStateException("Failed to set field " + field, t); + } + }; + } catch (IllegalAccessException e) { + throw new IllegalStateException("Failed to access field " + field, e); + } + } + + private static @Nullable Method findGetter(Class clazz, String propertyName) { + String getterName = buildMethodName("get", propertyName); + String isGetterName = buildMethodName("is", propertyName); + + Method isGetter = null; + Method prefixLess = null; + + for (Method method : clazz.getMethods()) { + if (method.isBridge() || method.isSynthetic()) { + // Ignore compiler-generated duplicates + continue; + } + if (method.getParameterCount() == 0) { + String name = method.getName(); + if (name.equals(getterName)) { + return method; + } + if (name.equals(isGetterName)) { + isGetter = method; + } + if (name.equals(propertyName)) { + prefixLess = method; + } + } + } + + if (isGetter != null) { + return isGetter; + } + return prefixLess; + } + + private static @Nullable Field findField(Class clazz, String propertyName) { + for (Field field : clazz.getFields()) { + if (field.getName().equals(propertyName)) { + return field; + } + } + return null; + } + + private static @Nullable Method findSetter( + Class clazz, String propertyName, Class propertyType) { + String setterName = buildMethodName("set", propertyName); + return stream(clazz.getMethods()) + .filter(m -> !m.isBridge() && !m.isSynthetic()) + .filter(m -> m.getName().equals(setterName)) + .filter(m -> m.getParameterCount() == 1) + .filter(m -> m.getParameterTypes()[0].equals(propertyType)) + .findFirst() + .orElse(null); + } + + private static Set getAllDeclaredFieldNames(Class clazz) { + Set declaredFieldNames = new HashSet<>(); + Class currentClass = clazz; + while (currentClass != null) { + for (Field field : currentClass.getDeclaredFields()) { + declaredFieldNames.add(field.getName()); + } + currentClass = currentClass.getSuperclass(); + } + return declaredFieldNames; + } + + @VisibleForTesting + static ImmutableSet getProperties(Class clazz) { + ImmutableSet.Builder properties = ImmutableSet.builder(); + Set declaredFieldNames = getAllDeclaredFieldNames(clazz); + for (Field field : clazz.getFields()) { + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + properties.add(field.getName()); + } + for (Method method : clazz.getMethods()) { + if (isGetter(method)) { + String propName = getPropertyName(method); + if (method.getName().startsWith("get") || method.getName().startsWith("is")) { + properties.add(propName); + } else if (declaredFieldNames.contains(propName)) { + properties.add(propName); + } + } + } + return properties.build(); + } + + private static boolean isGetter(Method method) { + if (Modifier.isStatic(method.getModifiers())) { + return false; + } + if (!Modifier.isPublic(method.getModifiers()) || method.getParameterCount() != 0) { + return false; + } + if (method.getReturnType() == void.class) { + return false; + } + String name = method.getName(); + if (OBJECT_METHOD_NAMES.contains(name)) { + return false; + } + if (name.startsWith("get")) { + return name.length() > 3; + } + if (name.startsWith("is")) { + return name.length() > 2 && Primitives.wrap(method.getReturnType()) == Boolean.class; + } + return true; + } + + private static String decapitalize(String name) { + Preconditions.checkArgument(name != null && !name.isEmpty()); + if (name.length() > 1 + && Character.isUpperCase(name.charAt(1)) + && Character.isUpperCase(name.charAt(0))) { + return name; + } + char[] chars = name.toCharArray(); + chars[0] = Character.toLowerCase(chars[0]); + return new String(chars); + } + + private static String getPropertyName(Method method) { + String name = method.getName(); + if (name.startsWith("get")) { + return decapitalize(name.substring(3)); + } + if (name.startsWith("is")) { + return decapitalize(name.substring(2)); + } + if (name.startsWith("set")) { + return decapitalize(name.substring(3)); + } + return name; + } + + private static String capitalize(String name) { + return Character.toUpperCase(name.charAt(0)) + name.substring(1); + } + + private static String buildMethodName(String prefix, String propertyName) { + return prefix + capitalize(propertyName); + } + } + + /** + * NativeTypeRegistry holds the state produced by NativeTypeScanner and acts as a CelValueProvider + * and CelTypeProvider for the CEL runtime. + */ + @VisibleForTesting + @Immutable + static final class NativeTypeRegistry implements CelValueProvider, CelTypeProvider { + + private final ImmutableMap> classMap; + private final ImmutableMap typeMap; + private final ImmutableMap, StructType> classToTypeMap; + private final ImmutableMap, ImmutableMap> accessorMap; + private final NativeValueConverter converter; + + private NativeTypeRegistry(NativeTypeScanner.ScanResult scanResult) { + this.classMap = scanResult.classMap; + this.typeMap = scanResult.typeMap; + this.classToTypeMap = scanResult.classToTypeMap; + this.accessorMap = scanResult.accessorMap; + this.converter = new NativeValueConverter(this); + } + + @Override + public ImmutableList types() { + return ImmutableList.copyOf(typeMap.values()); + } + + @Override + public Optional findType(String typeName) { + return Optional.ofNullable(typeMap.get(typeName)); + } + + @Override + public Optional newValue(String typeName, Map fields) { + Class clazz = classMap.get(typeName); + if (clazz == null) { + return Optional.empty(); + } + + try { + Constructor constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + Object instance = constructor.newInstance(); + ImmutableMap accessors = accessorMap.get(clazz); + + for (Map.Entry entry : fields.entrySet()) { + PropertyAccessor accessor = accessors.get(entry.getKey()); + if (accessor == null) { + throw new IllegalArgumentException( + "Unknown field: " + entry.getKey() + " for type " + typeName); + } + Object value = + converter.toNative(entry.getValue(), accessor.targetType, accessor.genericTargetType); + accessor.setValue(instance, value); + } + + StructType structType = typeMap.get(typeName); + return Optional.of(new PojoStructValue(instance, accessors, structType)); + } catch (NoSuchMethodException e) { + throw new IllegalStateException( + "Failed to create instance of " + + typeName + + ": No public no-argument constructor found.", + e); + } catch (Exception e) { + throw new IllegalStateException("Failed to create instance of " + typeName, e); + } + } + + @Override + public CelValueConverter celValueConverter() { + return this.converter; + } + } + + /** + * PropertyAccessor holds the compiled getter and setter for a property, along with its type + * information. + */ + @Immutable + @SuppressWarnings("Immutable") + private static final class PropertyAccessor { + private final Function getter; + private final @Nullable BiConsumer setter; + private final Class targetType; + private final @Nullable Type genericTargetType; + + private PropertyAccessor( + Function getter, + @Nullable BiConsumer setter, + Class targetType, + @Nullable Type genericTargetType) { + this.getter = getter; + this.setter = setter; + this.targetType = targetType; + this.genericTargetType = genericTargetType; + } + + Object getValue(Object instance) { + return getter.apply(instance); + } + + Object getDefaultValue() { + return getDefaultValue(targetType); + } + + private static Object getDefaultValue(Class targetType) { + Object defaultValue = JAVA_TO_DEFAULT_VALUE_MAP.get(targetType); + if (defaultValue != null) { + return defaultValue; + } + if (List.class.isAssignableFrom(targetType)) { + return ImmutableList.of(); + } + if (Map.class.isAssignableFrom(targetType)) { + return ImmutableMap.of(); + } + + try { + Constructor constructor = targetType.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } catch (Exception e) { + throw new IllegalStateException( + String.format( + "Failed to instantiate default instance for uninitialized field of type [%s]. " + + "Please ensure the class has a no-argument constructor or is initialized.", + targetType.getName()), + e); + } + } + + void setValue(Object instance, Object value) { + if (setter != null) { + setter.accept(instance, value); + } else { + throw new IllegalStateException("No setter found for property"); + } + } + } + + /** NativeValueConverter handles conversion between Java objects and CEL values. */ + @Immutable + private static final class NativeValueConverter extends CelValueConverter { + + private final NativeTypeRegistry registry; + + private NativeValueConverter(NativeTypeRegistry registry) { + this.registry = registry; + } + + @Override + public Object toRuntimeValue(Object value) { + if (value instanceof CelValue) { + return super.toRuntimeValue(value); + } + + Class clazz = value.getClass(); + ImmutableMap accessors = registry.accessorMap.get(clazz); + + if (accessors != null) { + return new PojoStructValue(value, accessors, registry.classToTypeMap.get(clazz)); + } + + return super.toRuntimeValue(value); + } + + Object toNative(Object value, Class targetType, Type genericType) { + if (value instanceof CelValue && !StructValue.class.isAssignableFrom(targetType)) { + value = super.maybeUnwrap(value); + } + if (targetType == Optional.class) { + if (value instanceof Optional) { + return value; + } + return Optional.ofNullable(value); + } + if (targetType == UnsignedLong.class) { + if (value instanceof UnsignedLong) { + return value; + } + } + if (targetType == byte[].class && value instanceof CelByteString) { + return ((CelByteString) value).toByteArray(); + } + + if (List.class.isAssignableFrom(targetType) && value instanceof List) { + return convertListToNative((List) value, targetType, genericType); + } + + if (Map.class.isAssignableFrom(targetType) && value instanceof Map) { + return convertMapToNative((Map) value, targetType, genericType); + } + + return downcastPrimitives(value, targetType); + } + + // Safe reflection collection cast. + @SuppressWarnings("unchecked") + private Object convertListToNative(List list, Class targetType, Type genericType) { + TypeToken token = TypeToken.of(genericType); + Type elementType = ReflectionUtil.resolveGenericParameter(token, List.class, 0); + Class componentType = ReflectionUtil.getRawType(elementType); + + boolean isConcreteClass = + !targetType.isInterface() && !Modifier.isAbstract(targetType.getModifiers()); + + // Instantiates concrete collection types to prevent ClassCastExceptions. + // For example, if a POJO field is declared as a concrete implementation like + // ArrayList, assigning a Guava ImmutableList will fail at runtime due to type + // mismatch. + if (isConcreteClass) { + List concreteList; + try { + concreteList = (List) targetType.getConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalStateException( + "Failed to instantiate concrete collection class for field target type: " + + targetType.getName(), + e); + } + + for (Object element : list) { + concreteList.add(toNative(element, componentType, elementType)); + } + return concreteList; + } + + ImmutableList.Builder builder = null; + for (int i = 0; i < list.size(); i++) { + Object element = list.get(i); + Object converted = toNative(element, componentType, elementType); + if (!Objects.equals(converted, element) && builder == null) { + builder = ImmutableList.builderWithExpectedSize(list.size()); + for (int j = 0; j < i; j++) { + builder.add(list.get(j)); + } + } + if (builder != null) { + builder.add(converted); + } + } + + if (builder == null) { + return list; + } + return builder.build(); + } + + // Safe reflection collection cast. + @SuppressWarnings("unchecked") + private Object convertMapToNative(Map map, Class targetType, Type genericType) { + TypeToken token = TypeToken.of(genericType); + Type keyType = ReflectionUtil.resolveGenericParameter(token, Map.class, 0); + Type valueType = ReflectionUtil.resolveGenericParameter(token, Map.class, 1); + Class rawKeyType = ReflectionUtil.getRawType(keyType); + Class rawValueType = ReflectionUtil.getRawType(valueType); + + boolean isConcreteClass = + !targetType.isInterface() && !Modifier.isAbstract(targetType.getModifiers()); + + // Instantiates concrete map types to prevent ClassCastExceptions. + // For example, if a POJO field is declared as a concrete implementation like HashMap, + // assigning a Guava ImmutableMap will fail at runtime due to type mismatch. + if (isConcreteClass) { + Map concreteMap; + try { + concreteMap = (Map) targetType.getConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalStateException( + "Failed to instantiate concrete map class for field target type: " + + targetType.getName(), + e); + } + + for (Map.Entry entry : map.entrySet()) { + concreteMap.put( + toNative(entry.getKey(), rawKeyType, keyType), + toNative(entry.getValue(), rawValueType, valueType)); + } + return concreteMap; + } + + ImmutableMap.Builder builder = null; + for (Map.Entry entry : map.entrySet()) { + Object key = entry.getKey(); + Object val = entry.getValue(); + Object convertedKey = toNative(key, rawKeyType, keyType); + Object convertedVal = toNative(val, rawValueType, valueType); + + if ((!Objects.equals(convertedKey, key) || !Objects.equals(convertedVal, val)) + && builder == null) { + builder = ImmutableMap.builderWithExpectedSize(map.size()); + for (Map.Entry prevEntry : map.entrySet()) { + if (Objects.equals(prevEntry.getKey(), entry.getKey())) { + break; + } + builder.put(prevEntry.getKey(), prevEntry.getValue()); + } + } + + if (builder != null) { + builder.put(convertedKey, convertedVal); + } + } + + if (builder == null) { + return map; + } + return builder.buildOrThrow(); + } + + private Object downcastPrimitives(Object value, Class targetType) { + Class wrappedTargetType = Primitives.wrap(targetType); + if (wrappedTargetType == Integer.class && value instanceof Long) { + return ((Long) value).intValue(); + } + if (wrappedTargetType == Float.class && value instanceof Double) { + return ((Double) value).floatValue(); + } + + return value; + } + } + + /** PojoStructValue represents a native Java object as a CEL struct value. */ + @SuppressWarnings("Immutable") + private static final class PojoStructValue extends StructValue { + private final Object instance; + private final ImmutableMap accessors; + private final StructType celType; + + private PojoStructValue( + Object instance, ImmutableMap accessors, StructType celType) { + this.instance = instance; + this.accessors = accessors; + this.celType = celType; + } + + @Override + public Object value() { + return instance; + } + + @Override + public boolean isZeroValue() { + throw new UnsupportedOperationException( + "isZeroValue is unsupported for ordinary Java POJOs. Please implement StructValue" + + " directly on the backing class if zero-value trait support is required."); + } + + @Override + public CelType celType() { + return celType; + } + + @Override + public Object select(String field) { + // Intentionally not proxying `find` here to avoid Optional wrapper allocations. + PropertyAccessor accessor = accessors.get(field); + if (accessor != null) { + Object value = accessor.getValue(instance); + if (value == null) { + return accessor.getDefaultValue(); + } + return value; + } + throw CelAttributeNotFoundException.forFieldResolution(field); + } + + @Override + public Optional find(String field) { + PropertyAccessor accessor = accessors.get(field); + if (accessor == null) { + return Optional.empty(); + } + Object value = accessor.getValue(instance); + return Optional.ofNullable(value); + } + } + + private static String getCelTypeName(Class clazz) { + String canonicalName = clazz.getCanonicalName(); + if (canonicalName == null) { + throw new IllegalArgumentException( + "Cannot get canonical name for class: " + + clazz.getName() + + ". Anonymous or local classes are not supported."); + } + return canonicalName; + } + + private CelNativeTypesExtensions(NativeTypeRegistry registry) { + this.registry = registry; + } +} diff --git a/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java b/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java index a3777c759..87a31341f 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java +++ b/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java @@ -53,6 +53,7 @@ import dev.cel.common.types.TypeParamType; import dev.cel.common.types.TypeType; import dev.cel.common.values.CelByteString; +import dev.cel.common.values.CelValue; import dev.cel.common.values.NullValue; import dev.cel.compiler.CelCompilerLibrary; import dev.cel.parser.CelMacro; @@ -415,9 +416,6 @@ private static ImmutableList elideOptionalCollection(Collection variables) throws Exception { + CelAbstractSyntaxTree ast = isParseOnly ? CEL.parse(expr).getAst() : CEL.compile(expr).getAst(); + return CEL.createProgram(ast).eval(variables); + } + + @Test + public void nativeTypes_createStructAndSelect() throws Exception { + Object result = + eval( + "TestAllTypesPublicFieldsPojo{boolVal:" + + " true, stringVal: 'hello'}.stringVal == 'hello'"); + + assertThat(result).isEqualTo(true); + } + + @Test + public void nativeTypes_createNestedStruct() throws Exception { + Object result = + eval( + "TestAllTypesPublicFieldsPojo{nestedVal:" + + " TestNestedType{value:" + + " 'nested'}}.nestedVal.value == 'nested'"); + + assertThat(result).isEqualTo(true); + } + + @Test + public void nativeTypes_resolveVariableWithNestedField() throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addVar( + "pojo", + StructTypeReference.create(TestAllTypesPublicFieldsPojo.class.getCanonicalName())) + .addCompilerLibraries(NATIVE_TYPE_EXTENSIONS) + .addRuntimeLibraries(NATIVE_TYPE_EXTENSIONS) + .build(); + CelAbstractSyntaxTree ast = + isParseOnly + ? cel.parse("pojo.nestedVal.value == 'nested'").getAst() + : cel.compile("pojo.nestedVal.value == 'nested'").getAst(); + CelRuntime.Program program = cel.createProgram(ast); + TestAllTypesPublicFieldsPojo pojo = new TestAllTypesPublicFieldsPojo(); + TestNestedType nested = new TestNestedType(); + nested.value = "nested"; + pojo.nestedVal = nested; + + Object result = program.eval(ImmutableMap.of("pojo", pojo)); + + assertThat(result).isEqualTo(true); + } + + @Test + public void nativeTypes_createStructWithComplexTypes() throws Exception { + assertThat( + eval( + "TestAllTypesPublicFieldsPojo{" + + " durationVal: duration('5s')," + + " listVal: ['a', 'b']," + + " mapVal: {'key': 'value'}" + + "}.durationVal == duration('5s')")) + .isEqualTo(true); + } + + @Test + public void nativeTypes_transitiveDiscoveryThroughMap() throws Exception { + PojoWithCustomMap pojo = new PojoWithCustomMap(); + HashMap map = new HashMap<>(); + TestNestedType nested = new TestNestedType(); + nested.value = "hello"; + map.put("key", nested); + pojo.mapVal = map; + + CelNativeTypesExtensions extensions = + CelNativeTypesExtensions.nativeTypes(PojoWithCustomMap.class); + Cel cel = + CelFactory.plannerCelBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addVar("pojo", StructTypeReference.create(PojoWithCustomMap.class.getCanonicalName())) + .addCompilerLibraries(extensions) + .addRuntimeLibraries(extensions) + .build(); + + CelAbstractSyntaxTree ast = cel.compile("pojo.mapVal['key'].value == 'hello'").getAst(); + Object result = cel.createProgram(ast).eval(ImmutableMap.of("pojo", pojo)); + + assertThat(result).isEqualTo(true); + } + + @Test + public void nativeTypes_createStructWithOptionalField() throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addCompilerLibraries( + CelExtensions.nativeTypes(TestRefValFieldType.class), CelExtensions.optional()) + .addRuntimeLibraries( + CelExtensions.nativeTypes(TestRefValFieldType.class), CelExtensions.optional()) + .build(); + CelAbstractSyntaxTree ast = + cel.parse( + "TestRefValFieldType{optionalName: optional.of('my name')}.optionalName.orValue('')" + + " == 'my name'") + .getAst(); + CelRuntime.Program program = cel.createProgram(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo(true); + } + + @Test + public void nativeTypes_createComprehensiveStruct() throws Exception { + String expr = + "ComprehensiveTestAllTypes{\n" + + " nestedVal: ComprehensiveTestNestedType{nestedMapVal: {1: false}},\n" + + " boolVal: true,\n" + + " bytesVal: b'hello',\n" + + " durationVal: duration('5s'),\n" + + " doubleVal: 1.5,\n" + + " floatVal: 2.5,\n" + + " int32Val: 10,\n" + + " int64Val: 20,\n" + + " stringVal: 'hello world',\n" + + " timestampVal: timestamp('2011-08-06T01:23:45Z'),\n" + + " uint32Val: 100,\n" + + " uint64Val: 200,\n" + + " listVal: [\n" + + " ComprehensiveTestNestedType{\n" + + " nestedListVal:['goodbye', 'cruel', 'world'],\n" + + " nestedMapVal: {42: true},\n" + + " customName: 'name'\n" + + " }\n" + + " ],\n" + + " arrayVal: [\n" + + " ComprehensiveTestNestedType{\n" + + " nestedListVal:['goodbye', 'cruel', 'world'],\n" + + " nestedMapVal: {42: true},\n" + + " customName: 'name'\n" + + " }\n" + + " ],\n" + + " mapVal: {'map-key': ComprehensiveTestAllTypes{boolVal: true}},\n" + + " customSliceVal: [TestNestedSliceType{value: 'none'}],\n" + + " customMapVal: {'even': TestMapVal{value: 'more'}},\n" + + " customName: 'name'\n" + + "}"; + + CelAbstractSyntaxTree ast = CEL.parse(expr).getAst(); + CelRuntime.Program program = CEL.createProgram(ast); + Object result = program.eval(); + + // Construct expected output + ComprehensiveTestAllTypes expected = new ComprehensiveTestAllTypes(); + expected.boolVal = true; + expected.bytesVal = "hello".getBytes(UTF_8); + expected.durationVal = Duration.ofSeconds(5); + expected.doubleVal = 1.5; + expected.floatVal = 2.5f; + expected.int32Val = 10; + expected.int64Val = 20; + expected.stringVal = "hello world"; + expected.timestampVal = Instant.parse("2011-08-06T01:23:45Z"); + expected.uint32Val = 100; + expected.uint64Val = 200; + expected.customName = "name"; + + ComprehensiveTestNestedType nested1 = new ComprehensiveTestNestedType(); + nested1.nestedMapVal = ImmutableMap.of(1L, false); + expected.nestedVal = nested1; + + ComprehensiveTestNestedType nested2 = new ComprehensiveTestNestedType(); + nested2.nestedListVal = ImmutableList.of("goodbye", "cruel", "world"); + nested2.nestedMapVal = ImmutableMap.of(42L, true); + nested2.customName = "name"; + expected.listVal = ImmutableList.of(nested2); + expected.arrayVal = ImmutableList.of(nested2); + + ComprehensiveTestAllTypes mapValElement = new ComprehensiveTestAllTypes(); + mapValElement.boolVal = true; + expected.mapVal = ImmutableMap.of("map-key", mapValElement); + + TestNestedSliceType sliceElem = new TestNestedSliceType(); + sliceElem.value = "none"; + expected.customSliceVal = ImmutableList.of(sliceElem); + + TestMapVal mapValElem = new TestMapVal(); + mapValElem.value = "more"; + expected.customMapVal = ImmutableMap.of("even", mapValElem); + + assertThat(result).isEqualTo(expected); + } + + @Test + public void nativeTypes_staticErrors() throws Exception { + // undeclared reference + CelValidationException e = + assertThrows(CelValidationException.class, () -> CEL.compile("UnknownType{}").getAst()); + assertThat(e).hasMessageThat().contains("reference"); + + // undefined field + e = + assertThrows( + CelValidationException.class, + () -> CEL.compile("ComprehensiveTestAllTypes{undefinedField: true}").getAst()); + assertThat(e).hasMessageThat().contains("undefined field"); + } + + @Test + public void nativeTypes_anonymousClass_throwsException() { + Object anon = new Object() {}; + + Class clazz = anon.getClass(); + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, () -> CelExtensions.nativeTypes(clazz)); + assertThat(exception).hasMessageThat().contains("Anonymous or local classes are not supported"); + } + + @Test + public void nativeTypes_createStruct_privateConstructor() throws Exception { + Object result = eval("TestPrivateConstructorPojo{value:" + " 'hello'}"); + + assertThat(result).isInstanceOf(TestPrivateConstructorPojo.class); + assertThat(((TestPrivateConstructorPojo) result).value).isEqualTo("hello"); + } + + @Test + public void nativeTypes_precedence_getterOverField() throws Exception { + assertThat(eval("TestPrecedencePojo{}.value")).isEqualTo("hello"); + } + + @Test + public void nativeTypes_protoPrecedence() throws Exception { + CelValueProvider customProvider = + (structType, fields) -> { + if (structType.equals("cel.expr.conformance.proto3.TestAllTypes")) { + return Optional.of("POJO_WINS"); + } + return Optional.empty(); + }; + Cel cel = + CelFactory.plannerCelBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .setValueProvider(customProvider) + .addMessageTypes(TestAllTypes.getDescriptor()) + .build(); + CelAbstractSyntaxTree ast = cel.compile("cel.expr.conformance.proto3.TestAllTypes{}").getAst(); + + Object result = cel.createProgram(ast).eval(); + + assertThat(result).isNotEqualTo("POJO_WINS"); + assertThat(result).isInstanceOf(TestAllTypes.class); + } + + @Test + public void nativeTypes_createWithSetterAndSelectWithGetter() throws Exception { + assertThat(eval("TestGetterSetterPojo{value: 'hello', active: true}.value == 'hello'")) + .isEqualTo(true); + } + + @Test + public void nativeTypes_missingNoArgConstructor_throws() throws Exception { + CelEvaluationException exception = + assertThrows( + CelEvaluationException.class, + () -> eval("TestMissingNoArgConstructorPojo{value: 'hello'}")); + + assertThat(exception).hasMessageThat().contains("No public no-argument constructor found"); + } + + @Test + public void nativeTypes_createWithDeepConversion() throws Exception { + Object result = eval("TestDeepConversionPojo{ints: [1, 2], floats: {'a': 1.0, 'b': 2.0}}"); + + assertThat(result).isInstanceOf(TestDeepConversionPojo.class); + TestDeepConversionPojo pojo = (TestDeepConversionPojo) result; + assertThat(pojo.ints.get(0)).isEqualTo(1); + assertThat(pojo.floats).containsEntry("a", 1.0f); + } + + @Test + public void nativeTypes_wildcardList_success() throws Exception { + assertThat(eval("TestWildcardPojo{values: ['hello']}.values[0] == 'hello'")).isEqualTo(true); + } + + @Test + public void nativeTypes_unsupportedTypeSet_throwsOnRegistration() throws Exception { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> CelExtensions.nativeTypes(TestUnsupportedSetPojo.class)); + assertThat(e).hasMessageThat().contains("Unsupported type for property 'strings'"); + } + + @Test + public void nativeTypes_arrayType_throwsOnRegistration() throws Exception { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, () -> CelExtensions.nativeTypes(TestArrayPojo.class)); + assertThat(e).hasMessageThat().contains("Unsupported type for property 'values'"); + } + + @Test + public void nativeTypes_packagePrivateClass_fieldAccess_success() throws Exception { + assertThat(eval("TestPackagePrivatePojo{value: 'hello'}.value == 'hello'")).isEqualTo(true); + } + + @Test + public void nativeTypes_packagePrivateClass_methodAccess_success() throws Exception { + assertThat(eval("TestPackagePrivateWithGetterPojo{value: 'hello'}.value == 'hello'")) + .isEqualTo(true); + } + + @Test + public void nativeTypes_privateField_notExposed() throws Exception { + CelNativeTypesExtensions extensions = CelExtensions.nativeTypes(TestPrivateFieldPojo.class); + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addLibraries(extensions) + .build(); + + CelValidationException e = + assertThrows( + CelValidationException.class, + () -> compiler.compile("TestPrivateFieldPojo{secret: 'hello'}").getAst()); + assertThat(e).hasMessageThat().contains("undefined field"); + } + + @Test + public void nativeTypes_inheritance_success() throws Exception { + // Accessing child's prefix-less getter + assertThat(eval("TestChildPojo{}.childValue")).isEqualTo("child"); + // Accessing parent's standard getter + assertThat(eval("TestChildPojo{}.standardValue")).isEqualTo("standard"); + // Accessing parent's prefix-less getter + assertThat(eval("TestChildPojo{}.parentValue")).isEqualTo("parent"); + } + + @Test + public void nativeTypes_standardType_cannotBeConstructedAsStruct() throws Exception { + CelValidationException e = + assertThrows( + CelValidationException.class, () -> CEL.compile("java.lang.String{}").getAst()); + assertThat(e).hasMessageThat().contains("undeclared reference"); + } + + @Test + public void nativeTypes_doubleMapKey_throwsOnRegistration() throws Exception { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> CelExtensions.nativeTypes(TestDoubleMapKeyPojo.class)); + assertThat(e).hasCauseThat().hasMessageThat().contains("Decimals are not allowed as map keys"); + } + + @Test + public void nativeTypes_optionalCustomStruct_registered() throws Exception { + CelNativeTypesExtensions extensions = CelExtensions.nativeTypes(TestOptionalUrlPojo.class); + CelNativeTypesExtensions.NativeTypeRegistry registry = extensions.getRegistry(); + + Optional type = registry.findType(TestURLPojo.class.getCanonicalName()); + + assertThat(type).isPresent(); + } + + @Test + public void nativeTypes_abstractClass_throwsOnConstruction() throws Exception { + CelAbstractSyntaxTree ast = CEL.parse("TestAbstractPojo{}").getAst(); + CelRuntime.Program program = CEL.createProgram(ast); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> program.eval()); + assertThat(e).hasMessageThat().contains("Failed to create instance of"); + assertThat(e).hasCauseThat().isInstanceOf(InstantiationException.class); + } + + @Test + public void nativeTypes_nestedList_registered() throws Exception { + CelNativeTypesExtensions extensions = + CelExtensions.nativeTypes(TestAllTypesPublicFieldsPojo.class); + CelNativeTypesExtensions.NativeTypeRegistry registry = extensions.getRegistry(); + + Optional type = + registry.findType(TestAllTypesPublicFieldsPojo.class.getCanonicalName()); + + assertThat(type).isPresent(); + StructType structType = (StructType) type.get(); + assertThat(structType.findField("nestedListVal")).isPresent(); + } + + @Test + public void nativeTypes_invalidGetters_notRegistered() throws Exception { + ImmutableSet properties = + CelNativeTypesExtensions.NativeTypeScanner.getProperties( + TestAllTypesPublicFieldsPojo.class); + + assertThat(properties).doesNotContain("invalidParam"); + assertThat(properties).doesNotContain("invalidString"); + } + + @Test + public void nativeTypes_celByteString_success() throws Exception { + assertThat(eval("TestAllTypesPublicFieldsPojo{}.celBytesVal" + " == b'\\x01\\x02\\x03'")) + .isEqualTo(true); + } + + @Test + public void nativeTypes_celByteString_construction_success() throws Exception { + assertThat( + eval( + "dev.cel.extensions.CelNativeTypesExtensionsTest.TestAllTypesPublicFieldsPojo{celBytesVal:" + + " b'\\x01\\x02\\x03'}.celBytesVal == b'\\x01\\x02\\x03'")) + .isEqualTo(true); + } + + @Test + public void nativeTypes_singleLetterGetter_success() throws Exception { + Object result = eval("TestAllTypesPublicFieldsPojo{}.a == 'a'"); + assertThat(result).isEqualTo(true); + } + + @Test + public void nativeTypes_getterNamedGet_rejected() throws Exception { + CelValidationException e = + assertThrows( + CelValidationException.class, + () -> CEL.compile("TestAllTypesPublicFieldsPojo{}.get").getAst()); + assertThat(e).hasMessageThat().contains("undefined field 'get'"); + } + + @Test + public void nativeTypes_circularReference_success() throws Exception { + CelNativeTypesExtensions extensions = CelExtensions.nativeTypes(TestCircularA.class); + CelNativeTypesExtensions.NativeTypeRegistry registry = extensions.getRegistry(); + + Optional typeA = registry.findType(TestCircularA.class.getCanonicalName()); + Optional typeB = registry.findType(TestCircularB.class.getCanonicalName()); + + assertThat(typeA).isPresent(); + assertThat(typeB).isPresent(); + } + + @Test + public void nativeTypes_specialDecapitalization_success() throws Exception { + Object result = eval("dev.cel.extensions.CelNativeTypesExtensionsTest.TestURLPojo{}.URL"); + + assertThat(result).isEqualTo("https://google.com"); + } + + @Test + public void nativeTypes_prefixLessGetter_success() throws Exception { + CelNativeTypesExtensions extensions = CelExtensions.nativeTypes(TestPrefixLessGetterPojo.class); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addLibraries(extensions) + .build(); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addLibraries(extensions) + .build(); + CelAbstractSyntaxTree ast = + celCompiler + .compile( + "dev.cel.extensions.CelNativeTypesExtensionsTest.TestPrefixLessGetterPojo{}.value") + .getAst(); + CelRuntime.Program program = celRuntime.createProgram(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo("hello"); + } + + @Test + public void nativeTypes_isGetter_success() throws Exception { + CelNativeTypesExtensions extensions = CelExtensions.nativeTypes(TestGetterSetterPojo.class); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addLibraries(extensions) + .build(); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addLibraries(extensions) + .build(); + CelAbstractSyntaxTree ast = + celCompiler + .compile( + "dev.cel.extensions.CelNativeTypesExtensionsTest.TestGetterSetterPojo{active:" + + " true}.active") + .getAst(); + CelRuntime.Program program = celRuntime.createProgram(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo(true); + } + + @Test + public void nativeTypes_selectUndefinedField_parsedOnly_throwsException() throws Exception { + + CelNativeTypesExtensions extensions = + CelExtensions.nativeTypes(TestAllTypesPublicFieldsPojo.class); + + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addLibraries(extensions) + .build(); + + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addLibraries(extensions) + .build(); + + CelAbstractSyntaxTree ast = celCompiler.parse("pojo.undefinedField").getAst(); + CelRuntime.Program program = celRuntime.createProgram(ast); + + TestAllTypesPublicFieldsPojo pojo = new TestAllTypesPublicFieldsPojo(); + + CelEvaluationException e = + assertThrows( + CelEvaluationException.class, () -> program.eval(ImmutableMap.of("pojo", pojo))); + assertThat(e).hasCauseThat().isInstanceOf(CelAttributeNotFoundException.class); + } + + @Test + public void nativeTypes_createWithUint_fromUnsignedLong() throws Exception { + CelNativeTypesExtensions extensions = + CelExtensions.nativeTypes(TestAllTypesPublicFieldsPojo.class); + CelRuntime celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addLibraries(extensions) + .build(); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addLibraries(extensions) + .build(); + CelAbstractSyntaxTree ast = + celCompiler + .compile( + "dev.cel.extensions.CelNativeTypesExtensionsTest.TestAllTypesPublicFieldsPojo{uintVal:" + + " 42u}") + .getAst(); + CelRuntime.Program program = celRuntime.createProgram(ast); + + Object result = program.eval(); + + assertThat(result).isInstanceOf(TestAllTypesPublicFieldsPojo.class); + TestAllTypesPublicFieldsPojo pojo = (TestAllTypesPublicFieldsPojo) result; + assertThat(pojo.uintVal).isEqualTo(UnsignedLong.fromLongBits(42L)); + } + + @Test + public void nativeTypes_mapJavaTypeToCelType_allSupportedTypes() throws Exception { + CelNativeTypesExtensions extensions = + CelExtensions.nativeTypes(TestAllTypesPublicFieldsPojo.class); + CelNativeTypesExtensions.NativeTypeRegistry registry = extensions.getRegistry(); + + Optional type = + registry.findType(TestAllTypesPublicFieldsPojo.class.getCanonicalName()); + + assertThat(type).isPresent(); + assertThat(type.get()).isInstanceOf(StructType.class); + StructType structType = (StructType) type.get(); + + assertThat(structType.findField("boolVal").map(StructType.Field::type)) + .hasValue(SimpleType.BOOL); + assertThat(structType.findField("boolObjVal").map(StructType.Field::type)) + .hasValue(SimpleType.BOOL); + assertThat(structType.findField("int32Val").map(StructType.Field::type)) + .hasValue(SimpleType.INT); + assertThat(structType.findField("intObjVal").map(StructType.Field::type)) + .hasValue(SimpleType.INT); + assertThat(structType.findField("int64Val").map(StructType.Field::type)) + .hasValue(SimpleType.INT); + assertThat(structType.findField("longObjVal").map(StructType.Field::type)) + .hasValue(SimpleType.INT); + assertThat(structType.findField("uintVal").map(StructType.Field::type)) + .hasValue(SimpleType.UINT); + assertThat(structType.findField("floatVal").map(StructType.Field::type)) + .hasValue(SimpleType.DOUBLE); + assertThat(structType.findField("floatObjVal").map(StructType.Field::type)) + .hasValue(SimpleType.DOUBLE); + assertThat(structType.findField("doubleVal").map(StructType.Field::type)) + .hasValue(SimpleType.DOUBLE); + assertThat(structType.findField("doubleObjVal").map(StructType.Field::type)) + .hasValue(SimpleType.DOUBLE); + assertThat(structType.findField("stringVal").map(StructType.Field::type)) + .hasValue(SimpleType.STRING); + assertThat(structType.findField("bytesVal").map(StructType.Field::type)) + .hasValue(SimpleType.BYTES); + assertThat(structType.findField("durationVal").map(StructType.Field::type)) + .hasValue(SimpleType.DURATION); + assertThat(structType.findField("timestampVal").map(StructType.Field::type)) + .hasValue(SimpleType.TIMESTAMP); + + assertThat(structType.findField("listVal").map(StructType.Field::type).get()) + .isInstanceOf(ListType.class); + ListType listType = + (ListType) structType.findField("listVal").map(StructType.Field::type).get(); + assertThat(listType.elemType()).isEqualTo(SimpleType.STRING); + + assertThat(structType.findField("mapIntVal").map(StructType.Field::type).get()) + .isInstanceOf(MapType.class); + MapType mapType = (MapType) structType.findField("mapIntVal").map(StructType.Field::type).get(); + assertThat(mapType.keyType()).isEqualTo(SimpleType.STRING); + assertThat(mapType.valueType()).isEqualTo(SimpleType.INT); + + assertThat(structType.findField("optionalVal").map(StructType.Field::type).get()) + .isInstanceOf(OptionalType.class); + OptionalType optionalType = + (OptionalType) structType.findField("optionalVal").map(StructType.Field::type).get(); + assertThat(optionalType.parameters().get(0)).isEqualTo(SimpleType.STRING); + } + + @Test + public void nativeTypes_mapJavaTypeToCelType_customCollectionSubclasses() throws Exception { + CelNativeTypesExtensions extensions = CelExtensions.nativeTypes(TestCustomCollectionPojo.class); + CelNativeTypesExtensions.NativeTypeRegistry registry = extensions.getRegistry(); + + Optional type = registry.findType(TestCustomCollectionPojo.class.getCanonicalName()); + StructType structType = (StructType) type.get(); + + assertThat(structType.findField("customList").map(StructType.Field::type)) + .hasValue(ListType.create(SimpleType.STRING)); + assertThat(structType.findField("customMap").map(StructType.Field::type)) + .hasValue(MapType.create(SimpleType.STRING, SimpleType.INT)); + } + + @Test + public void nativeTypes_objectMethods_notExposed() throws Exception { + CelNativeTypesExtensions extensions = + CelExtensions.nativeTypes(TestAllTypesPublicFieldsPojo.class); + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addLibraries(extensions) + .build(); + + CelValidationException e = + assertThrows( + CelValidationException.class, + () -> compiler.compile("TestAllTypesPublicFieldsPojo{}.toString").getAst()); + assertThat(e).hasMessageThat().contains("undefined field"); + } + + @Test + public void nativeTypes_nullSafeTraversal() throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addCompilerLibraries(NATIVE_TYPE_EXTENSIONS) + .addRuntimeLibraries(NATIVE_TYPE_EXTENSIONS) + .addVar( + "pojo", + StructTypeReference.create(TestAllTypesPublicFieldsPojo.class.getCanonicalName())) + .build(); + + TestAllTypesPublicFieldsPojo pojo = new TestAllTypesPublicFieldsPojo(); + ImmutableMap vars = ImmutableMap.of("pojo", pojo); + + assertThat(cel.createProgram(cel.compile("pojo.stringVal").getAst()).eval(vars)).isEqualTo(""); + assertThat(cel.createProgram(cel.compile("pojo.int64Val").getAst()).eval(vars)).isEqualTo(0L); + assertThat(cel.createProgram(cel.compile("pojo.nestedVal.value").getAst()).eval(vars)) + .isEqualTo(""); + CelAbstractSyntaxTree abstractPojoAst = cel.compile("pojo.abstractPojo.value").getAst(); + CelRuntime.Program abstractPojoProgram = cel.createProgram(abstractPojoAst); + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> abstractPojoProgram.eval(vars)); + assertThat(e).hasMessageThat().contains("Failed to instantiate default instance"); + } + + @Test + public void nativeTypes_presenceTest() throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addCompilerLibraries(NATIVE_TYPE_EXTENSIONS) + .addRuntimeLibraries(NATIVE_TYPE_EXTENSIONS) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addVar( + "pojo", + StructTypeReference.create(TestAllTypesPublicFieldsPojo.class.getCanonicalName())) + .build(); + + TestAllTypesPublicFieldsPojo pojo = new TestAllTypesPublicFieldsPojo(); + ImmutableMap nullVars = ImmutableMap.of("pojo", pojo); + + TestAllTypesPublicFieldsPojo pojoWithValues = new TestAllTypesPublicFieldsPojo(); + pojoWithValues.stringVal = "hello"; + ImmutableMap valueVars = ImmutableMap.of("pojo", pojoWithValues); + + boolean hasPopulatedString = + (boolean) cel.createProgram(cel.compile("has(pojo.stringVal)").getAst()).eval(valueVars); + assertThat(hasPopulatedString).isTrue(); + + boolean hasNullString = + (boolean) cel.createProgram(cel.compile("has(pojo.stringVal)").getAst()).eval(nullVars); + assertThat(hasNullString).isFalse(); + + assertThrows( + CelValidationException.class, () -> cel.compile("has(pojo.nonExistentField)").getAst()); + } + + @Test + public void nativeTypes_zeroValue_collections_comprehensions() throws Exception { + assertThat(eval("TestAllTypesPublicFieldsPojo{}.listVal.filter(x, true) == []")) + .isEqualTo(true); + assertThat(eval("TestAllTypesPublicFieldsPojo{}.listVal.map(x, x + 'foo') == []")) + .isEqualTo(true); + assertThat(eval("TestAllTypesPublicFieldsPojo{}.listVal.exists(x, true)")).isEqualTo(false); + assertThat(eval("TestAllTypesPublicFieldsPojo{}.listVal.all(x, true)")).isEqualTo(true); + assertThat(eval("TestAllTypesPublicFieldsPojo{}.mapVal.exists(k, true)")).isEqualTo(false); + assertThat(eval("TestAllTypesPublicFieldsPojo{}.mapVal.all(k, true)")).isEqualTo(true); + } + + @Test + public void nativeTypes_customStructValue_optionalOfNonZeroValue() throws Exception { + CelNativeTypesExtensions extensions = + CelExtensions.nativeTypes(TestCustomStructValuePojo.class); + Cel cel = + CelFactory.plannerCelBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addCompilerLibraries(extensions, CelExtensions.optional()) + .addRuntimeLibraries(extensions, CelExtensions.optional()) + .addVar( + "pojo", + StructTypeReference.create(TestCustomStructValuePojo.class.getCanonicalName())) + .build(); + + TestCustomStructValuePojo emptyPojo = + new TestCustomStructValuePojo(ImmutableMap.of("value", "")); + ImmutableMap emptyVars = ImmutableMap.of("pojo", emptyPojo); + boolean isEmptyNone = + (boolean) + cel.createProgram(cel.compile("!optional.ofNonZeroValue(pojo).hasValue()").getAst()) + .eval(emptyVars); + assertThat(isEmptyNone).isTrue(); + + TestCustomStructValuePojo populatedPojo = + new TestCustomStructValuePojo(ImmutableMap.of("value", "hello")); + ImmutableMap populatedVars = ImmutableMap.of("pojo", populatedPojo); + boolean isPopulatedPresent = + (boolean) + cel.createProgram(cel.compile("optional.ofNonZeroValue(pojo).hasValue()").getAst()) + .eval(populatedVars); + assertThat(isPopulatedPresent).isTrue(); + } + + @Test + public void nativeTypes_staticMembers_skipped() throws Exception { + ImmutableSet properties = + CelNativeTypesExtensions.NativeTypeScanner.getProperties(TestStaticMembersPojo.class); + + assertThat(properties).contains("instanceField"); + assertThat(properties).doesNotContain("STATIC_FIELD"); + assertThat(properties).doesNotContain("staticGetter"); + assertThat(properties).doesNotContain("staticProperty"); + } + + @Test + public void nativeTypes_deeplyNestedGenerics_discovered() throws Exception { + CelNativeTypesExtensions extensions = CelExtensions.nativeTypes(TestNestedGenericsPojo.class); + Cel cel = + CelFactory.plannerCelBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addCompilerLibraries(extensions) + .addRuntimeLibraries(extensions) + .addVar( + "pojo", StructTypeReference.create(TestNestedGenericsPojo.class.getCanonicalName())) + .build(); + + TestNestedSimplePojo simplePojo = new TestNestedSimplePojo(); + TestNestedGenericsPojo pojo = new TestNestedGenericsPojo(); + pojo.nestedList = ImmutableList.of(ImmutableList.of(simplePojo)); + + boolean result = + (boolean) + cel.createProgram(cel.compile("pojo.nestedList[0][0].value == 'nested'").getAst()) + .eval(ImmutableMap.of("pojo", pojo)); + + assertThat(result).isTrue(); + } + + @Test + public void nativeTypes_concreteCollectionInstantiation_success() throws Exception { + TestCustomCollectionPojo result = + (TestCustomCollectionPojo) + eval("TestCustomCollectionPojo{customList: ['a', 'b'], customMap: {'key': 1}}"); + + assertThat(result).isNotNull(); + assertThat(result.customList).containsExactly("a", "b"); + assertThat(result.customMap).containsEntry("key", 1L); + } + + @Test + public void nativeTypes_getterFieldTypeMismatch_readOnly() throws Exception { + CelAbstractSyntaxTree ast = + CEL.compile("TestGetterFieldTypeMismatchPojo{mismatchField: 'hello'}").getAst(); + + CelRuntime.Program program = CEL.createProgram(ast); + CelEvaluationException exception = + assertThrows(CelEvaluationException.class, () -> program.eval(ImmutableMap.of())); + + assertThat(exception.getMessage()).contains("Failed to create instance"); + } + + public static class TestAllTypesPublicFieldsPojo { + public void doNothing() {} + + public String getA() { + return "a"; + } + + public String get() { + return "get"; + } + + public boolean boolVal; + public String stringVal; + public long int64Val; + public int int32Val; + public double doubleVal; + public float floatVal; + public byte[] bytesVal; + public Duration durationVal; + public Instant timestampVal; + public TestNestedType nestedVal; + public List listVal; + public Map mapVal; + + public Boolean boolObjVal; + public Integer intObjVal; + public Long longObjVal; + public UnsignedLong uintVal; + public Float floatObjVal; + public Double doubleObjVal; + public Optional optionalVal; + public Optional optionalNestedVal; + public Map mapIntVal; + public List> nestedListVal; + public CelByteString celBytesVal = CelByteString.of(new byte[] {1, 2, 3}); + public TestAbstractPojo abstractPojo; + + public String getInvalidParam(String param) { + return "invalid"; + } + + public String isInvalidString() { + return "invalid"; + } + } + + public static class PojoWithCustomMap { + public Map mapVal; + } + + public static class TestNestedType { + public String value; + } + + static class TestPackagePrivatePojo { + public String value; + } + + static class TestPackagePrivateWithGetterPojo { + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + public static class TestPrivateConstructorPojo { + public String value; + + private TestPrivateConstructorPojo() { + this.value = "default"; + } + } + + public static class TestPrecedencePojo { + public int value = 1; + + public String getValue() { + return "hello"; + } + } + + static final class TestGetterSetterPojo { + private String value; + private boolean active; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + } + + public static final class TestUnsupportedSetPojo { + public Set strings; + } + + public static final class TestDeepConversionPojo { + public List ints; + public Map floats; + } + + public static final class TestMissingNoArgConstructorPojo { + public String value; + + public TestMissingNoArgConstructorPojo(String value) { + this.value = value; + } + } + + public static class TestRefValFieldType { + public Optional optionalName; + public int intVal; + public Instant time; + } + + public static class ComprehensiveTestNestedType { + public List nestedListVal; + public Map nestedMapVal; + public String customName; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ComprehensiveTestNestedType)) { + return false; + } + ComprehensiveTestNestedType that = (ComprehensiveTestNestedType) o; + return Objects.equals(nestedListVal, that.nestedListVal) + && Objects.equals(nestedMapVal, that.nestedMapVal) + && Objects.equals(customName, that.customName); + } + + @Override + public int hashCode() { + return Objects.hash(nestedListVal, nestedMapVal, customName); + } + } + + public static class TestNestedSliceType { + public String value; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof TestNestedSliceType)) { + return false; + } + TestNestedSliceType that = (TestNestedSliceType) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + } + + public static class TestMapVal { + public String value; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof TestMapVal)) { + return false; + } + TestMapVal that = (TestMapVal) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + } + + public static class ComprehensiveTestAllTypes { + public ComprehensiveTestNestedType nestedVal; + public ComprehensiveTestNestedType nestedStructVal; + public boolean boolVal; + public byte[] bytesVal; + public Duration durationVal; + public double doubleVal; + public float floatVal; + public int int32Val; + public long int64Val; + public String stringVal; + public Instant timestampVal; + public long uint32Val; + public long uint64Val; + public List listVal; + public List arrayVal; + public byte[] bytesArrayVal; + public Map mapVal; + public List customSliceVal; + public Map customMapVal; + public String customName; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ComprehensiveTestAllTypes)) { + return false; + } + ComprehensiveTestAllTypes that = (ComprehensiveTestAllTypes) o; + return boolVal == that.boolVal + && doubleVal == that.doubleVal + && floatVal == that.floatVal + && int32Val == that.int32Val + && int64Val == that.int64Val + && uint32Val == that.uint32Val + && uint64Val == that.uint64Val + && Objects.equals(nestedVal, that.nestedVal) + && Objects.equals(nestedStructVal, that.nestedStructVal) + && Arrays.equals(bytesVal, that.bytesVal) + && Objects.equals(durationVal, that.durationVal) + && Objects.equals(stringVal, that.stringVal) + && Objects.equals(timestampVal, that.timestampVal) + && Objects.equals(listVal, that.listVal) + && Objects.equals(arrayVal, that.arrayVal) + && Arrays.equals(bytesArrayVal, that.bytesArrayVal) + && Objects.equals(mapVal, that.mapVal) + && Objects.equals(customSliceVal, that.customSliceVal) + && Objects.equals(customMapVal, that.customMapVal) + && Objects.equals(customName, that.customName); + } + + @Override + public int hashCode() { + int result = + Objects.hash( + nestedVal, + nestedStructVal, + boolVal, + durationVal, + doubleVal, + floatVal, + int32Val, + int64Val, + stringVal, + timestampVal, + uint32Val, + uint64Val, + listVal, + arrayVal, + mapVal, + customSliceVal, + customMapVal, + customName); + result = 31 * result + Arrays.hashCode(bytesVal); + result = 31 * result + Arrays.hashCode(bytesArrayVal); + return result; + } + } + + public static final class TestPrivateFieldPojo { + // Intentionally unread to test private fields are not exposed + @SuppressWarnings("UnusedVariable") + private String secret; + } + + public static class TestPrefixLessGetterPojo { + private String value = "hello"; + + public String value() { + return value; + } + } + + public static class TestParentPojo { + private String parentValue = "parent"; + private String standardValue = "standard"; + + public String parentValue() { + return parentValue; + } + + public String getStandardValue() { + return standardValue; + } + } + + public static class TestChildPojo extends TestParentPojo { + private String childValue = "child"; + + public String childValue() { + return childValue; + } + } + + // Intentionally violating style guide to test special decapitalization. + @SuppressWarnings("IdentifierName") + public static class TestURLPojo { + public String getURL() { + return "https://google.com"; + } + } + + public static class TestDoubleMapKeyPojo { + public Map map; + } + + public static class TestWildcardPojo { + public List values; + } + + public static class TestArrayPojo { + public String[] values; + } + + public static class TestOptionalUrlPojo { + public Optional optionalUrl; + } + + public abstract static class TestAbstractPojo { + public String value; + } + + public static class TestCircularA { + public TestCircularB b; + } + + public static class TestCircularB { + public TestCircularA a; + } + + public static class CustomListImplementation extends ArrayList {} + + public static class CustomMapImplementation extends HashMap {} + + public static class TestCustomCollectionPojo { + public CustomListImplementation customList; + public CustomMapImplementation customMap; + } + + @SuppressWarnings("Immutable") + static final class TestCustomStructValuePojo extends StructValue { + private final ImmutableMap fields; + + public TestCustomStructValuePojo(ImmutableMap fields) { + this.fields = fields; + } + + @Override + public Object value() { + return this; + } + + @Override + public boolean isZeroValue() { + for (Object val : fields.values()) { + if (val != null && !val.equals("") && !val.equals(0L)) { + return false; + } + } + return true; + } + + @Override + public CelType celType() { + return StructTypeReference.create(TestCustomStructValuePojo.class.getCanonicalName()); + } + + @Override + public Optional find(String field) { + return Optional.ofNullable(fields.get(field)); + } + + @Override + public Object select(String field) { + Object val = fields.get(field); + if (val == null) { + throw new NoSuchElementException("Field not found: " + field); + } + return val; + } + } + + public static class TestStaticMembersPojo { + public static final String STATIC_FIELD = "static_value"; + + public static String getStaticGetter() { + return "static_getter_value"; + } + + public static String staticProperty() { + return "static_property_value"; + } + + public String instanceField = "instance_value"; + } + + public static class TestNestedGenericsPojo { + public List> nestedList; + public Map> nestedMap; + } + + public static class TestNestedSimplePojo { + public String value = "nested"; + } + + public static class TestGetterFieldTypeMismatchPojo { + public int mismatchField = 10; + + public String getMismatchField() { + return "mismatch"; + } + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java index 0000ad764..561e25f7f 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java @@ -192,7 +192,9 @@ private static Object applyQualifiers( // Avoid enhanced for loop to prevent UnmodifiableIterator from being allocated for (int i = 0; i < qualifiers.size(); i++) { - obj = qualifiers.get(i).qualify(obj); + Qualifier element = qualifiers.get(i); + obj = element.qualify(obj); + obj = celValueConverter.toRuntimeValue(obj); } return celValueConverter.maybeUnwrap(obj); diff --git a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java index addbeb4d0..38f733c79 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java @@ -42,7 +42,9 @@ public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { // Avoid enhanced for loop to prevent UnmodifiableIterator from being allocated for (int i = 0; i < qualifiers.size(); i++) { - obj = qualifiers.get(i).qualify(obj); + Qualifier element = qualifiers.get(i); + obj = element.qualify(obj); + obj = celValueConverter.toRuntimeValue(obj); } return celValueConverter.maybeUnwrap(obj); From 7bf3e04351570ce8a9e49e947aae18c335abb2bc Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 14 May 2026 16:32:52 -0700 Subject: [PATCH 075/204] Optimize list/map adaptations PiperOrigin-RevId: 915678440 --- .../java/dev/cel/common/values/BUILD.bazel | 32 +++++--- .../cel/common/values/CelPreAdaptedList.java | 49 ++++++++++++ .../cel/common/values/CelValueConverter.java | 74 +++++++++++++++---- .../common/values/ProtoCelValueConverter.java | 12 +++ 4 files changed, 144 insertions(+), 23 deletions(-) create mode 100644 common/src/main/java/dev/cel/common/values/CelPreAdaptedList.java diff --git a/common/src/main/java/dev/cel/common/values/BUILD.bazel b/common/src/main/java/dev/cel/common/values/BUILD.bazel index d572bb2bc..5ccc498fd 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -60,7 +60,6 @@ java_library( deps = [ "//common/values", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", ], ) @@ -72,7 +71,6 @@ cel_android_library( deps = [ "//common/values:values_android", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven_android//:com_google_guava_guava", ], ) @@ -118,7 +116,6 @@ java_library( deps = [ ":values", "//common/annotations", - "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:org_jspecify_jspecify", ], @@ -134,12 +131,31 @@ cel_android_library( deps = [ ":values_android", "//common/annotations", - "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:org_jspecify_jspecify", "@maven_android//:com_google_guava_guava", ], ) +java_library( + name = "preadapted_list", + srcs = [ + "CelPreAdaptedList.java", + ], + tags = [ + ], + deps = ["//common/annotations"], +) + +cel_android_library( + name = "preadapted_list_android", + srcs = [ + "CelPreAdaptedList.java", + ], + tags = [ + ], + deps = ["//common/annotations"], +) + java_library( name = "values", srcs = CEL_VALUES_SOURCES, @@ -148,6 +164,7 @@ java_library( deps = [ ":cel_byte_string", ":cel_value", + ":preadapted_list", "//:auto_value", "//common/annotations", "//common/types", @@ -198,6 +215,7 @@ cel_android_library( deps = [ ":cel_byte_string", ":cel_value_android", + ":preadapted_list_android", "//:auto_value", "//common/annotations", "//common/types:type_providers_android", @@ -226,7 +244,6 @@ java_library( ], deps = [ ":cel_byte_string", - ":values", "//common/annotations", "//common/internal:proto_time_utils", "//common/internal:well_known_proto", @@ -261,6 +278,7 @@ java_library( ], deps = [ ":base_proto_cel_value_converter", + ":preadapted_list", ":values", "//:auto_value", "//common:options", @@ -273,7 +291,6 @@ java_library( "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", - "@maven//:org_jspecify_jspecify", ], ) @@ -316,8 +333,6 @@ java_library( "//protobuf:cel_lite_descriptor", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", - "@maven//:com_google_protobuf_protobuf_java", - "@maven//:org_jspecify_jspecify", "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) @@ -343,7 +358,6 @@ cel_android_library( "//protobuf:cel_lite_descriptor", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", - "@maven//:org_jspecify_jspecify", "@maven_android//:com_google_guava_guava", "@maven_android//:com_google_protobuf_protobuf_javalite", ], diff --git a/common/src/main/java/dev/cel/common/values/CelPreAdaptedList.java b/common/src/main/java/dev/cel/common/values/CelPreAdaptedList.java new file mode 100644 index 000000000..c0ff25e45 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/CelPreAdaptedList.java @@ -0,0 +1,49 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.values; + +import dev.cel.common.annotations.Internal; +import java.util.AbstractList; +import java.util.List; +import java.util.RandomAccess; + +/** + * A zero-allocation view over a list we know is already adapted. + * + *

This class purely exists as an optimization scheme to avoid redundant collection traversals in + * {@link CelValueConverter}, and is not intended for general use. + */ +@Internal +final class CelPreAdaptedList extends AbstractList implements RandomAccess { + private final List delegate; + + private CelPreAdaptedList(List delegate) { + this.delegate = delegate; + } + + static CelPreAdaptedList wrap(List safeList) { + return new CelPreAdaptedList<>(safeList); + } + + @Override + public E get(int index) { + return delegate.get(index); + } + + @Override + public int size() { + return delegate.size(); + } +} diff --git a/common/src/main/java/dev/cel/common/values/CelValueConverter.java b/common/src/main/java/dev/cel/common/values/CelValueConverter.java index 89f5ab100..20deef1d3 100644 --- a/common/src/main/java/dev/cel/common/values/CelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/CelValueConverter.java @@ -20,8 +20,11 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.common.annotations.Internal; import java.util.Collection; +import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.RandomAccess; import java.util.function.Function; /** @@ -53,16 +56,11 @@ public static CelValueConverter getDefaultInstance() { *

The value may be a {@link CelValue}, a {@link Collection} or a {@link Map}. */ public Object maybeUnwrap(Object value) { - if (value instanceof CelValue) { - return unwrap((CelValue) value); + if (value instanceof CelValue || value instanceof CelPreAdaptedList) { + return value instanceof CelValue ? unwrap((CelValue) value) : value; } - Object mapped = mapContainer(value, maybeUnwrapFunction); - if (mapped != value) { - return mapped; - } - - return value; + return mapContainer(value, maybeUnwrapFunction); } /** @@ -70,6 +68,34 @@ public Object maybeUnwrap(Object value) { * Returns the original value if it's not a supported container. */ protected Object mapContainer(Object value, Function mapper) { + + // Zero allocation path for standard lists that support O(1) indexing + // Generally, protobuf lists (backed by arrays) fall into this category + if (value instanceof List && value instanceof RandomAccess) { + List list = (List) value; + for (int i = 0; i < list.size(); i++) { + Object element = list.get(i); + Object mapped = mapper.apply(element); + + if (mapped != element) { + ImmutableList.Builder builder = + ImmutableList.builderWithExpectedSize(list.size()); + for (int j = 0; j < i; j++) { + builder.add(list.get(j)); + } + builder.add(mapped); + for (int j = i + 1; j < list.size(); j++) { + builder.add(mapper.apply(list.get(j))); + } + return builder.build(); + } + } + + // Zero allocations if unmodified + return value; + } + + // Fallback for lists that are unordered if (value instanceof Collection) { Collection collection = (Collection) value; ImmutableList.Builder builder = @@ -82,12 +108,32 @@ protected Object mapContainer(Object value, Function mapper) { if (value instanceof Map) { Map map = (Map) value; - ImmutableMap.Builder builder = - ImmutableMap.builderWithExpectedSize(map.size()); - for (Map.Entry entry : map.entrySet()) { - builder.put(mapper.apply(entry.getKey()), mapper.apply(entry.getValue())); + Iterator> iterator = map.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + Object mappedKey = mapper.apply(entry.getKey()); + Object mappedValue = mapper.apply(entry.getValue()); + + if (mappedKey != entry.getKey() || mappedValue != entry.getValue()) { + ImmutableMap.Builder builder = + ImmutableMap.builderWithExpectedSize(map.size()); + + for (Map.Entry prevEntry : map.entrySet()) { + if (prevEntry.getKey() == entry.getKey()) { + break; + } + builder.put(mapper.apply(prevEntry.getKey()), mapper.apply(prevEntry.getValue())); + } + builder.put(mappedKey, mappedValue); + while (iterator.hasNext()) { + Map.Entry nextEntry = iterator.next(); + builder.put(mapper.apply(nextEntry.getKey()), mapper.apply(nextEntry.getValue())); + } + return builder.buildOrThrow(); + } } - return builder.buildOrThrow(); + return value; } return value; @@ -96,7 +142,7 @@ protected Object mapContainer(Object value, Function mapper) { public Object toRuntimeValue(Object value) { Preconditions.checkNotNull(value); - if (value instanceof CelValue) { + if (value instanceof CelValue || value instanceof CelPreAdaptedList) { return value; } diff --git a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java index 565c65438..948df759c 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java @@ -167,6 +167,18 @@ public Object fromProtoMessageFieldToCelValue(Message message, FieldDescriptor f break; } + if (fieldDescriptor.isRepeated()) { + switch (fieldDescriptor.getType()) { + case INT64: + case BOOL: + case STRING: + case DOUBLE: + return CelPreAdaptedList.wrap((List) result); + default: + break; + } + } + return toRuntimeValue(result); } From 0837a5190501466d3b4545c72f26ef4e8dbbedef Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 14 May 2026 18:21:01 -0700 Subject: [PATCH 076/204] Fix double qualification in NamespacedAttribute PiperOrigin-RevId: 915718942 --- .../cel/runtime/planner/NamespacedAttribute.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java index 561e25f7f..95a4489fd 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java @@ -171,19 +171,15 @@ private GlobalResolver unwrapToNonLocal(GlobalResolver resolver) { @Override public NamespacedAttribute addQualifier(Qualifier qualifier) { - ImmutableMap.Builder attributesBuilder = ImmutableMap.builder(); - CelAttribute.Qualifier celQualifier = CelAttribute.Qualifier.fromGeneric(qualifier.value()); - - for (Map.Entry entry : candidateAttributes.entrySet()) { - attributesBuilder.put(entry.getKey(), entry.getValue().qualify(celQualifier)); - } - return new NamespacedAttribute( typeProvider, celValueConverter, - attributesBuilder.buildOrThrow(), + candidateAttributes, disambiguateNames, - ImmutableList.builder().addAll(qualifiers).add(qualifier).build()); + ImmutableList.builderWithExpectedSize(qualifiers.size() + 1) + .addAll(qualifiers) + .add(qualifier) + .build()); } private static Object applyQualifiers( From bfc4fdfa9a846c493028ca986c06eda88b2a207e Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 14 May 2026 19:43:25 -0700 Subject: [PATCH 077/204] Refactor native extensions to separately hold type references PiperOrigin-RevId: 915744276 --- .../extensions/CelNativeTypesExtensions.java | 75 ++++++++++--------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java index fd579a3bc..ae9483f7c 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java @@ -356,12 +356,12 @@ private static Optional buildPropertyAccessor( if (getter != null) { propType = getter.getReturnType(); genericPropType = getter.getGenericReturnType(); - discoverCustomTypes(genericPropType, queue); + queue.addAll(TypeReferenceCollector.collect(genericPropType)); compiledGetter = compileGetter(getter); } else if (field != null) { propType = field.getType(); genericPropType = field.getGenericType(); - discoverCustomTypes(genericPropType, queue); + queue.addAll(TypeReferenceCollector.collect(genericPropType)); compiledGetter = compileFieldGetter(field); } @@ -386,46 +386,53 @@ private static Optional buildPropertyAccessor( /** * Recursively explores a {@link Type} and discovers any transitive, user-defined custom POJO - * classes nested inside multi-level generic collections, lists, maps, or optionals, pushing - * them into the scanning discovery queue. + * classes nested inside multi-level generic collections, lists, maps, or optionals, collecting + * them for subsequent properties discovery. * *

"Custom types" are any public non-primitive, non-built-in Java classes that require * explicit properties reflective scanning and mapping to a CEL StructType schema (as opposed to * standard built-in types like {@code String}, {@code List}, or {@code Map}). - * - * @param type The Java type token or parameterized collection type to recursively unpack. - * @param queue The central scanning queue where newly discovered custom classes are pushed for - * subsequent properties discovery. */ - private static void discoverCustomTypes(Type type, Queue> queue) { - Preconditions.checkNotNull(type, "Type to discover cannot be null."); - Preconditions.checkNotNull(queue, "Queue cannot be null."); - TypeToken token = TypeToken.of(type); - Class rawType = token.getRawType(); - - if (List.class.isAssignableFrom(rawType)) { - Type elementType = ReflectionUtil.resolveGenericParameter(token, List.class, 0); - discoverCustomTypes(elementType, queue); - return; - } + private static final class TypeReferenceCollector { + private final Set> collectedTypes = new HashSet<>(); + + /** + * Traverses the given type and returns an immutable set of all custom POJO classes found. + * + * @param type The Java type token or parameterized collection type to recursively unpack. + */ + private static ImmutableSet> collect(Type type) { + TypeReferenceCollector collector = new TypeReferenceCollector(); + collector.discover(type); + return ImmutableSet.copyOf(collector.collectedTypes); + } + + private void discover(Type type) { + Preconditions.checkNotNull(type, "Type to discover cannot be null."); + TypeToken token = TypeToken.of(type); + Class rawType = token.getRawType(); + + if (List.class.isAssignableFrom(rawType)) { + discover(ReflectionUtil.resolveGenericParameter(token, List.class, 0)); + return; + } - if (Map.class.isAssignableFrom(rawType)) { - Type keyType = ReflectionUtil.resolveGenericParameter(token, Map.class, 0); - Type valueType = ReflectionUtil.resolveGenericParameter(token, Map.class, 1); - discoverCustomTypes(keyType, queue); - discoverCustomTypes(valueType, queue); - return; - } + if (Map.class.isAssignableFrom(rawType)) { + discover(ReflectionUtil.resolveGenericParameter(token, Map.class, 0)); + discover(ReflectionUtil.resolveGenericParameter(token, Map.class, 1)); + return; + } - if (rawType == Optional.class) { - Type optionalType = ReflectionUtil.resolveGenericParameter(token, Optional.class, 0); - discoverCustomTypes(optionalType, queue); - return; - } + if (rawType == Optional.class) { + discover(ReflectionUtil.resolveGenericParameter(token, Optional.class, 0)); + return; + } - if (!JAVA_TO_DEFAULT_VALUE_MAP.containsKey(rawType) - && Modifier.isPublic(rawType.getModifiers())) { - queue.add(rawType); + // Custom types are non-builtin, public classes + if (!JAVA_TO_DEFAULT_VALUE_MAP.containsKey(rawType) + && Modifier.isPublic(rawType.getModifiers())) { + collectedTypes.add(rawType); + } } } From c57d9d02259795c761e82f8870303b93a7a672c8 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 15 May 2026 16:48:33 -0700 Subject: [PATCH 078/204] Release 0.13.0 PiperOrigin-RevId: 916246350 --- MODULE.bazel | 2 +- README.md | 4 ++-- publish/cel_version.bzl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 895715a5f..6689158c6 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -46,7 +46,7 @@ TRUTH_VERSION = "1.4.4" PROTOBUF_JAVA_VERSION = "4.33.5" -CEL_VERSION = "0.12.0" +CEL_VERSION = "0.13.0" # Compile only artifacts [ diff --git a/README.md b/README.md index 40bd9deac..78e38961f 100644 --- a/README.md +++ b/README.md @@ -55,14 +55,14 @@ CEL-Java is available in Maven Central Repository. [Download the JARs here][8] o dev.cel cel - 0.12.0 + 0.13.0 ``` **Gradle** ```gradle -implementation 'dev.cel:cel:0.12.0' +implementation 'dev.cel:cel:0.13.0' ``` Then run this example: diff --git a/publish/cel_version.bzl b/publish/cel_version.bzl index b40addd73..70fa1a010 100644 --- a/publish/cel_version.bzl +++ b/publish/cel_version.bzl @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. """Maven artifact version for CEL.""" -CEL_VERSION = "0.12.0" +CEL_VERSION = "0.13.0" From 7f1948d1cffca2198dbd1ad98f5a95765adbcfd6 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 15 May 2026 18:14:15 -0700 Subject: [PATCH 079/204] Deprecate enableCelValue option PiperOrigin-RevId: 916273030 --- common/src/main/java/dev/cel/common/CelOptions.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/common/src/main/java/dev/cel/common/CelOptions.java b/common/src/main/java/dev/cel/common/CelOptions.java index c39e0fea8..d9c2dd818 100644 --- a/common/src/main/java/dev/cel/common/CelOptions.java +++ b/common/src/main/java/dev/cel/common/CelOptions.java @@ -17,7 +17,6 @@ import com.google.auto.value.AutoValue; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; -import dev.cel.common.annotations.Beta; /** * Options to configure how the CEL parser, type-checker, and evaluator behave. @@ -434,13 +433,10 @@ public abstract static class Builder { public abstract Builder enableUnknownTracking(boolean value); /** - * Enables the usage of {@code CelValue} for the runtime. It is a native value representation of - * CEL that wraps Java native objects, and comes with extended capabilities, such as allowing - * value constructs not understood by CEL (ex: POJOs). - * - *

Warning: This option is experimental. + * @deprecated Do not use, this flag will be removed in the future. Use the planner based + * runtime instead, which supports CelValue by default. */ - @Beta + @Deprecated public abstract Builder enableCelValue(boolean value); /** From 919ae0bca3f445cb383cb0343635acbbd3f636a9 Mon Sep 17 00:00:00 2001 From: Stephen Roberts Date: Wed, 20 May 2026 12:07:43 -0700 Subject: [PATCH 080/204] Internal change PiperOrigin-RevId: 918580020 --- runtime/BUILD.bazel | 1 - 1 file changed, 1 deletion(-) diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index f4a150ff3..d1cb99b64 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -354,6 +354,5 @@ java_library( java_library( name = "partial_vars", - visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime:partial_vars"], ) From 1647b3b660a803ee4bffadc658673dc560d85c63 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 22 May 2026 14:54:40 -0700 Subject: [PATCH 081/204] Remove duplicate test policies and use the ones from cel-policy as SoT PiperOrigin-RevId: 919872992 --- .../src/test/java/dev/cel/policy/BUILD.bazel | 4 + .../cel/policy/CelPolicyCompilerImplTest.java | 43 +++-- .../java/dev/cel/policy/PolicyTestHelper.java | 76 ++++++-- repositories.bzl | 4 +- .../dev/cel/testing/testrunner/BUILD.bazel | 37 ++-- .../policy/compile_errors/config.yaml | 52 ----- .../compile_errors/expected_errors.baseline | 30 --- .../policy/compile_errors/policy.yaml | 45 ----- .../expected_errors.baseline | 6 + .../expected_errors.baseline | 6 + .../config.yaml | 22 --- .../expected_errors.baseline | 6 - .../policy.yaml | 23 --- .../config.yaml | 22 --- .../expected_errors.baseline | 6 - .../policy.yaml | 37 ---- .../resources/policy/context_pb/config.yaml | 19 -- .../resources/policy/context_pb/policy.yaml | 25 --- .../policy/context_pb/tests.textproto | 19 -- .../resources/policy/context_pb/tests.yaml | 35 ---- .../expected_errors.baseline | 3 + .../policy/errors_unreachable/config.yaml | 54 ------ .../expected_errors.baseline | 6 - .../policy/errors_unreachable/policy.yaml | 39 ---- .../policy/import/expected_errors.baseline | 6 + .../expected_errors.baseline | 6 + .../src/test/resources/policy/k8s/config.yaml | 33 ---- .../src/test/resources/policy/k8s/policy.yaml | 36 ---- .../src/test/resources/policy/k8s/tests.yaml | 32 ---- .../test/resources/policy/limits/config.yaml | 22 --- .../test/resources/policy/limits/policy.yaml | 50 ----- .../test/resources/policy/limits/tests.yaml | 42 ----- .../resources/policy/nested_rule/config.yaml | 22 --- .../resources/policy/nested_rule/policy.yaml | 38 ---- .../nested_rule/testrunner_tests.textproto | 79 -------- .../policy/nested_rule/testrunner_tests.yaml | 47 ----- .../resources/policy/nested_rule/tests.yaml | 41 ---- .../resources/policy/nested_rule2/config.yaml | 22 --- .../resources/policy/nested_rule2/policy.yaml | 40 ---- .../resources/policy/nested_rule2/tests.yaml | 52 ----- .../resources/policy/nested_rule3/config.yaml | 22 --- .../resources/policy/nested_rule3/policy.yaml | 39 ---- .../resources/policy/nested_rule3/tests.yaml | 52 ----- .../resources/policy/nested_rule4/config.yaml | 19 -- .../resources/policy/nested_rule4/policy.yaml | 24 --- .../resources/policy/nested_rule4/tests.yaml | 30 --- .../resources/policy/nested_rule5/config.yaml | 19 -- .../resources/policy/nested_rule5/policy.yaml | 30 --- .../resources/policy/nested_rule5/tests.yaml | 42 ----- .../resources/policy/nested_rule6/config.yaml | 19 -- .../resources/policy/nested_rule6/policy.yaml | 28 --- .../resources/policy/nested_rule6/tests.yaml | 24 --- .../resources/policy/nested_rule7/config.yaml | 19 -- .../resources/policy/nested_rule7/policy.yaml | 29 --- .../resources/policy/nested_rule7/tests.yaml | 42 ----- .../src/test/resources/policy/pb/config.yaml | 23 --- .../src/test/resources/policy/pb/policy.yaml | 36 ---- .../src/test/resources/policy/pb/tests.yaml | 34 ---- .../policy/required_labels/config.yaml | 32 ---- .../policy/required_labels/policy.yaml | 32 ---- .../policy/required_labels/tests.yaml | 80 -------- .../restricted_destinations/config.yaml | 52 ----- .../restricted_destinations/policy.yaml | 42 ----- .../policy/restricted_destinations/tests.yaml | 122 ------------ .../policy/syntax/expected_errors.baseline | 12 ++ .../expected_errors.baseline | 6 + .../unreachable/expected_errors.baseline | 6 + testing/testrunner/cel_java_test.bzl | 178 ++++++++---------- 68 files changed, 244 insertions(+), 2036 deletions(-) delete mode 100644 testing/src/test/resources/policy/compile_errors/config.yaml delete mode 100644 testing/src/test/resources/policy/compile_errors/expected_errors.baseline delete mode 100644 testing/src/test/resources/policy/compile_errors/policy.yaml create mode 100644 testing/src/test/resources/policy/compose_conflicting_output/expected_errors.baseline create mode 100644 testing/src/test/resources/policy/compose_conflicting_subrule/expected_errors.baseline delete mode 100644 testing/src/test/resources/policy/compose_errors_conflicting_output/config.yaml delete mode 100644 testing/src/test/resources/policy/compose_errors_conflicting_output/expected_errors.baseline delete mode 100644 testing/src/test/resources/policy/compose_errors_conflicting_output/policy.yaml delete mode 100644 testing/src/test/resources/policy/compose_errors_conflicting_subrule/config.yaml delete mode 100644 testing/src/test/resources/policy/compose_errors_conflicting_subrule/expected_errors.baseline delete mode 100644 testing/src/test/resources/policy/compose_errors_conflicting_subrule/policy.yaml delete mode 100644 testing/src/test/resources/policy/context_pb/config.yaml delete mode 100644 testing/src/test/resources/policy/context_pb/policy.yaml delete mode 100644 testing/src/test/resources/policy/context_pb/tests.textproto delete mode 100644 testing/src/test/resources/policy/context_pb/tests.yaml create mode 100644 testing/src/test/resources/policy/duplicate_variable/expected_errors.baseline delete mode 100644 testing/src/test/resources/policy/errors_unreachable/config.yaml delete mode 100644 testing/src/test/resources/policy/errors_unreachable/expected_errors.baseline delete mode 100644 testing/src/test/resources/policy/errors_unreachable/policy.yaml create mode 100644 testing/src/test/resources/policy/import/expected_errors.baseline create mode 100644 testing/src/test/resources/policy/incompatible_outputs/expected_errors.baseline delete mode 100644 testing/src/test/resources/policy/k8s/config.yaml delete mode 100644 testing/src/test/resources/policy/k8s/policy.yaml delete mode 100644 testing/src/test/resources/policy/k8s/tests.yaml delete mode 100644 testing/src/test/resources/policy/limits/config.yaml delete mode 100644 testing/src/test/resources/policy/limits/policy.yaml delete mode 100644 testing/src/test/resources/policy/limits/tests.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule/config.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule/policy.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule/testrunner_tests.textproto delete mode 100644 testing/src/test/resources/policy/nested_rule/testrunner_tests.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule/tests.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule2/config.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule2/policy.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule2/tests.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule3/config.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule3/policy.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule3/tests.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule4/config.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule4/policy.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule4/tests.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule5/config.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule5/policy.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule5/tests.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule6/config.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule6/policy.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule6/tests.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule7/config.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule7/policy.yaml delete mode 100644 testing/src/test/resources/policy/nested_rule7/tests.yaml delete mode 100644 testing/src/test/resources/policy/pb/config.yaml delete mode 100644 testing/src/test/resources/policy/pb/policy.yaml delete mode 100644 testing/src/test/resources/policy/pb/tests.yaml delete mode 100644 testing/src/test/resources/policy/required_labels/config.yaml delete mode 100644 testing/src/test/resources/policy/required_labels/policy.yaml delete mode 100644 testing/src/test/resources/policy/required_labels/tests.yaml delete mode 100644 testing/src/test/resources/policy/restricted_destinations/config.yaml delete mode 100644 testing/src/test/resources/policy/restricted_destinations/policy.yaml delete mode 100644 testing/src/test/resources/policy/restricted_destinations/tests.yaml create mode 100644 testing/src/test/resources/policy/syntax/expected_errors.baseline create mode 100644 testing/src/test/resources/policy/undeclared_reference/expected_errors.baseline create mode 100644 testing/src/test/resources/policy/unreachable/expected_errors.baseline diff --git a/policy/src/test/java/dev/cel/policy/BUILD.bazel b/policy/src/test/java/dev/cel/policy/BUILD.bazel index bc8a5d4b4..5fcfd5693 100644 --- a/policy/src/test/java/dev/cel/policy/BUILD.bazel +++ b/policy/src/test/java/dev/cel/policy/BUILD.bazel @@ -7,6 +7,9 @@ java_library( name = "tests", testonly = True, srcs = glob(["*.java"]), + data = [ + "@cel_policy//conformance:testdata", + ], resources = [ "//testing:policy_test_resources", ], @@ -39,6 +42,7 @@ java_library( "//runtime:function_binding", "//testing:cel_runtime_flavor", "//testing/protos:single_file_java_proto", + "@bazel_tools//tools/java/runfiles", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@maven//:com_google_guava_guava", "@maven//:com_google_testparameterinjector_test_parameter_injector", diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java index b4065b60c..e9c2afed5 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java @@ -17,10 +17,12 @@ import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.truth.Truth.assertThat; import static dev.cel.policy.PolicyTestHelper.readFromYaml; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.io.Resources; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameterValue; @@ -50,6 +52,7 @@ import dev.cel.testing.testdata.SingleFile; import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; import java.io.IOException; +import java.net.URL; import java.util.Map; import java.util.Optional; import org.junit.Test; @@ -112,9 +115,12 @@ public void compileYamlPolicy_withImportsOnNestedRules() throws Exception { public void compileYamlPolicy_containsCompilationError_throws( @TestParameter TestErrorYamlPolicy testCase) throws Exception { // Read config and produce an environment to compile policies - String configSource = testCase.readConfigYamlContent(); - CelEnvironment celEnvironment = ENVIRONMENT_PARSER.parse(configSource); - Cel cel = celEnvironment.extend(newCel(), CEL_OPTIONS); + Optional configSource = testCase.readConfigYamlContent(); + Cel baseCel = newCel(); + Cel cel = + configSource.isPresent() + ? ENVIRONMENT_PARSER.parse(configSource.get()).extend(baseCel, CEL_OPTIONS) + : baseCel; // Read the policy source String policySource = testCase.readPolicyYamlContent(); CelPolicy policy = POLICY_PARSER.parse(policySource, testCase.getPolicyFilePath()); @@ -509,10 +515,14 @@ private enum MultilineErrorTest { } private enum TestErrorYamlPolicy { - COMPILE_ERRORS("compile_errors"), - COMPOSE_ERRORS_CONFLICTING_OUTPUT("compose_errors_conflicting_output"), - COMPOSE_ERRORS_CONFLICTING_SUBRULE("compose_errors_conflicting_subrule"), - ERRORS_UNREACHABLE("errors_unreachable"); + COMPOSE_ERRORS_CONFLICTING_OUTPUT("compose_conflicting_output"), + COMPOSE_ERRORS_CONFLICTING_SUBRULE("compose_conflicting_subrule"), + ERRORS_UNREACHABLE("unreachable"), + DUPLICATE_VARIABLE("duplicate_variable"), + IMPORT("import"), + INCOMPATIBLE_OUTPUTS("incompatible_outputs"), + SYNTAX("syntax"), + UNDECLARED_REFERENCE("undeclared_reference"); private final String name; private final String policyFilePath; @@ -522,15 +532,26 @@ private String getPolicyFilePath() { } private String readPolicyYamlContent() throws IOException { - return readFromYaml(String.format("policy/%s/policy.yaml", name)); + return readFromYaml( + String.format( + "cel_policy/conformance/testdata/compile_errors/%s/policy.yaml", + name)); } - private String readConfigYamlContent() throws IOException { - return readFromYaml(String.format("policy/%s/config.yaml", name)); + private Optional readConfigYamlContent() throws IOException { + String rlocationPath = + String.format( + "cel_policy/conformance/testdata/compile_errors/%s/config.yaml", + name); + if (PolicyTestHelper.hasRunfile(rlocationPath)) { + return Optional.of(readFromYaml(rlocationPath)); + } + return Optional.empty(); } private String readExpectedErrorsBaseline() throws IOException { - return readFromYaml(String.format("policy/%s/expected_errors.baseline", name)); + URL url = Resources.getResource(String.format("policy/%s/expected_errors.baseline", name)); + return Resources.toString(url, UTF_8).trim(); } TestErrorYamlPolicy(String name) { diff --git a/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java b/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java index 6e918286b..3fe2e3322 100644 --- a/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java +++ b/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java @@ -18,9 +18,11 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Ascii; -import com.google.common.io.Resources; +import com.google.common.io.Files; +import com.google.devtools.build.runfiles.AutoBazelRepository; +import com.google.devtools.build.runfiles.Runfiles; +import java.io.File; import java.io.IOException; -import java.net.URL; import java.util.List; import java.util.Map; import org.yaml.snakeyaml.LoaderOptions; @@ -28,8 +30,11 @@ import org.yaml.snakeyaml.constructor.Constructor; /** Package-private class to assist with policy testing. */ +@AutoBazelRepository final class PolicyTestHelper { + private static final Runfiles runfiles = createRunfiles(); + enum TestYamlPolicy { NESTED_RULE( "nested_rule", @@ -74,11 +79,11 @@ enum TestYamlPolicy { "required_labels", true, "cel.@block([spec.labels.filter(@it:0:0, !(@it:0:0 in resource.labels)), spec.labels," - + " resource.labels, @index2.filter(@it:0:0, @it:0:0 in @index1 && @index1[@it:0:0] !=" - + " @index2[@it:0:0])], (@index0.size() > 0) ? optional.of(\"missing one or more" - + " required labels: [\"\" + @index0.join(\",\") + \"\"]\") : ((@index3.size() > 0) ?" - + " optional.of(\"invalid values provided on one or more labels: [\"\" +" - + " @index3.join(\",\") + \"\"]\") : optional.none()))"), + + " resource.labels.transformList(@it:0:1, @it2:0:1, @it:0:1 in @index1 && @it2:0:1 !=" + + " @index1[@it:0:1], @it:0:1)], (@index0.size() > 0) ? optional.of(\"missing one or" + + " more required labels: [\"\" + @index0.join(\"\", \"\") + \"\"]\") :" + + " ((@index2.size() > 0) ? optional.of(\"invalid values provided on one or more" + + " labels: [\"\" + @index2.join(\"\", \"\") + \"\"]\") : optional.none()))"), RESTRICTED_DESTINATIONS( "restricted_destinations", false, @@ -102,9 +107,10 @@ enum TestYamlPolicy { "cel.@block([spec.single_int32], (@index0 > 10) ? optional.of(\"invalid spec, got" + " single_int32=\" + string(@index0) + \", wanted <= 10\") : ((spec.standalone_enum ==" + " cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAR ||" - + " dev.cel.testing.testdata.proto3.StandaloneGlobalEnum.SGAR ==" - + " dev.cel.testing.testdata.proto3.StandaloneGlobalEnum.SGOO) ? optional.of(\"invalid" - + " spec, neither nested nor imported enums may refer to BAR\") : optional.none()))"), + + " cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAZ in" + + " spec.repeated_nested_enum || cel.expr.conformance.proto3.GlobalEnum.GAR ==" + + " cel.expr.conformance.proto3.GlobalEnum.GOO) ? optional.of(\"invalid spec, neither" + + " nested nor repeated enums may refer to BAR or BAZ\") : optional.none()))"), LIMITS( "limits", true, @@ -136,16 +142,23 @@ String getUnparsed() { } String readPolicyYamlContent() throws IOException { - return readFromYaml(String.format("policy/%s/policy.yaml", name)); + return readFromYaml( + String.format( + "cel_policy/conformance/testdata/%s/policy.yaml", name)); } String readConfigYamlContent() throws IOException { - return readFromYaml(String.format("policy/%s/config.yaml", name)); + return readFromYaml( + String.format( + "cel_policy/conformance/testdata/%s/config.yaml", name)); } PolicyTestSuite readTestYamlContent() throws IOException { Yaml yaml = new Yaml(new Constructor(PolicyTestSuite.class, new LoaderOptions())); - String testContent = readFile(String.format("policy/%s/tests.yaml", name)); + String testContent = + readFile( + String.format( + "cel_policy/conformance/testdata/%s/tests.yaml", name)); return yaml.load(testContent); } @@ -163,9 +176,18 @@ static String readFromYaml(String yamlPath) throws IOException { */ @VisibleForTesting public static final class PolicyTestSuite { + private String name; private String description; private List section; + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + public void setDescription(String description) { this.description = description; } @@ -258,12 +280,32 @@ public void setExpr(String expr) { } } - private static URL getResource(String path) { - return Resources.getResource(Ascii.toLowerCase(path)); + private static String readFile(String rlocationPath) throws IOException { + String resolvedPath = runfiles.rlocation(Ascii.toLowerCase(rlocationPath)); + if (resolvedPath == null) { + throw new IOException("Unmapped runfile path: " + rlocationPath); + } + File file = new File(resolvedPath); + if (!file.exists()) { + throw new IOException( + String.format( + "Runfile not found on disk at '%s' (unresolved path: '%s')", + resolvedPath, rlocationPath)); + } + return Files.asCharSource(file, UTF_8).read(); + } + + static boolean hasRunfile(String rlocationPath) { + String resolvedPath = runfiles.rlocation(Ascii.toLowerCase(rlocationPath)); + return resolvedPath != null && new File(resolvedPath).exists(); } - private static String readFile(String path) throws IOException { - return Resources.toString(getResource(path), UTF_8); + private static Runfiles createRunfiles() { + try { + return Runfiles.preload().withSourceRepository(AutoBazelRepository_PolicyTestHelper.NAME); + } catch (IOException e) { + throw new RuntimeException("Failed to initialize Runfiles", e); + } } private PolicyTestHelper() {} diff --git a/repositories.bzl b/repositories.bzl index 88f01019a..cbb7b3832 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -34,8 +34,8 @@ def bazel_common_dependency(): ) def cel_policy_dependency(): - cel_policy_tag = "569292f1c4eaa41894c1e37ee94eb146e284bcfa" - cel_policy_sha = "5a68318d906f6ce18492ad6f82b5f8bb083fd9d694cf567d399216c11da03157" + cel_policy_tag = "e4c38defbbf34dfff2dc448dc58e93a9733ae8b1" + cel_policy_sha = "46378e0d17a16465899f9fefc94c3d44e1f40aedd8a31c9c0b2b6198048eabd6" http_archive( name = "cel_policy", sha256 = cel_policy_sha, diff --git a/testing/src/test/java/dev/cel/testing/testrunner/BUILD.bazel b/testing/src/test/java/dev/cel/testing/testrunner/BUILD.bazel index a12654d2c..69c53e5d9 100644 --- a/testing/src/test/java/dev/cel/testing/testrunner/BUILD.bazel +++ b/testing/src/test/java/dev/cel/testing/testrunner/BUILD.bazel @@ -157,14 +157,13 @@ java_test( cel_java_test( name = "test_runner_sample_yaml", - cel_expr = "nested_rule/policy.yaml", + cel_expr = "@cel_policy//conformance:testdata/nested_rule/policy.yaml", proto_deps = [ "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", ], - test_data_path = "//testing/src/test/resources/policy", test_src = ":user_test", - test_suite = "nested_rule/testrunner_tests.yaml", + test_suite = "@cel_policy//conformance:testdata/nested_rule/tests.yaml", deps = [ "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", @@ -173,7 +172,7 @@ cel_java_test( cel_java_test( name = "unknown_set_yaml", - cel_expr = "nested_rule/policy.yaml", + cel_expr = "@cel_policy//conformance:testdata/nested_rule/policy.yaml", proto_deps = [ "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", @@ -211,15 +210,14 @@ cel_java_test( cel_java_test( name = "context_pb_user_test_runner_sample", - cel_expr = "context_pb/policy.yaml", - config = "context_pb/config.yaml", + cel_expr = "@cel_policy//conformance:testdata/context_pb/policy.yaml", + config = "@cel_policy//conformance:testdata/context_pb/config.yaml", proto_deps = [ "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", ], - test_data_path = "//testing/src/test/resources/policy", test_src = ":context_pb_user_test", - test_suite = "context_pb/tests.yaml", + test_suite = "@cel_policy//conformance:testdata/context_pb/tests.yaml", deps = [ "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", @@ -228,15 +226,14 @@ cel_java_test( cel_java_test( name = "additional_config_test_runner_sample", - cel_expr = "nested_rule/policy.yaml", - config = "nested_rule/config.yaml", + cel_expr = "@cel_policy//conformance:testdata/nested_rule/policy.yaml", + config = "@cel_policy//conformance:testdata/nested_rule/config.yaml", proto_deps = [ "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", ], - test_data_path = "//testing/src/test/resources/policy", test_src = ":env_config_user_test", - test_suite = "nested_rule/testrunner_tests.textproto", + test_suite = "@cel_policy//conformance:testdata/nested_rule/tests.textproto", deps = [ "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", @@ -245,14 +242,13 @@ cel_java_test( cel_java_test( name = "test_runner_sample", - cel_expr = "nested_rule/policy.yaml", + cel_expr = "@cel_policy//conformance:testdata/nested_rule/policy.yaml", proto_deps = [ "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", ], - test_data_path = "//testing/src/test/resources/policy", test_src = ":user_test", - test_suite = "nested_rule/testrunner_tests.textproto", + test_suite = "@cel_policy//conformance:testdata/nested_rule/tests.textproto", deps = [ "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", @@ -286,8 +282,8 @@ cel_java_test( cel_java_test( name = "context_message_user_test_runner_textproto_sample", - cel_expr = "context_pb/policy.yaml", - config = "context_pb/config.yaml", + cel_expr = "@cel_policy//conformance:testdata/context_pb/policy.yaml", + config = "@cel_policy//conformance:testdata/context_pb/config.yaml", proto_deps = [ "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", @@ -303,15 +299,14 @@ cel_java_test( cel_java_test( name = "context_pb_user_test_runner_textproto_sample", - cel_expr = "context_pb/policy.yaml", - config = "context_pb/config.yaml", + cel_expr = "@cel_policy//conformance:testdata/context_pb/policy.yaml", + config = "@cel_policy//conformance:testdata/context_pb/config.yaml", proto_deps = [ "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", ], - test_data_path = "//testing/src/test/resources/policy", test_src = ":context_pb_user_test", - test_suite = "context_pb/tests.textproto", + test_suite = "@cel_policy//conformance:testdata/context_pb/tests.textproto", deps = [ "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", diff --git a/testing/src/test/resources/policy/compile_errors/config.yaml b/testing/src/test/resources/policy/compile_errors/config.yaml deleted file mode 100644 index b9c8f9750..000000000 --- a/testing/src/test/resources/policy/compile_errors/config.yaml +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "labels" -extensions: - - name: "sets" -variables: - - name: "destination.ip" - type: - type_name: "string" - - name: "origin.ip" - type: - type_name: "string" - - name: "spec.restricted_destinations" - type: - type_name: "list" - params: - - type_name: "string" - - name: "spec.origin" - type: - type_name: "string" - - name: "request" - type: - type_name: "map" - params: - - type_name: "string" - - type_name: "dyn" - - name: "resource" - type: - type_name: "map" - params: - - type_name: "string" - - type_name: "dyn" -functions: - - name: "locationCode" - overloads: - - id: "locationCode_string" - args: - - type_name: "string" - return: - type_name: "string" diff --git a/testing/src/test/resources/policy/compile_errors/expected_errors.baseline b/testing/src/test/resources/policy/compile_errors/expected_errors.baseline deleted file mode 100644 index 850ecce9d..000000000 --- a/testing/src/test/resources/policy/compile_errors/expected_errors.baseline +++ /dev/null @@ -1,30 +0,0 @@ -ERROR: compile_errors/policy.yaml:19:5: Error configuring import: invalid qualified name: punc.Import!, wanted name of the form 'qualified.name' - | punc.Import! - | ....^ -ERROR: compile_errors/policy.yaml:20:10: Error configuring import: invalid qualified name: bad import, wanted name of the form 'qualified.name' - | - name: "bad import" - | .........^ -ERROR: compile_errors/policy.yaml:24:19: undeclared reference to 'spec' (in container '') - | expression: spec.labels - | ..................^ -ERROR: compile_errors/policy.yaml:26:50: mismatched input 'resource' expecting {'==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', ')', '.', '-', '?', '+', '*', '/', '%%'} - | expression: variables.want.filter(l, !(lin resource.labels)) - | .................................................^ -ERROR: compile_errors/policy.yaml:26:66: extraneous input ')' expecting - | expression: variables.want.filter(l, !(lin resource.labels)) - | .................................................................^ -ERROR: compile_errors/policy.yaml:28:27: mismatched input '2' expecting {'}', ','} - | expression: "{1:305 2:569}" - | ..........................^ -ERROR: compile_errors/policy.yaml:36:75: extraneous input ']' expecting ')' - | "missing one or more required labels: %s".format(variables.missing]) - | ..........................................................................^ -ERROR: compile_errors/policy.yaml:39:67: undeclared reference to 'format' (in container '') - | "invalid values provided on one or more labels: %s".format([variables.invalid]) - | ..................................................................^ -ERROR: compile_errors/policy.yaml:40:19: condition must produce a boolean output. - | - condition: '1' - | ..................^ -ERROR: compile_errors/policy.yaml:43:24: found no matching overload for '_==_' applied to '(bool, string)' (candidates: (%A0, %A0)) - | - condition: false == "0" - | .......................^ \ No newline at end of file diff --git a/testing/src/test/resources/policy/compile_errors/policy.yaml b/testing/src/test/resources/policy/compile_errors/policy.yaml deleted file mode 100644 index c17cd3056..000000000 --- a/testing/src/test/resources/policy/compile_errors/policy.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "errors" -imports: -- name: " untrimmed.Import1 " -- name: > - punc.Import! -- name: "bad import" -rule: - variables: - - name: want - expression: spec.labels - - name: missing - expression: variables.want.filter(l, !(lin resource.labels)) - - name: bad_data - expression: "{1:305 2:569}" - - name: invalid - expression: > - resource.labels.filter(l, - l in variables.want && variables.want[l] != resource.labels[l]) - match: - - condition: variables.missing.size() > 0 - output: | - "missing one or more required labels: %s".format(variables.missing]) - - condition: variables.invalid.size() > 0 - output: | - "invalid values provided on one or more labels: %s".format([variables.invalid]) - - condition: '1' - output: | - "condition wrong type" - - condition: false == "0" - output: | - "condition type-check failure" diff --git a/testing/src/test/resources/policy/compose_conflicting_output/expected_errors.baseline b/testing/src/test/resources/policy/compose_conflicting_output/expected_errors.baseline new file mode 100644 index 000000000..241fca0f6 --- /dev/null +++ b/testing/src/test/resources/policy/compose_conflicting_output/expected_errors.baseline @@ -0,0 +1,6 @@ +ERROR: compose_conflicting_output/policy.yaml:22:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool + | output: "false" + | .............^ +ERROR: compose_conflicting_output/policy.yaml:23:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool + | - output: "{'banned': true}" + | .............^ \ No newline at end of file diff --git a/testing/src/test/resources/policy/compose_conflicting_subrule/expected_errors.baseline b/testing/src/test/resources/policy/compose_conflicting_subrule/expected_errors.baseline new file mode 100644 index 000000000..663821b52 --- /dev/null +++ b/testing/src/test/resources/policy/compose_conflicting_subrule/expected_errors.baseline @@ -0,0 +1,6 @@ +ERROR: compose_conflicting_subrule/policy.yaml:34:18: failed composing the subrule 'banned regions' due to incompatible output types. + | output: "true" + | .................^ +ERROR: compose_conflicting_subrule/policy.yaml:36:14: failed composing the subrule 'banned regions' due to incompatible output types. + | output: "{'banned': false}" + | .............^ \ No newline at end of file diff --git a/testing/src/test/resources/policy/compose_errors_conflicting_output/config.yaml b/testing/src/test/resources/policy/compose_errors_conflicting_output/config.yaml deleted file mode 100644 index 5d048a225..000000000 --- a/testing/src/test/resources/policy/compose_errors_conflicting_output/config.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "labels" -variables: -- name: "resource" - type: - type_name: "map" - params: - - type_name: "string" - - type_name: "dyn" diff --git a/testing/src/test/resources/policy/compose_errors_conflicting_output/expected_errors.baseline b/testing/src/test/resources/policy/compose_errors_conflicting_output/expected_errors.baseline deleted file mode 100644 index bc205c2ab..000000000 --- a/testing/src/test/resources/policy/compose_errors_conflicting_output/expected_errors.baseline +++ /dev/null @@ -1,6 +0,0 @@ -ERROR: compose_errors_conflicting_output/policy.yaml:22:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool - | output: "false" - | .............^ -ERROR: compose_errors_conflicting_output/policy.yaml:23:14: incompatible output types: block has output type map(string, bool), but previous outputs have type bool - | - output: "{'banned': true}" - | .............^ \ No newline at end of file diff --git a/testing/src/test/resources/policy/compose_errors_conflicting_output/policy.yaml b/testing/src/test/resources/policy/compose_errors_conflicting_output/policy.yaml deleted file mode 100644 index a5ed5c09c..000000000 --- a/testing/src/test/resources/policy/compose_errors_conflicting_output/policy.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: nested_rule -rule: - variables: - - name: "permitted_regions" - expression: "['us', 'uk', 'es']" - match: - - condition: resource.origin in variables.permitted_regions - output: "false" - - output: "{'banned': true}" diff --git a/testing/src/test/resources/policy/compose_errors_conflicting_subrule/config.yaml b/testing/src/test/resources/policy/compose_errors_conflicting_subrule/config.yaml deleted file mode 100644 index 5d048a225..000000000 --- a/testing/src/test/resources/policy/compose_errors_conflicting_subrule/config.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "labels" -variables: -- name: "resource" - type: - type_name: "map" - params: - - type_name: "string" - - type_name: "dyn" diff --git a/testing/src/test/resources/policy/compose_errors_conflicting_subrule/expected_errors.baseline b/testing/src/test/resources/policy/compose_errors_conflicting_subrule/expected_errors.baseline deleted file mode 100644 index 66e48ea57..000000000 --- a/testing/src/test/resources/policy/compose_errors_conflicting_subrule/expected_errors.baseline +++ /dev/null @@ -1,6 +0,0 @@ -ERROR: compose_errors_conflicting_subrule/policy.yaml:34:18: failed composing the subrule 'banned regions' due to incompatible output types. - | output: "true" - | .................^ -ERROR: compose_errors_conflicting_subrule/policy.yaml:36:14: failed composing the subrule 'banned regions' due to incompatible output types. - | output: "{'banned': false}" - | .............^ \ No newline at end of file diff --git a/testing/src/test/resources/policy/compose_errors_conflicting_subrule/policy.yaml b/testing/src/test/resources/policy/compose_errors_conflicting_subrule/policy.yaml deleted file mode 100644 index 9df1df8d0..000000000 --- a/testing/src/test/resources/policy/compose_errors_conflicting_subrule/policy.yaml +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: nested_rule -rule: - variables: - - name: "permitted_regions" - expression: "['us', 'uk', 'es']" - match: - - rule: - id: "banned regions" - description: > - determine whether the resource origin is in the banned - list. If the region is also in the permitted list, the - ban has no effect. - variables: - - name: "banned_regions" - expression: "{'us': false, 'ru': false, 'ir': false}" - match: - - condition: | - resource.origin in variables.banned_regions && - !(resource.origin in variables.permitted_regions) - output: "true" - - condition: resource.origin in variables.permitted_regions - output: "{'banned': false}" - - output: "{'banned': true}" diff --git a/testing/src/test/resources/policy/context_pb/config.yaml b/testing/src/test/resources/policy/context_pb/config.yaml deleted file mode 100644 index 2ca7fac42..000000000 --- a/testing/src/test/resources/policy/context_pb/config.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "context_pb" -container: "cel.expr.conformance.proto3" -extensions: - - name: "strings" - version: "latest" \ No newline at end of file diff --git a/testing/src/test/resources/policy/context_pb/policy.yaml b/testing/src/test/resources/policy/context_pb/policy.yaml deleted file mode 100644 index 8111bb76c..000000000 --- a/testing/src/test/resources/policy/context_pb/policy.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "context_pb" -rule: - match: - - condition: > - single_int32 > TestAllTypes{single_int64: 10}.single_int64 - output: | - ["invalid spec, got single_int32=" , single_int32 , ", wanted <= 10"].join() - - condition: > - standalone_enum == TestAllTypes.NestedEnum.BAR - output: | - "invalid spec, no nested enums may refer to BAR" \ No newline at end of file diff --git a/testing/src/test/resources/policy/context_pb/tests.textproto b/testing/src/test/resources/policy/context_pb/tests.textproto deleted file mode 100644 index 77f97fa7c..000000000 --- a/testing/src/test/resources/policy/context_pb/tests.textproto +++ /dev/null @@ -1,19 +0,0 @@ -# proto-file: google3/third_party/cel/spec/proto/cel/expr/conformance/test/suite.proto -# proto-message: cel.expr.conformance.test.TestSuite - -name: "context_pb_tests" -description: "Protobuf input tests" -sections { - name: "valid" - description: "Valid protobuf input tests" - tests { - name: "good spec" - description: "Valid protobuf input tests" - input_context { - context_expr: "TestAllTypes{single_int32: 10}" - } - output { - result_expr: "optional.none()" - } - } -} diff --git a/testing/src/test/resources/policy/context_pb/tests.yaml b/testing/src/test/resources/policy/context_pb/tests.yaml deleted file mode 100644 index d37e2bee3..000000000 --- a/testing/src/test/resources/policy/context_pb/tests.yaml +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "context_pb_cel_tests" -description: "Protobuf input tests" -sections: - - name: "valid" - description: "Valid context_expr" - tests: - - name: "good spec" - description: "good spec" - context_expr: - "TestAllTypes{single_int32: 10}" - output: - expr: "optional.none()" - - name: "invalid" - description: "Invalid context_expr" - tests: - - name: "bad spec" - description: "bad spec" - context_expr: - "TestAllTypes{single_int32: 11}" - output: - value: "invalid spec, got single_int32=11, wanted <= 10" \ No newline at end of file diff --git a/testing/src/test/resources/policy/duplicate_variable/expected_errors.baseline b/testing/src/test/resources/policy/duplicate_variable/expected_errors.baseline new file mode 100644 index 000000000..b1025bb60 --- /dev/null +++ b/testing/src/test/resources/policy/duplicate_variable/expected_errors.baseline @@ -0,0 +1,3 @@ +ERROR: duplicate_variable/policy.yaml:23:19: overlapping declaration name 'variables.want' (type 'int' cannot be distinguished from 'string') + | - condition: "true" + | ..................^ diff --git a/testing/src/test/resources/policy/errors_unreachable/config.yaml b/testing/src/test/resources/policy/errors_unreachable/config.yaml deleted file mode 100644 index 8f79bb763..000000000 --- a/testing/src/test/resources/policy/errors_unreachable/config.yaml +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "errors_unreachable" -extensions: -- name: "sets" -- name: "strings" - version: "latest" -variables: -- name: "destination.ip" - type: - type_name: "string" -- name: "origin.ip" - type: - type_name: "string" -- name: "spec.restricted_destinations" - type: - type_name: "list" - params: - - type_name: "string" -- name: "spec.origin" - type: - type_name: "string" -- name: "request" - type: - type_name: "map" - params: - - type_name: "string" - - type_name: "dyn" -- name: "resource" - type: - type_name: "map" - params: - - type_name: "string" - - type_name: "dyn" -functions: -- name: "locationCode" - overloads: - - id: "locationCode_string" - args: - - type_name: "string" - return: - type_name: "string" diff --git a/testing/src/test/resources/policy/errors_unreachable/expected_errors.baseline b/testing/src/test/resources/policy/errors_unreachable/expected_errors.baseline deleted file mode 100644 index f5f24acbe..000000000 --- a/testing/src/test/resources/policy/errors_unreachable/expected_errors.baseline +++ /dev/null @@ -1,6 +0,0 @@ -ERROR: errors_unreachable/policy.yaml:36:9: Match creates unreachable outputs - | - output: | - | ........^ -ERROR: errors_unreachable/policy.yaml:28:7: Rule creates unreachable outputs - | match: - | ......^ \ No newline at end of file diff --git a/testing/src/test/resources/policy/errors_unreachable/policy.yaml b/testing/src/test/resources/policy/errors_unreachable/policy.yaml deleted file mode 100644 index f43fd62c7..000000000 --- a/testing/src/test/resources/policy/errors_unreachable/policy.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "errors_unreachable" -rule: - variables: - - name: want - expression: request.labels - - name: missing - expression: variables.want.filter(l, !(l in resource.labels)) - - name: invalid - expression: > - resource.labels.filter(l, - l in variables.want && variables.want[l] != resource.labels[l]) - match: - - rule: - match: - - output: "''" - - condition: variables.missing.size() > 0 - output: | - "missing one or more required labels: [\"" + variables.missing.join(',') + "\"]" - - condition: variables.invalid.size() > 0 - rule: - match: - - output: | - "invalid values provided on one or more labels: [\"" + variables.invalid.join(',') + "\"]" - - condition: "false" - output: "'unreachable'" diff --git a/testing/src/test/resources/policy/import/expected_errors.baseline b/testing/src/test/resources/policy/import/expected_errors.baseline new file mode 100644 index 000000000..b88402b8c --- /dev/null +++ b/testing/src/test/resources/policy/import/expected_errors.baseline @@ -0,0 +1,6 @@ +ERROR: import/policy.yaml:19:7: Error configuring import: invalid qualified name: punc.Import!, wanted name of the form 'qualified.name' + | punc.Import! + | ......^ +ERROR: import/policy.yaml:20:12: Error configuring import: invalid qualified name: bad import, wanted name of the form 'qualified.name' + | - name: "bad import" + | ...........^ diff --git a/testing/src/test/resources/policy/incompatible_outputs/expected_errors.baseline b/testing/src/test/resources/policy/incompatible_outputs/expected_errors.baseline new file mode 100644 index 000000000..be370847f --- /dev/null +++ b/testing/src/test/resources/policy/incompatible_outputs/expected_errors.baseline @@ -0,0 +1,6 @@ +ERROR: incompatible_outputs/policy.yaml:19:16: incompatible output types: block has output type optional_type(string), but previous outputs have type bool + | output: "true" + | ...............^ +ERROR: incompatible_outputs/policy.yaml:21:16: incompatible output types: block has output type optional_type(string), but previous outputs have type bool + | output: "'false'" + | ...............^ diff --git a/testing/src/test/resources/policy/k8s/config.yaml b/testing/src/test/resources/policy/k8s/config.yaml deleted file mode 100644 index 4df8439ea..000000000 --- a/testing/src/test/resources/policy/k8s/config.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: k8s -extensions: -- name: "strings" - version: 2 -variables: -- name: "resource.labels" - type: - type_name: "map" - params: - - type_name: "string" - - type_name: "string" -- name: "resource.containers" - type: - type_name: "list" - params: - - type_name: "string" -- name: "resource.namespace" - type: - type_name: "string" diff --git a/testing/src/test/resources/policy/k8s/policy.yaml b/testing/src/test/resources/policy/k8s/policy.yaml deleted file mode 100644 index 9cc9782fa..000000000 --- a/testing/src/test/resources/policy/k8s/policy.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: k8s -kind: ValidatingAdmissionPolicy -metadata: - name: "policy.cel.dev" -spec: - failurePolicy: Fail - matchConstraints: - resourceRules: - - apiGroups: ["services"] - apiVersions: ["v3"] - operations: ["CREATE", "UPDATE"] - variables: - - name: env - expression: "resource.labels.?environment.orValue('prod')" - - name: break_glass - expression: "resource.labels.?break_glass.orValue('false') == 'true'" - validations: - - expression: > - variables.break_glass || - resource.containers.all(c, c.startsWith(variables.env + '.')) - messageExpression: > - 'only ' + variables.env + ' containers are allowed in namespace ' + resource.namespace diff --git a/testing/src/test/resources/policy/k8s/tests.yaml b/testing/src/test/resources/policy/k8s/tests.yaml deleted file mode 100644 index f3e7de790..000000000 --- a/testing/src/test/resources/policy/k8s/tests.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -description: K8s admission control tests -section: - - name: "invalid" - tests: - - name: "restricted_container" - input: - resource.namespace: - value: "dev.cel" - resource.labels: - value: - environment: "staging" - resource.containers: - value: - - staging.dev.cel.container1 - - staging.dev.cel.container2 - - preprod.dev.cel.container3 - output: - value: "only staging containers are allowed in namespace dev.cel" diff --git a/testing/src/test/resources/policy/limits/config.yaml b/testing/src/test/resources/policy/limits/config.yaml deleted file mode 100644 index fa6fc737c..000000000 --- a/testing/src/test/resources/policy/limits/config.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "limits" -extensions: -- name: "strings" - version: latest -variables: -- name: "now" - type: - type_name: "google.protobuf.Timestamp" \ No newline at end of file diff --git a/testing/src/test/resources/policy/limits/policy.yaml b/testing/src/test/resources/policy/limits/policy.yaml deleted file mode 100644 index 13c47c39b..000000000 --- a/testing/src/test/resources/policy/limits/policy.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "limits" -rule: - variables: - - name: "greeting" - expression: "'hello'" - - name: "farewell" - expression: "'goodbye'" - - name: "person" - expression: "'me'" - - name: "message_fmt" - expression: "'%s, %s'" - match: - - condition: | - now.getHours() >= 20 - rule: - id: "farewells" - variables: - - name: "message" - expression: > - variables.farewell + ', ' + variables.person -# TODO: replace when string.format is available -# variables.message_fmt.format([variables.farewell, -# variables.person]) - match: - - condition: > - now.getHours() < 21 - output: variables.message + "!" - - condition: > - now.getHours() < 22 - output: variables.message + "!!" - - condition: > - now.getHours() < 24 - output: variables.message + "!!!" - - output: > - variables.greeting + ', ' + variables.person -# variables.message_fmt.format([variables.greeting, variables.person]) TODO: replace when string.format is available \ No newline at end of file diff --git a/testing/src/test/resources/policy/limits/tests.yaml b/testing/src/test/resources/policy/limits/tests.yaml deleted file mode 100644 index 88772e075..000000000 --- a/testing/src/test/resources/policy/limits/tests.yaml +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -description: Limits related tests -section: - - name: "now_after_hours" - tests: - - name: "7pm" - input: - now: - expr: "timestamp('2024-07-30T00:30:00Z')" - output: - value: "hello, me" - - name: "8pm" - input: - now: - expr: "timestamp('2024-07-30T20:30:00Z')" - output: - value: "goodbye, me!" - - name: "9pm" - input: - now: - expr: "timestamp('2024-07-30T21:30:00Z')" - output: - value: "goodbye, me!!" - - name: "11pm" - input: - now: - expr: "timestamp('2024-07-30T23:30:00Z')" - output: - value: "goodbye, me!!!" diff --git a/testing/src/test/resources/policy/nested_rule/config.yaml b/testing/src/test/resources/policy/nested_rule/config.yaml deleted file mode 100644 index bfd94b33c..000000000 --- a/testing/src/test/resources/policy/nested_rule/config.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "nested_rule" -variables: - - name: "resource" - type: - type_name: "map" - params: - - type_name: "string" - - type_name: "dyn" diff --git a/testing/src/test/resources/policy/nested_rule/policy.yaml b/testing/src/test/resources/policy/nested_rule/policy.yaml deleted file mode 100644 index 2fc566b85..000000000 --- a/testing/src/test/resources/policy/nested_rule/policy.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: nested_rule -rule: - variables: - - name: "permitted_regions" - expression: "['us', 'uk', 'es']" - match: - - rule: - id: "banned regions" - description: > - determine whether the resource origin is in the banned - list. If the region is also in the permitted list, the - ban has no effect. - variables: - - name: "banned_regions" - expression: "{'us': false, 'ru': false, 'ir': false}" - match: - - condition: | - resource.origin in variables.banned_regions && - !(resource.origin in variables.permitted_regions) - output: "{'banned': true}" - - condition: resource.origin in variables.permitted_regions - output: "{'banned': false}" - - output: "{'banned': true}" - explanation: "'resource is in the banned region ' + resource.origin" \ No newline at end of file diff --git a/testing/src/test/resources/policy/nested_rule/testrunner_tests.textproto b/testing/src/test/resources/policy/nested_rule/testrunner_tests.textproto deleted file mode 100644 index 9a8dc691e..000000000 --- a/testing/src/test/resources/policy/nested_rule/testrunner_tests.textproto +++ /dev/null @@ -1,79 +0,0 @@ -# proto-file: google3/third_party/cel/spec/proto/cel/expr/conformance/test/suite.proto -# proto-message: cel.expr.conformance.test.TestSuite - -name: "nested_rule" -description: "Nested rule conformance tests" -sections { - name: "valid" - description: "Valid nested rule" - tests { - name: "restricted_origin" - description: "Restricted origin" - input { - key: "resource" - value { - value { - object_value { - [type.googleapis.com/google.protobuf.Struct] { - fields { - key: "origin" - value { string_value: "ir" } - } - } - } - } - } - } - output { - result_expr: "{'banned': true}" - } - } - tests { - name: "by_default" - description: "By default" - input { - key: "resource" - value { - value { - object_value { - [type.googleapis.com/google.protobuf.Struct] { - fields { - key: "origin" - value { string_value: "'de'" } - } - } - } - } - } - } - output { - result_expr: "{'banned': true}" - } - } -} - -sections { - name: "permitted" - description: "Permitted nested rule" - tests { - name: "valid_origin" - input { - key: "resource" - value { - value { - object_value { - [type.googleapis.com/google.protobuf.Struct] { - fields { - key: "origin" - value { string_value: "uk" } - } - } - } - } - } - } - output { - result_expr: "{'banned': false}" - } - } -} \ No newline at end of file diff --git a/testing/src/test/resources/policy/nested_rule/testrunner_tests.yaml b/testing/src/test/resources/policy/nested_rule/testrunner_tests.yaml deleted file mode 100644 index 414784ad8..000000000 --- a/testing/src/test/resources/policy/nested_rule/testrunner_tests.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "nested_rule" -description: Nested rule conformance tests -sections: - - name: "banned" - description: "Tests for the banned section." - tests: - - name: "restricted_origin" - description: "Tests that the ir origin is restricted." - input: - resource: - value: - origin: "ir" - output: - expr: "{'banned': true}" - - name: "by_default" - description: "Tests that the de origin is restricted." - input: - resource: - value: - origin: "de" - output: - expr: "{'banned': true}" - - name: "permitted" - description: "Tests for the permitted section." - tests: - - name: "valid_origin" - description: "Tests that the valid origin is permitted." - input: - resource: - value: - origin: "uk" - output: - expr: "{'banned': false}" diff --git a/testing/src/test/resources/policy/nested_rule/tests.yaml b/testing/src/test/resources/policy/nested_rule/tests.yaml deleted file mode 100644 index 3f9f63437..000000000 --- a/testing/src/test/resources/policy/nested_rule/tests.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -description: Nested rule conformance tests -section: - - name: "banned" - tests: - - name: "restricted_origin" - input: - resource: - value: - origin: "ir" - output: - expr: "{'banned': true}" - - name: "by_default" - input: - resource: - value: - origin: "de" - output: - expr: "{'banned': true}" - - name: "permitted" - tests: - - name: "valid_origin" - input: - resource: - value: - origin: "uk" - output: - expr: "{'banned': false}" diff --git a/testing/src/test/resources/policy/nested_rule2/config.yaml b/testing/src/test/resources/policy/nested_rule2/config.yaml deleted file mode 100644 index 9ee6f0e49..000000000 --- a/testing/src/test/resources/policy/nested_rule2/config.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "nested_rule2" -variables: -- name: "resource" - type: - type_name: "map" - params: - - type_name: "string" - - type_name: "dyn" \ No newline at end of file diff --git a/testing/src/test/resources/policy/nested_rule2/policy.yaml b/testing/src/test/resources/policy/nested_rule2/policy.yaml deleted file mode 100644 index fef91869f..000000000 --- a/testing/src/test/resources/policy/nested_rule2/policy.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: nested_rule2 -rule: - variables: - - name: "permitted_regions" - expression: "['us', 'uk', 'es']" - match: - - condition: resource.?user.orValue("").startsWith("bad") - rule: - id: "banned regions" - description: > - determine whether the resource origin is in the banned - list. If the region is also in the permitted list, the - ban has no effect. - variables: - - name: "banned_regions" - expression: "{'us': false, 'ru': false, 'ir': false}" - match: - - condition: | - resource.origin in variables.banned_regions && - !(resource.origin in variables.permitted_regions) - output: "{'banned': 'restricted_region'}" - explanation: "'resource is in the banned region ' + resource.origin" - - output: "{'banned': 'bad_actor'}" - - condition: "!(resource.origin in variables.permitted_regions)" - output: "{'banned': 'unconfigured_region'}" - - output: "{}" \ No newline at end of file diff --git a/testing/src/test/resources/policy/nested_rule2/tests.yaml b/testing/src/test/resources/policy/nested_rule2/tests.yaml deleted file mode 100644 index 0e1a9ca69..000000000 --- a/testing/src/test/resources/policy/nested_rule2/tests.yaml +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -description: Nested rule conformance tests -section: - - name: "banned" - tests: - - name: "restricted_origin" - input: - resource: - value: - user: "bad-user" - origin: "ir" - output: - expr: "{'banned': 'restricted_region'}" - - name: "by_default" - input: - resource: - value: - user: "bad-user" - origin: "de" - output: - expr: "{'banned': 'bad_actor'}" - - name: "unconfigured_region" - input: - resource: - value: - user: "good-user" - origin: "de" - output: - expr: "{'banned': 'unconfigured_region'}" - - name: "permitted" - tests: - - name: "valid_origin" - input: - resource: - value: - user: "good-user" - origin: "uk" - output: - expr: "{}" diff --git a/testing/src/test/resources/policy/nested_rule3/config.yaml b/testing/src/test/resources/policy/nested_rule3/config.yaml deleted file mode 100644 index d9360d5c9..000000000 --- a/testing/src/test/resources/policy/nested_rule3/config.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "nested_rule3" -variables: -- name: "resource" - type: - type_name: "map" - params: - - type_name: "string" - - type_name: "dyn" \ No newline at end of file diff --git a/testing/src/test/resources/policy/nested_rule3/policy.yaml b/testing/src/test/resources/policy/nested_rule3/policy.yaml deleted file mode 100644 index 4ad765c8d..000000000 --- a/testing/src/test/resources/policy/nested_rule3/policy.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: nested_rule3 -rule: - variables: - - name: "permitted_regions" - expression: "['us', 'uk', 'es']" - match: - - condition: resource.?user.orValue("").startsWith("bad") - rule: - id: "banned regions" - description: > - determine whether the resource origin is in the banned - list. If the region is also in the permitted list, the - ban has no effect. - variables: - - name: "banned_regions" - expression: "{'us': false, 'ru': false, 'ir': false}" - match: - - condition: | - resource.origin in variables.banned_regions && - !(resource.origin in variables.permitted_regions) - output: "{'banned': 'restricted_region'}" - explanation: "'resource is in the banned region ' + resource.origin" - - output: "{'banned': 'bad_actor'}" - - condition: "!(resource.origin in variables.permitted_regions)" - output: "{'banned': 'unconfigured_region'}" \ No newline at end of file diff --git a/testing/src/test/resources/policy/nested_rule3/tests.yaml b/testing/src/test/resources/policy/nested_rule3/tests.yaml deleted file mode 100644 index 9d993c65f..000000000 --- a/testing/src/test/resources/policy/nested_rule3/tests.yaml +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -description: Nested rule conformance tests -section: - - name: "banned" - tests: - - name: "restricted_origin" - input: - resource: - value: - user: "bad-user" - origin: "ir" - output: - expr: "{'banned': 'restricted_region'}" - - name: "by_default" - input: - resource: - value: - user: "bad-user" - origin: "de" - output: - expr: "{'banned': 'bad_actor'}" - - name: "unconfigured_region" - input: - resource: - value: - user: "good-user" - origin: "de" - output: - expr: "{'banned': 'unconfigured_region'}" - - name: "permitted" - tests: - - name: "valid_origin" - input: - resource: - value: - user: "good-user" - origin: "uk" - output: - expr: "optional.none()" diff --git a/testing/src/test/resources/policy/nested_rule4/config.yaml b/testing/src/test/resources/policy/nested_rule4/config.yaml deleted file mode 100644 index 5afb8c587..000000000 --- a/testing/src/test/resources/policy/nested_rule4/config.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "nested_rule4" -variables: - - name: x - type: - type_name: int diff --git a/testing/src/test/resources/policy/nested_rule4/policy.yaml b/testing/src/test/resources/policy/nested_rule4/policy.yaml deleted file mode 100644 index ea53bfb25..000000000 --- a/testing/src/test/resources/policy/nested_rule4/policy.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: nested_rule4 -rule: - match: - - condition: x > 0 - rule: - match: - - rule: - match: - - output: "true" - - output: "false" diff --git a/testing/src/test/resources/policy/nested_rule4/tests.yaml b/testing/src/test/resources/policy/nested_rule4/tests.yaml deleted file mode 100644 index 006eddb88..000000000 --- a/testing/src/test/resources/policy/nested_rule4/tests.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -description: "Nested rule tests which explore optional vs non-optional returns" -section: - - name: "valid" - tests: - - name: "x=0" - input: - x: - value: 0 - output: - value: false - - name: "x=2" - input: - x: - value: 2 - output: - value: true diff --git a/testing/src/test/resources/policy/nested_rule5/config.yaml b/testing/src/test/resources/policy/nested_rule5/config.yaml deleted file mode 100644 index 499450090..000000000 --- a/testing/src/test/resources/policy/nested_rule5/config.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "nested_rule5" -variables: - - name: x - type: - type_name: int diff --git a/testing/src/test/resources/policy/nested_rule5/policy.yaml b/testing/src/test/resources/policy/nested_rule5/policy.yaml deleted file mode 100644 index e43dce188..000000000 --- a/testing/src/test/resources/policy/nested_rule5/policy.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: nested_rule5 -rule: - match: - - condition: x > 0 - rule: - match: - - rule: - match: - - condition: "x > 2" - output: "true" - - condition: x > 1 - rule: - match: - - condition: "x >= 2" - output: "true" - - output: "false" diff --git a/testing/src/test/resources/policy/nested_rule5/tests.yaml b/testing/src/test/resources/policy/nested_rule5/tests.yaml deleted file mode 100644 index 8cd794051..000000000 --- a/testing/src/test/resources/policy/nested_rule5/tests.yaml +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -description: "Nested rule tests which explore optional vs non-optional returns" -section: - - name: "valid" - tests: - - name: "x=0" - input: - x: - value: 0 - output: - value: false - - name: "x=1" - input: - x: - value: 1 - output: - expr: "optional.none()" - - name: "x=2" - input: - x: - value: 2 - output: - expr: "optional.none()" - - name: "x=3" - input: - x: - value: 3 - output: - value: true diff --git a/testing/src/test/resources/policy/nested_rule6/config.yaml b/testing/src/test/resources/policy/nested_rule6/config.yaml deleted file mode 100644 index a5b1ee16b..000000000 --- a/testing/src/test/resources/policy/nested_rule6/config.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "nested_rule6" -variables: - - name: x - type: - type_name: int diff --git a/testing/src/test/resources/policy/nested_rule6/policy.yaml b/testing/src/test/resources/policy/nested_rule6/policy.yaml deleted file mode 100644 index a3360e7c1..000000000 --- a/testing/src/test/resources/policy/nested_rule6/policy.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: nested_rule6 -rule: - match: - - rule: - match: - - rule: - match: - - condition: "x > 2" - output: "true" - - rule: - match: - - condition: "x > 3" - output: "true" - - output: "false" diff --git a/testing/src/test/resources/policy/nested_rule6/tests.yaml b/testing/src/test/resources/policy/nested_rule6/tests.yaml deleted file mode 100644 index fef586df0..000000000 --- a/testing/src/test/resources/policy/nested_rule6/tests.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -description: "Nested rule tests which explore optional vs non-optional returns" -section: - - name: "valid" - tests: - - name: "x=0" - input: - x: - value: 0 - output: - value: false diff --git a/testing/src/test/resources/policy/nested_rule7/config.yaml b/testing/src/test/resources/policy/nested_rule7/config.yaml deleted file mode 100644 index 74d4d8c2d..000000000 --- a/testing/src/test/resources/policy/nested_rule7/config.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "nested_rule7" -variables: - - name: x - type: - type_name: int diff --git a/testing/src/test/resources/policy/nested_rule7/policy.yaml b/testing/src/test/resources/policy/nested_rule7/policy.yaml deleted file mode 100644 index fcacd017e..000000000 --- a/testing/src/test/resources/policy/nested_rule7/policy.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: nested_rule7 -rule: - match: - - rule: - match: - - rule: - match: - - condition: "x > 2" - output: "true" - - rule: - match: - - condition: "x > 3" - output: "true" - - condition: "x > 1" - output: "false" diff --git a/testing/src/test/resources/policy/nested_rule7/tests.yaml b/testing/src/test/resources/policy/nested_rule7/tests.yaml deleted file mode 100644 index ec2896878..000000000 --- a/testing/src/test/resources/policy/nested_rule7/tests.yaml +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -description: "Nested rule tests which explore optional vs non-optional returns" -section: - - name: "valid" - tests: - - name: "x=1" - input: - x: - value: 1 - output: - expr: "optional.none()" - - name: "x=2" - input: - x: - value: 2 - output: - value: false - - name: "x=3" - input: - x: - value: 3 - output: - value: true - - name: "x=4" - input: - x: - value: 4 - output: - value: true diff --git a/testing/src/test/resources/policy/pb/config.yaml b/testing/src/test/resources/policy/pb/config.yaml deleted file mode 100644 index d13ce2ae1..000000000 --- a/testing/src/test/resources/policy/pb/config.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "pb" -container: "cel.expr.conformance.proto3" -extensions: -- name: "strings" - version: 2 -variables: -- name: "spec" - type: - type_name: "cel.expr.conformance.proto3.TestAllTypes" diff --git a/testing/src/test/resources/policy/pb/policy.yaml b/testing/src/test/resources/policy/pb/policy.yaml deleted file mode 100644 index 5d2b1d22a..000000000 --- a/testing/src/test/resources/policy/pb/policy.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "pb" - -imports: -- name: cel.expr.conformance.proto3.TestAllTypes -- name: cel.expr.conformance.proto3.TestAllTypes.NestedEnum - # Note: Following enum is CEL-Java only. -- name: | - dev.cel.testing.testdata.proto3.StandaloneGlobalEnum - -rule: - match: - - condition: > - spec.single_int32 > TestAllTypes{single_int64: 10}.single_int64 - output: | - "invalid spec, got single_int32=" + string(spec.single_int32) + ", wanted <= 10" -# TODO: replace when string.format is available -# "invalid spec, got single_int32=%d, wanted <= 10".format([spec.single_int32]) - - condition: > - spec.standalone_enum == NestedEnum.BAR || - StandaloneGlobalEnum.SGAR == StandaloneGlobalEnum.SGOO - output: | - "invalid spec, neither nested nor imported enums may refer to BAR" \ No newline at end of file diff --git a/testing/src/test/resources/policy/pb/tests.yaml b/testing/src/test/resources/policy/pb/tests.yaml deleted file mode 100644 index 71cd56b57..000000000 --- a/testing/src/test/resources/policy/pb/tests.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -description: "Protobuf input tests" -section: - - name: "valid" - tests: - - name: "good spec" - input: - spec: - expr: > - TestAllTypes{single_int32: 10} - output: - expr: "optional.none()" - - name: "invalid" - tests: - - name: "bad spec" - input: - spec: - expr: > - TestAllTypes{single_int32: 11} - output: - value: "invalid spec, got single_int32=11, wanted <= 10" diff --git a/testing/src/test/resources/policy/required_labels/config.yaml b/testing/src/test/resources/policy/required_labels/config.yaml deleted file mode 100644 index 14311d763..000000000 --- a/testing/src/test/resources/policy/required_labels/config.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "labels" -extensions: - - name: "bindings" - - name: "strings" - version: 2 -variables: - - name: "spec" - type: - type_name: "map" - params: - - type_name: "string" - - type_name: "dyn" - - name: "resource" - type: - type_name: "map" - params: - - type_name: "string" - - type_name: "dyn" diff --git a/testing/src/test/resources/policy/required_labels/policy.yaml b/testing/src/test/resources/policy/required_labels/policy.yaml deleted file mode 100644 index aca75290f..000000000 --- a/testing/src/test/resources/policy/required_labels/policy.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "required_labels" -rule: - variables: - - name: want - expression: spec.labels - - name: missing - expression: variables.want.filter(l, !(l in resource.labels)) - - name: invalid - expression: > - resource.labels.filter(l, - l in variables.want && variables.want[l] != resource.labels[l]) - match: - - condition: variables.missing.size() > 0 - output: | - "missing one or more required labels: [\"" + variables.missing.join(',') + "\"]" - - condition: variables.invalid.size() > 0 - output: | - "invalid values provided on one or more labels: [\"" + variables.invalid.join(',') + "\"]" diff --git a/testing/src/test/resources/policy/required_labels/tests.yaml b/testing/src/test/resources/policy/required_labels/tests.yaml deleted file mode 100644 index 4296c6914..000000000 --- a/testing/src/test/resources/policy/required_labels/tests.yaml +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -description: "Required labels conformance tests" -section: - - name: "valid" - tests: - - name: "matching" - input: - spec: - value: - labels: - env: prod - experiment: "group b" - resource: - value: - labels: - env: prod - experiment: "group b" - release: "v0.1.0" - output: - expr: "optional.none()" - - name: "missing" - tests: - - name: "env" - input: - spec: - value: - labels: - env: prod - experiment: "group b" - resource: - value: - labels: - experiment: "group b" - release: "v0.1.0" - output: - value: "missing one or more required labels: [\"env\"]" - - name: "experiment" - input: - spec: - value: - labels: - env: prod - experiment: "group b" - resource: - value: - labels: - env: staging - release: "v0.1.0" - output: - value: "missing one or more required labels: [\"experiment\"]" - - name: "invalid" - tests: - - name: "env" - input: - spec: - value: - labels: - env: prod - experiment: "group b" - resource: - value: - labels: - env: staging - experiment: "group b" - release: "v0.1.0" - output: - value: "invalid values provided on one or more labels: [\"env\"]" diff --git a/testing/src/test/resources/policy/restricted_destinations/config.yaml b/testing/src/test/resources/policy/restricted_destinations/config.yaml deleted file mode 100644 index b9c8f9750..000000000 --- a/testing/src/test/resources/policy/restricted_destinations/config.yaml +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "labels" -extensions: - - name: "sets" -variables: - - name: "destination.ip" - type: - type_name: "string" - - name: "origin.ip" - type: - type_name: "string" - - name: "spec.restricted_destinations" - type: - type_name: "list" - params: - - type_name: "string" - - name: "spec.origin" - type: - type_name: "string" - - name: "request" - type: - type_name: "map" - params: - - type_name: "string" - - type_name: "dyn" - - name: "resource" - type: - type_name: "map" - params: - - type_name: "string" - - type_name: "dyn" -functions: - - name: "locationCode" - overloads: - - id: "locationCode_string" - args: - - type_name: "string" - return: - type_name: "string" diff --git a/testing/src/test/resources/policy/restricted_destinations/policy.yaml b/testing/src/test/resources/policy/restricted_destinations/policy.yaml deleted file mode 100644 index 95fb454d7..000000000 --- a/testing/src/test/resources/policy/restricted_destinations/policy.yaml +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "restricted_destinations" -rule: - variables: - - name: matches_origin_ip - expression: > - locationCode(origin.ip) == spec.origin - - name: has_nationality - expression: > - has(request.auth.claims.nationality) - - name: matches_nationality - expression: > - variables.has_nationality && request.auth.claims.nationality == spec.origin - - name: matches_dest_ip - expression: > - locationCode(destination.ip) in spec.restricted_destinations - - name: matches_dest_label - expression: > - resource.labels.location in spec.restricted_destinations - - name: matches_dest - expression: > - variables.matches_dest_ip || variables.matches_dest_label - match: - - condition: variables.matches_nationality && variables.matches_dest - output: "true" - - condition: > - !variables.has_nationality && variables.matches_origin_ip && variables.matches_dest - output: "true" - - output: "false" diff --git a/testing/src/test/resources/policy/restricted_destinations/tests.yaml b/testing/src/test/resources/policy/restricted_destinations/tests.yaml deleted file mode 100644 index f7ae36550..000000000 --- a/testing/src/test/resources/policy/restricted_destinations/tests.yaml +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -description: Restricted destinations conformance tests. -section: - - name: "valid" - tests: - - name: "ip_allowed" - input: - spec.origin: - value: "us" - spec.restricted_destinations: - value: - - "cu" - - "ir" - - "kp" - - "sd" - - "sy" - destination.ip: - value: "10.0.0.1" - origin.ip: - value: "10.0.0.1" - request: - value: - auth: - claims: {} - resource: - value: - name: "/company/acme/secrets/doomsday-device" - labels: - location: "us" - output: - value: false # false means unrestricted - - name: "nationality_allowed" - input: - spec.origin: - value: "us" - spec.restricted_destinations: - value: - - "cu" - - "ir" - - "kp" - - "sd" - - "sy" - destination.ip: - value: "10.0.0.1" - request: - value: - auth: - claims: - nationality: "us" - resource: - value: - name: "/company/acme/secrets/doomsday-device" - labels: - location: "us" - output: - value: false - - name: "invalid" - tests: - - name: "destination_ip_prohibited" - input: - spec.origin: - value: "us" - spec.restricted_destinations: - value: - - "cu" - - "ir" - - "kp" - - "sd" - - "sy" - destination.ip: - value: "123.123.123.123" - origin.ip: - value: "10.0.0.1" - request: - value: - auth: - claims: {} - resource: - value: - name: "/company/acme/secrets/doomsday-device" - labels: - location: "us" - output: - value: true # true means restricted - - name: "resource_nationality_prohibited" - input: - spec.origin: - value: "us" - spec.restricted_destinations: - value: - - "cu" - - "ir" - - "kp" - - "sd" - - "sy" - destination.ip: - value: "10.0.0.1" - request: - value: - auth: - claims: - nationality: "us" - resource: - value: - name: "/company/acme/secrets/doomsday-device" - labels: - location: "cu" - output: - value: true diff --git a/testing/src/test/resources/policy/syntax/expected_errors.baseline b/testing/src/test/resources/policy/syntax/expected_errors.baseline new file mode 100644 index 000000000..dd6af277e --- /dev/null +++ b/testing/src/test/resources/policy/syntax/expected_errors.baseline @@ -0,0 +1,12 @@ +ERROR: syntax/policy.yaml:19:51: mismatched input 'resource' expecting {'==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', ')', '.', '-', '?', '+', '*', '/', '%%'} + | expression: "variables.want.filter(l, !(lin resource.labels))" + | ..................................................^ +ERROR: syntax/policy.yaml:19:67: extraneous input ')' expecting + | expression: "variables.want.filter(l, !(lin resource.labels))" + | ..................................................................^ +ERROR: syntax/policy.yaml:21:27: mismatched input '2' expecting {'}', ','} + | expression: "{1:305 2:569}" + | ..........................^ +ERROR: syntax/policy.yaml:24:33: extraneous input ']' expecting + | output: "variables.missing]" + | ................................^ diff --git a/testing/src/test/resources/policy/undeclared_reference/expected_errors.baseline b/testing/src/test/resources/policy/undeclared_reference/expected_errors.baseline new file mode 100644 index 000000000..4b887180e --- /dev/null +++ b/testing/src/test/resources/policy/undeclared_reference/expected_errors.baseline @@ -0,0 +1,6 @@ +ERROR: undeclared_reference/policy.yaml:19:19: undeclared reference to 'spec' (in container '') + | expression: spec.labels + | ..................^ +ERROR: undeclared_reference/policy.yaml:23:29: undeclared reference to 'format' (in container '') + | "invalid: %s".format([variables.val]) + | ............................^ diff --git a/testing/src/test/resources/policy/unreachable/expected_errors.baseline b/testing/src/test/resources/policy/unreachable/expected_errors.baseline new file mode 100644 index 000000000..768f0eeb1 --- /dev/null +++ b/testing/src/test/resources/policy/unreachable/expected_errors.baseline @@ -0,0 +1,6 @@ +ERROR: unreachable/policy.yaml:36:9: Match creates unreachable outputs + | - output: | + | ........^ +ERROR: unreachable/policy.yaml:28:7: Rule creates unreachable outputs + | match: + | ......^ \ No newline at end of file diff --git a/testing/testrunner/cel_java_test.bzl b/testing/testrunner/cel_java_test.bzl index b3457f6f0..450b62af3 100644 --- a/testing/testrunner/cel_java_test.bzl +++ b/testing/testrunner/cel_java_test.bzl @@ -20,6 +20,9 @@ load("@rules_shell//shell:sh_test.bzl", "sh_test") load("@bazel_skylib//lib:paths.bzl", "paths") load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") +def _is_label(s): + return s.startswith("//") or s.startswith(":") or s.startswith("@") + def cel_java_test( name, cel_expr, @@ -33,145 +36,124 @@ def cel_java_test( enable_coverage = False, test_data_path = "", data = []): - """trigger the java impl of the CEL test runner. + """Triggers the Java impl of the CEL test runner. - This rule will generate a java_binary and a run_test rule. This rule will be used to trigger - the java impl of the cel_test rule. + This rule generates a java_binary and a run_test rule. - Note: This rule is to be used only for OSS until cel/expr folder is made available in OSS. Internally, - the cel_test rule is supposed to be used. + Note: This rule is to be used only for OSS until cel/expr folder is made available in OSS. + Internally, the cel_test rule is supposed to be used. Args: - name: str name for the generated artifact - test_suite: str label of a file containing a test suite. The file should have a .yaml or a - .textproto extension. - cel_expr: cel expression to be evaluated. This could be a raw expression or a compiled - expression or cel policy. - is_raw_expr: bool whether the cel_expr is a raw expression or not. If true, the cel_expr - will be used as is and would not be treated as a file path. - filegroup: str label of a filegroup containing the test suite, the config and the checked - expression. - config: str label of a file containing a google.api.expr.conformance.Environment message. - The file should have the .textproto extension. + name: str name for the generated artifact. + cel_expr: cel expression to be evaluated (raw expression, compiled expression, or policy). test_src: user's test class build target. + is_raw_expr: bool whether the cel_expr is a raw expression (not treated as a file path). + test_suite: str label of a test suite file (.yaml or .textproto). + filegroup: str label of a filegroup containing the test suite, config, and checked expression. + config: str label of a google.api.expr.conformance.Environment textproto file. deps: list of dependencies for the java_binary rule. + proto_deps: list of proto_library dependencies for the test. + enable_coverage: bool whether to enable coverage for the test. + test_data_path: absolute path of the directory containing the test files (e.g., "//foo/bar"). data: list of data dependencies for the java_binary rule. - proto_deps: str label of the proto dependencies for the test. Note: This only supports proto_library rules. - enable_coverage: bool whether to enable coverage for the test. This is needed only if the - test runner is being used for gathering coverage data. - test_data_path: absolute path of the directory containing the test files. This is needed only - if the test files are not located in the same directory as the BUILD file. This - would be of the form "//foo/bar". """ + jvm_flags = [] - data, test_data_path = _update_data_with_test_files(data, filegroup, test_data_path, config, test_suite, cel_expr, is_raw_expr) + # Avoid mutating the original data list passed into the macro + resolved_data = list(data) + resolved_deps = list(deps) + + # Normalize paths + pkg_name = native.package_name() + test_data_dir = test_data_path.lstrip("/") if test_data_path else pkg_name - # Since the test_data_path is of the form "//foo/bar", we need to strip the leading "/" to get - # the absolute path. - test_data_path = test_data_path.lstrip("/") + # Add filegroup if provided + if filegroup: + resolved_data.append(filegroup) - if test_suite != "": - test_suite = test_data_path + "/" + test_suite - jvm_flags.append("-Dtest_suite_path=%s" % test_suite) + def _process_file_arg(file_val, flag_name): + """Helper to append JVM flags and resolve data targets for file inputs.""" + if not file_val: + return - if config != "": - config = test_data_path + "/" + config - jvm_flags.append("-Dconfig_path=%s" % config) + if _is_label(file_val): + jvm_flags.append("-D{}=$(location {})".format(flag_name, file_val)) + resolved_data.append(file_val) + else: + jvm_flags.append("-D{}={}/{}".format(flag_name, test_data_dir, file_val)) + # If no filegroup is provided, we must add the file directly to data + if not filegroup: + target = file_val if test_data_dir == pkg_name else "//{}:{}".format(test_data_dir, file_val) + resolved_data.append(target) + + # Process standard file inputs + _process_file_arg(test_suite, "test_suite_path") + _process_file_arg(config, "config_path") + + # Process cel_expr (has specialized fallback logic) _, cel_expr_format = paths.split_extension(cel_expr) + is_valid_cel_ext = cel_expr_format in [".cel", ".celpolicy", ".yaml"] - if is_valid_cel_file_format(file_extension = cel_expr_format) == True: - jvm_flags.append("-Dcel_expr=%s" % test_data_path + "/" + cel_expr) - elif is_raw_expr == True: - jvm_flags.append("-Dcel_expr='%s'" % cel_expr) - elif not is_valid_cel_file_format(file_extension = cel_expr_format) and not is_raw_expr: + if _is_label(cel_expr): jvm_flags.append("-Dcel_expr=$(location {})".format(cel_expr)) + resolved_data.append(cel_expr) + elif is_raw_expr: + jvm_flags.append("-Dcel_expr='{}'".format(cel_expr)) + elif is_valid_cel_ext: + jvm_flags.append("-Dcel_expr={}/{}".format(test_data_dir, cel_expr)) + if not filegroup: + target = cel_expr if test_data_dir == pkg_name else "//{}:{}".format(test_data_dir, cel_expr) + resolved_data.append(target) + else: + # Fallback: Treat as a local target + jvm_flags.append("-Dcel_expr=$(location {})".format(cel_expr)) + resolved_data.append(cel_expr) + # Process Proto Dependencies if proto_deps: + descriptor_set_name = name + "_proto_descriptor_set" + descriptor_set_path = ":" + descriptor_set_name + proto_descriptor_set( - name = name + "_proto_descriptor_set", + name = descriptor_set_name, deps = proto_deps, ) - descriptor_set_path = ":" + name + "_proto_descriptor_set" - data.append(descriptor_set_path) - jvm_flags.append("-Dfile_descriptor_set_path=$(location {})".format(descriptor_set_path)) - java_proto_library( - name = name + "_proto_descriptor_set_java_proto", + name = descriptor_set_name + "_java_proto", deps = proto_deps, ) - deps = deps + [":" + name + "_proto_descriptor_set_java_proto"] - jvm_flags.append("-Dis_raw_expr=%s" % is_raw_expr) - jvm_flags.append("-Dis_coverage_enabled=%s" % enable_coverage) + resolved_data.append(descriptor_set_path) + resolved_deps.append(":" + descriptor_set_name + "_java_proto") + jvm_flags.append("-Dfile_descriptor_set_path=$(location {})".format(descriptor_set_path)) + + # Add boolean flags + jvm_flags.append("-Dis_raw_expr={}".format(is_raw_expr)) + jvm_flags.append("-Dis_coverage_enabled={}".format(enable_coverage)) + # Generate the runner binary java_binary( name = name + "_test_runner_binary", srcs = ["//testing/testrunner:test_runner_binary"], - data = data, + data = resolved_data, jvm_flags = jvm_flags, testonly = True, main_class = "dev.cel.testing.testrunner.TestRunnerBinary", - runtime_deps = [ - test_src, - ], + runtime_deps = [test_src], deps = [ "//testing/testrunner:test_executor", "@maven//:com_google_guava_guava", "@bazel_tools//tools/java/runfiles:runfiles", - ] + deps, + ] + resolved_deps, ) + # Generate the execution shell test sh_test( name = name, tags = ["nomsan"], srcs = ["//testing/testrunner:run_testrunner_binary.sh"], - data = [ - ":%s_test_runner_binary" % name, - ], - args = [ - name, - ], + data = [":{}_test_runner_binary".format(name)], + args = [name], ) - -def _update_data_with_test_files(data, filegroup, test_data_path, config, test_suite, cel_expr, is_raw_expr): - """Updates the data with the test files.""" - - _, cel_expr_format = paths.split_extension(cel_expr) - if filegroup != "": - data = data + [filegroup] - elif test_data_path != "" and test_data_path != native.package_name(): - if config != "": - data = data + [test_data_path + ":" + config] - if test_suite != "": - data = data + [test_data_path + ":" + test_suite] - if is_valid_cel_file_format(file_extension = cel_expr_format): - data = data + [test_data_path + ":" + cel_expr] - else: - test_data_path = native.package_name() - if config != "": - data = data + [config] - if test_suite != "": - data = data + [test_suite] - if is_valid_cel_file_format(file_extension = cel_expr_format): - data = data + [cel_expr] - - if not is_valid_cel_file_format(file_extension = cel_expr_format) and not is_raw_expr: - data = data + [cel_expr] - return data, test_data_path - -def is_valid_cel_file_format(file_extension): - """Checks if the file extension is a valid CEL file format. - - Args: - file_extension: The file extension to check. - - Returns: - True if the file extension is a valid CEL file format, False otherwise. - """ - return file_extension in [ - ".cel", - ".celpolicy", - ".yaml", - ] From caf39d774fe7fcf69d5776a3be6af7665bfb1cea Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Tue, 26 May 2026 14:53:34 -0700 Subject: [PATCH 082/204] Repo move announcement PiperOrigin-RevId: 921698756 --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 78e38961f..88a69ee85 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # Common Expression Language for Java +> [!WARNING] +> **On June 16, 2026, this repository will move to +> github.com/cel-expr/cel-java!** +> +> Please update your links and dependencies. See the [pinned +> issue](https://github.com/google/cel-java/issues/1066) for details. + The Common Expression Language (CEL) is a non-Turing complete language designed for simplicity, speed, safety, and portability. CEL's C-like [syntax][1] looks nearly identical to equivalent expressions in C++, Go, Java, and TypeScript. From 8c9bb8f57d1306697abb88793bcc78a73240e5da Mon Sep 17 00:00:00 2001 From: CEL Dev Team Date: Tue, 26 May 2026 21:11:51 -0700 Subject: [PATCH 083/204] Internal Changes PiperOrigin-RevId: 921860311 --- .../src/test/java/dev/cel/bundle/BUILD.bazel | 8 ++-- .../src/test/java/dev/cel/checker/BUILD.bazel | 8 ++-- common/BUILD.bazel | 4 ++ common/ast/BUILD.bazel | 2 + common/internal/BUILD.bazel | 7 +++ .../src/main/java/dev/cel/common/BUILD.bazel | 7 +++ .../main/java/dev/cel/common/ast/BUILD.bazel | 2 + .../java/dev/cel/common/internal/BUILD.bazel | 7 +++ .../java/dev/cel/common/types/BUILD.bazel | 6 +++ .../java/dev/cel/common/values/BUILD.bazel | 11 +++++ .../src/test/java/dev/cel/common/BUILD.bazel | 8 ++-- .../test/java/dev/cel/common/ast/BUILD.bazel | 8 ++-- .../java/dev/cel/common/internal/BUILD.bazel | 8 ++-- .../dev/cel/common/navigation/BUILD.bazel | 4 +- .../java/dev/cel/common/types/BUILD.bazel | 4 +- .../java/dev/cel/common/values/BUILD.bazel | 4 +- common/types/BUILD.bazel | 5 +++ common/values/BUILD.bazel | 10 +++++ extensions/BUILD.bazel | 1 + .../main/java/dev/cel/extensions/BUILD.bazel | 2 + .../test/java/dev/cel/extensions/BUILD.bazel | 4 +- .../test/java/dev/cel/optimizer/BUILD.bazel | 4 +- .../dev/cel/optimizer/optimizers/BUILD.bazel | 4 +- .../src/test/java/dev/cel/parser/BUILD.bazel | 8 ++-- .../src/test/java/dev/cel/policy/BUILD.bazel | 4 +- runtime/BUILD.bazel | 16 +++++++ .../src/main/java/dev/cel/runtime/BUILD.bazel | 28 ++++++++++++ .../java/dev/cel/runtime/standard/BUILD.bazel | 43 +++++++++++++++++++ .../java/dev/cel/runtime/async/BUILD.bazel | 8 ++-- runtime/standard/BUILD.bazel | 43 +++++++++++++++++++ .../java/dev/cel/testing/compiled/BUILD.bazel | 1 + .../test/java/dev/cel/validator/BUILD.bazel | 4 +- .../dev/cel/validator/validators/BUILD.bazel | 4 +- 33 files changed, 257 insertions(+), 30 deletions(-) diff --git a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel index 265f6d89c..548b4483d 100644 --- a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel +++ b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel @@ -1,9 +1,11 @@ load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") -package(default_applicable_licenses = [ - "//:license", -]) +package( + default_applicable_licenses = [ + "//:license", + ], +) java_library( name = "tests", diff --git a/checker/src/test/java/dev/cel/checker/BUILD.bazel b/checker/src/test/java/dev/cel/checker/BUILD.bazel index 1821a5d85..3eb64bd11 100644 --- a/checker/src/test/java/dev/cel/checker/BUILD.bazel +++ b/checker/src/test/java/dev/cel/checker/BUILD.bazel @@ -1,9 +1,11 @@ load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") -package(default_applicable_licenses = [ - "//:license", -]) +package( + default_applicable_licenses = [ + "//:license", + ], +) java_library( name = "tests", diff --git a/common/BUILD.bazel b/common/BUILD.bazel index 4e0d7485c..9e189a9a7 100644 --- a/common/BUILD.bazel +++ b/common/BUILD.bazel @@ -29,6 +29,7 @@ java_library( cel_android_library( name = "proto_ast_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common:proto_ast_android"], ) @@ -76,6 +77,7 @@ java_library( cel_android_library( name = "cel_source_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common:cel_source_android"], ) @@ -92,6 +94,7 @@ java_library( cel_android_library( name = "cel_ast_android", + compatible_with = [], exports = [ "//common/src/main/java/dev/cel/common:cel_ast_android", ], @@ -125,5 +128,6 @@ java_library( cel_android_library( name = "operator_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common:operator_android"], ) diff --git a/common/ast/BUILD.bazel b/common/ast/BUILD.bazel index 276db0322..3b2016f7a 100644 --- a/common/ast/BUILD.bazel +++ b/common/ast/BUILD.bazel @@ -13,6 +13,7 @@ java_library( cel_android_library( name = "ast_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/ast:ast_android"], ) @@ -23,6 +24,7 @@ java_library( cel_android_library( name = "expr_converter_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/ast:expr_converter_android"], ) diff --git a/common/internal/BUILD.bazel b/common/internal/BUILD.bazel index 7c33e56b9..9a4fa0e3e 100644 --- a/common/internal/BUILD.bazel +++ b/common/internal/BUILD.bazel @@ -23,6 +23,7 @@ java_library( cel_android_library( name = "comparison_functions_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/internal:comparison_functions_android"], ) @@ -79,6 +80,7 @@ java_library( cel_android_library( name = "well_known_proto_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/internal:well_known_proto_android"], ) @@ -104,6 +106,7 @@ java_library( cel_android_library( name = "cel_lite_descriptor_pool_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/internal:cel_lite_descriptor_pool_android"], ) @@ -114,6 +117,7 @@ java_library( cel_android_library( name = "default_lite_descriptor_pool_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/internal:default_lite_descriptor_pool_android"], ) @@ -125,6 +129,7 @@ java_library( cel_android_library( name = "internal_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/internal:internal_android"], ) @@ -135,6 +140,7 @@ java_library( cel_android_library( name = "proto_time_utils_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/internal:proto_time_utils_android"], ) @@ -145,6 +151,7 @@ java_library( cel_android_library( name = "date_time_helpers_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers_android"], ) diff --git a/common/src/main/java/dev/cel/common/BUILD.bazel b/common/src/main/java/dev/cel/common/BUILD.bazel index 38548744c..9f9efd165 100644 --- a/common/src/main/java/dev/cel/common/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/BUILD.bazel @@ -133,6 +133,7 @@ java_library( cel_android_library( name = "proto_ast_android", srcs = PROTO_AST_SOURCE, + compatible_with = [], tags = [ ], deps = [ @@ -247,6 +248,7 @@ java_library( cel_android_library( name = "cel_source_android", srcs = ["CelSource.java"], + compatible_with = [], tags = [ ], deps = [ @@ -277,6 +279,7 @@ java_library( cel_android_library( name = "cel_source_helper_android", srcs = ["CelSourceHelper.java"], + compatible_with = [], deps = [ ":source_location_android", "//common/annotations", @@ -305,6 +308,7 @@ java_library( cel_android_library( name = "cel_ast_android", srcs = ["CelAbstractSyntaxTree.java"], + compatible_with = [], tags = [ ], deps = [ @@ -334,6 +338,7 @@ java_library( cel_android_library( name = "source_android", srcs = SOURCE_SOURCES, + compatible_with = [], visibility = ["//visibility:private"], deps = [ "//common/annotations", @@ -345,6 +350,7 @@ cel_android_library( cel_android_library( name = "source_location_android", srcs = ["CelSourceLocation.java"], + compatible_with = [], visibility = ["//visibility:private"], deps = [ "//:auto_value", @@ -376,6 +382,7 @@ java_library( cel_android_library( name = "operator_android", srcs = ["Operator.java"], + compatible_with = [], tags = [ ], deps = ["@maven_android//:com_google_guava_guava"], diff --git a/common/src/main/java/dev/cel/common/ast/BUILD.bazel b/common/src/main/java/dev/cel/common/ast/BUILD.bazel index 3fc709a07..25ee88990 100644 --- a/common/src/main/java/dev/cel/common/ast/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/ast/BUILD.bazel @@ -76,6 +76,7 @@ java_library( cel_android_library( name = "expr_converter_android", srcs = EXPR_CONVERTER_SOURCES, + compatible_with = [], tags = [ ], deps = [ @@ -142,6 +143,7 @@ java_library( cel_android_library( name = "ast_android", srcs = AST_SOURCES, + compatible_with = [], tags = [ ], deps = [ diff --git a/common/src/main/java/dev/cel/common/internal/BUILD.bazel b/common/src/main/java/dev/cel/common/internal/BUILD.bazel index 58b15b103..3af687192 100644 --- a/common/src/main/java/dev/cel/common/internal/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/internal/BUILD.bazel @@ -67,6 +67,7 @@ java_library( cel_android_library( name = "internal_android", srcs = INTERNAL_SOURCES, + compatible_with = [], tags = [ ], deps = [ @@ -98,6 +99,7 @@ java_library( cel_android_library( name = "comparison_functions_android", srcs = ["ComparisonFunctions.java"], + compatible_with = [], tags = [ ], deps = [ @@ -275,6 +277,7 @@ java_library( cel_android_library( name = "well_known_proto_android", srcs = ["WellKnownProto.java"], + compatible_with = [], tags = [ ], deps = [ @@ -341,6 +344,7 @@ java_library( cel_android_library( name = "cel_lite_descriptor_pool_android", srcs = ["CelLiteDescriptorPool.java"], + compatible_with = [], tags = [ ], deps = [ @@ -370,6 +374,7 @@ java_library( cel_android_library( name = "default_lite_descriptor_pool_android", srcs = ["DefaultLiteDescriptorPool.java"], + compatible_with = [], tags = [ ], deps = [ @@ -422,6 +427,7 @@ java_library( cel_android_library( name = "proto_time_utils_android", srcs = ["ProtoTimeUtils.java"], + compatible_with = [], tags = [ ], deps = [ @@ -449,6 +455,7 @@ java_library( cel_android_library( name = "date_time_helpers_android", srcs = ["DateTimeHelpers.java"], + compatible_with = [], tags = [ ], deps = [ diff --git a/common/src/main/java/dev/cel/common/types/BUILD.bazel b/common/src/main/java/dev/cel/common/types/BUILD.bazel index de65d0b1f..4eebe5d37 100644 --- a/common/src/main/java/dev/cel/common/types/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/types/BUILD.bazel @@ -99,6 +99,7 @@ java_library( cel_android_library( name = "cel_proto_types_android", srcs = ["CelProtoTypes.java"], + compatible_with = [], tags = [ ], deps = [ @@ -204,6 +205,7 @@ cel_android_library( srcs = [ "DefaultTypeProvider.java", ], + compatible_with = [], tags = [ ], deps = [ @@ -217,6 +219,7 @@ cel_android_library( cel_android_library( name = "cel_types_android", srcs = ["CelTypes.java"], + compatible_with = [], tags = [ ], deps = [ @@ -230,6 +233,7 @@ cel_android_library( cel_android_library( name = "type_providers_android", srcs = CEL_TYPE_PROVIDER_SOURCES, + compatible_with = [], tags = [ ], deps = [ @@ -241,6 +245,7 @@ cel_android_library( cel_android_library( name = "types_android", srcs = CEL_TYPE_SOURCES, + compatible_with = [], tags = [ ], deps = [ @@ -255,6 +260,7 @@ cel_android_library( cel_android_library( name = "cel_internal_types_android", srcs = CEL_INTERNAL_TYPE_SOURCES, + compatible_with = [], deps = [ "//:auto_value", "//common/annotations", diff --git a/common/src/main/java/dev/cel/common/values/BUILD.bazel b/common/src/main/java/dev/cel/common/values/BUILD.bazel index 5ccc498fd..fdb2496d3 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -43,6 +43,7 @@ java_library( cel_android_library( name = "cel_value_android", srcs = ["CelValue.java"], + compatible_with = [], tags = [ ], deps = [ @@ -66,6 +67,7 @@ java_library( cel_android_library( name = "cel_value_provider_android", srcs = ["CelValueProvider.java"], + compatible_with = [], tags = [ ], deps = [ @@ -95,6 +97,7 @@ cel_android_library( srcs = [ "CombinedCelValueProvider.java", ], + compatible_with = [], tags = [ ], deps = [ @@ -126,6 +129,7 @@ cel_android_library( srcs = [ "CombinedCelValueConverter.java", ], + compatible_with = [], tags = [ ], deps = [ @@ -151,6 +155,7 @@ cel_android_library( srcs = [ "CelPreAdaptedList.java", ], + compatible_with = [], tags = [ ], deps = ["//common/annotations"], @@ -194,6 +199,7 @@ java_library( cel_android_library( name = "mutable_map_value_android", srcs = ["MutableMapValue.java"], + compatible_with = [], tags = [ ], deps = [ @@ -210,6 +216,7 @@ cel_android_library( cel_android_library( name = "values_android", srcs = CEL_VALUES_SOURCES, + compatible_with = [], tags = [ ], deps = [ @@ -257,6 +264,7 @@ java_library( cel_android_library( name = "base_proto_cel_value_converter_android", srcs = ["BaseProtoCelValueConverter.java"], + compatible_with = [], tags = [ ], deps = [ @@ -343,6 +351,7 @@ cel_android_library( "ProtoLiteCelValueConverter.java", "ProtoMessageLiteValue.java", ], + compatible_with = [], tags = [ ], deps = [ @@ -385,6 +394,7 @@ java_library( cel_android_library( name = "proto_message_lite_value_provider_android", srcs = ["ProtoMessageLiteValueProvider.java"], + compatible_with = [], tags = [ ], deps = [ @@ -417,6 +427,7 @@ java_library( cel_android_library( name = "base_proto_message_value_provider_android", srcs = ["BaseProtoMessageValueProvider.java"], + compatible_with = [], tags = [ ], deps = [ diff --git a/common/src/test/java/dev/cel/common/BUILD.bazel b/common/src/test/java/dev/cel/common/BUILD.bazel index 94060f89a..98be87d17 100644 --- a/common/src/test/java/dev/cel/common/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/BUILD.bazel @@ -1,9 +1,11 @@ load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") -package(default_applicable_licenses = [ - "//:license", -]) +package( + default_applicable_licenses = [ + "//:license", + ], +) java_library( name = "tests", diff --git a/common/src/test/java/dev/cel/common/ast/BUILD.bazel b/common/src/test/java/dev/cel/common/ast/BUILD.bazel index 19726aa21..e4aac277b 100644 --- a/common/src/test/java/dev/cel/common/ast/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/ast/BUILD.bazel @@ -1,9 +1,11 @@ load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") -package(default_applicable_licenses = [ - "//:license", -]) +package( + default_applicable_licenses = [ + "//:license", + ], +) java_library( name = "tests", diff --git a/common/src/test/java/dev/cel/common/internal/BUILD.bazel b/common/src/test/java/dev/cel/common/internal/BUILD.bazel index 33127ce16..a70489fb4 100644 --- a/common/src/test/java/dev/cel/common/internal/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/internal/BUILD.bazel @@ -1,9 +1,11 @@ load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") -package(default_applicable_licenses = [ - "//:license", -]) +package( + default_applicable_licenses = [ + "//:license", + ], +) java_library( name = "tests", diff --git a/common/src/test/java/dev/cel/common/navigation/BUILD.bazel b/common/src/test/java/dev/cel/common/navigation/BUILD.bazel index 74ec3e080..f8b2b988b 100644 --- a/common/src/test/java/dev/cel/common/navigation/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/navigation/BUILD.bazel @@ -1,7 +1,9 @@ load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") -package(default_applicable_licenses = ["//:license"]) +package( + default_applicable_licenses = ["//:license"], +) java_library( name = "tests", diff --git a/common/src/test/java/dev/cel/common/types/BUILD.bazel b/common/src/test/java/dev/cel/common/types/BUILD.bazel index 0c8121bbd..64f555547 100644 --- a/common/src/test/java/dev/cel/common/types/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/types/BUILD.bazel @@ -1,7 +1,9 @@ load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") -package(default_applicable_licenses = ["//:license"]) +package( + default_applicable_licenses = ["//:license"], +) java_library( name = "tests", diff --git a/common/src/test/java/dev/cel/common/values/BUILD.bazel b/common/src/test/java/dev/cel/common/values/BUILD.bazel index bf151fcb7..cd7c24a63 100644 --- a/common/src/test/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/values/BUILD.bazel @@ -1,7 +1,9 @@ load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") -package(default_applicable_licenses = ["//:license"]) +package( + default_applicable_licenses = ["//:license"], +) java_library( name = "tests", diff --git a/common/types/BUILD.bazel b/common/types/BUILD.bazel index df249ddbc..a36485227 100644 --- a/common/types/BUILD.bazel +++ b/common/types/BUILD.bazel @@ -64,25 +64,30 @@ java_library( cel_android_library( name = "cel_types_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/types:cel_types_android"], ) cel_android_library( name = "types_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/types:types_android"], ) cel_android_library( name = "type_providers_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/types:type_providers_android"], ) cel_android_library( name = "cel_proto_types_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/types:cel_proto_types_android"], ) cel_android_library( name = "default_type_provider_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/types:default_type_provider_android"], ) diff --git a/common/values/BUILD.bazel b/common/values/BUILD.bazel index 9853289a9..509ff2460 100644 --- a/common/values/BUILD.bazel +++ b/common/values/BUILD.bazel @@ -14,6 +14,7 @@ java_library( cel_android_library( name = "cel_value_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/values:cel_value_android"], ) @@ -24,6 +25,7 @@ java_library( cel_android_library( name = "cel_value_provider_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/values:cel_value_provider_android"], ) @@ -34,6 +36,7 @@ java_library( cel_android_library( name = "combined_cel_value_provider_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/values:combined_cel_value_provider_android"], ) @@ -45,6 +48,7 @@ java_library( cel_android_library( name = "combined_cel_value_converter_android", + compatible_with = [], visibility = ["//:internal"], exports = ["//common/src/main/java/dev/cel/common/values:combined_cel_value_converter_android"], ) @@ -56,6 +60,7 @@ java_library( cel_android_library( name = "values_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/values:values_android"], ) @@ -67,6 +72,7 @@ java_library( cel_android_library( name = "mutable_map_value_android", + compatible_with = [], visibility = ["//:internal"], exports = ["//common/src/main/java/dev/cel/common/values:mutable_map_value_android"], ) @@ -78,6 +84,7 @@ java_library( cel_android_library( name = "base_proto_cel_value_converter_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/values:base_proto_cel_value_converter_android"], ) @@ -104,6 +111,7 @@ java_library( cel_android_library( name = "proto_message_lite_value_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/values:proto_message_lite_value_android"], ) @@ -114,6 +122,7 @@ java_library( cel_android_library( name = "proto_message_lite_value_provider_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/values:proto_message_lite_value_provider_android"], ) @@ -124,5 +133,6 @@ java_library( cel_android_library( name = "base_proto_message_value_provider_android", + compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/values:base_proto_message_value_provider_android"], ) diff --git a/extensions/BUILD.bazel b/extensions/BUILD.bazel index dea4cd760..0419a7303 100644 --- a/extensions/BUILD.bazel +++ b/extensions/BUILD.bazel @@ -24,6 +24,7 @@ java_library( cel_android_library( name = "lite_extensions_android", + compatible_with = [], exports = ["//extensions/src/main/java/dev/cel/extensions:lite_extensions_android"], ) diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel index 73bab08c9..d60835595 100644 --- a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel @@ -66,6 +66,7 @@ java_library( cel_android_library( name = "lite_extensions_android", srcs = ["CelLiteExtensions.java"], + compatible_with = [], tags = [ ], deps = [ @@ -247,6 +248,7 @@ java_library( cel_android_library( name = "sets_runtime_impl_android", srcs = ["SetsExtensionsRuntimeImpl.java"], + compatible_with = [], visibility = ["//visibility:private"], deps = [ ":sets_function", diff --git a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel index d5671fbd7..31720917f 100644 --- a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel @@ -1,7 +1,9 @@ load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") -package(default_applicable_licenses = ["//:license"]) +package( + default_applicable_licenses = ["//:license"], +) java_library( name = "tests", diff --git a/optimizer/src/test/java/dev/cel/optimizer/BUILD.bazel b/optimizer/src/test/java/dev/cel/optimizer/BUILD.bazel index 748e7ee89..8ea72a261 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/BUILD.bazel +++ b/optimizer/src/test/java/dev/cel/optimizer/BUILD.bazel @@ -1,7 +1,9 @@ load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") -package(default_applicable_licenses = ["//:license"]) +package( + default_applicable_licenses = ["//:license"], +) java_library( name = "tests", diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel index d1220a41a..393014a7d 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -1,7 +1,9 @@ load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") -package(default_applicable_licenses = ["//:license"]) +package( + default_applicable_licenses = ["//:license"], +) java_library( name = "tests", diff --git a/parser/src/test/java/dev/cel/parser/BUILD.bazel b/parser/src/test/java/dev/cel/parser/BUILD.bazel index 1b1668ce3..1ade0181d 100644 --- a/parser/src/test/java/dev/cel/parser/BUILD.bazel +++ b/parser/src/test/java/dev/cel/parser/BUILD.bazel @@ -1,9 +1,11 @@ load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") -package(default_applicable_licenses = [ - "//:license", -]) +package( + default_applicable_licenses = [ + "//:license", + ], +) java_library( name = "tests", diff --git a/policy/src/test/java/dev/cel/policy/BUILD.bazel b/policy/src/test/java/dev/cel/policy/BUILD.bazel index 5fcfd5693..6a76cf3b0 100644 --- a/policy/src/test/java/dev/cel/policy/BUILD.bazel +++ b/policy/src/test/java/dev/cel/policy/BUILD.bazel @@ -1,7 +1,9 @@ load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") -package(default_applicable_licenses = ["//:license"]) +package( + default_applicable_licenses = ["//:license"], +) java_library( name = "tests", diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index d1cb99b64..802a6c858 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -47,6 +47,7 @@ java_library( cel_android_library( name = "dispatcher_android", + compatible_with = [], visibility = ["//:internal"], exports = [ "//runtime/src/main/java/dev/cel/runtime:dispatcher_android", @@ -70,6 +71,7 @@ java_library( cel_android_library( name = "activation_android", + compatible_with = [], visibility = ["//:internal"], exports = [ "//runtime/src/main/java/dev/cel/runtime:activation_android", @@ -91,6 +93,7 @@ java_library( cel_android_library( name = "function_binding_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime:function_binding_android"], ) @@ -107,6 +110,7 @@ java_library( cel_android_library( name = "late_function_binding_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime:late_function_binding_android"], ) @@ -148,6 +152,7 @@ java_library( cel_android_library( name = "interpretable_android", + compatible_with = [], visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime:interpretable_android"], ) @@ -160,6 +165,7 @@ java_library( cel_android_library( name = "runtime_helpers_android", + compatible_with = [], visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime:runtime_helpers_android"], ) @@ -178,6 +184,7 @@ java_library( cel_android_library( name = "runtime_equality_android", + compatible_with = [], visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime:runtime_equality_android"], ) @@ -196,6 +203,7 @@ java_library( cel_android_library( name = "type_resolver_android", + compatible_with = [], visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime:type_resolver_android"], ) @@ -213,6 +221,7 @@ java_library( cel_android_library( name = "unknown_attributes_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime:unknown_attributes_android"], ) @@ -233,6 +242,7 @@ java_library( cel_android_library( name = "standard_functions_android", + compatible_with = [], exports = [ "//runtime/src/main/java/dev/cel/runtime:standard_functions_android", ], @@ -240,11 +250,13 @@ cel_android_library( cel_android_library( name = "lite_runtime_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime:lite_runtime_android"], ) cel_android_library( name = "lite_runtime_factory_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime:lite_runtime_factory_android"], ) @@ -264,6 +276,7 @@ java_library( cel_android_library( name = "lite_runtime_impl_android", + compatible_with = [], visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime:lite_runtime_impl_android"], ) @@ -276,6 +289,7 @@ java_library( cel_android_library( name = "resolved_overload_android", + compatible_with = [], visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime:resolved_overload_android"], ) @@ -288,6 +302,7 @@ java_library( cel_android_library( name = "internal_function_binder_android", + compatible_with = [], visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime:internal_function_binder_andriod"], ) @@ -300,6 +315,7 @@ java_library( cel_android_library( name = "program_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime:program_android"], ) diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 5178ae27c..5dfe6bc02 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -82,6 +82,7 @@ java_library( cel_android_library( name = "runtime_type_provider_android", srcs = ["RuntimeTypeProvider.java"], + compatible_with = [], visibility = ["//visibility:private"], deps = [ ":base_android", @@ -139,6 +140,7 @@ java_library( cel_android_library( name = "dispatcher_android", srcs = DISPATCHER_SOURCES, + compatible_with = [], tags = [ ], deps = [ @@ -175,6 +177,7 @@ java_library( cel_android_library( name = "activation_android", srcs = ["Activation.java"], + compatible_with = [], tags = [ ], deps = [ @@ -222,6 +225,7 @@ java_library( cel_android_library( name = "type_resolver_android", srcs = ["TypeResolver.java"], + compatible_with = [], tags = [ ], deps = [ @@ -272,6 +276,7 @@ java_library( cel_android_library( name = "base_android", srcs = BASE_SOURCES, + compatible_with = [], visibility = ["//visibility:private"], deps = [ ":function_overload_android", @@ -326,6 +331,7 @@ java_library( cel_android_library( name = "interpreter_android", srcs = INTERPRETER_SOURCES, + compatible_with = [], visibility = ["//visibility:private"], deps = [ ":accumulated_unknowns_android", @@ -384,6 +390,7 @@ java_library( cel_android_library( name = "runtime_equality_android", srcs = ["RuntimeEquality.java"], + compatible_with = [], tags = [ ], deps = [ @@ -421,6 +428,7 @@ java_library( cel_android_library( name = "runtime_helpers_android", srcs = ["RuntimeHelpers.java"], + compatible_with = [], tags = [ ], deps = [ @@ -514,6 +522,7 @@ java_library( cel_android_library( name = "late_function_binding_android", srcs = LATE_FUNCTION_BINDING_SOURCES, + compatible_with = [], tags = [ ], deps = [ @@ -537,6 +546,7 @@ java_library( cel_android_library( name = "lite_runtime_library_android", srcs = ["CelLiteRuntimeLibrary.java"], + compatible_with = [], deps = [":lite_runtime_android"], ) @@ -603,6 +613,7 @@ java_library( cel_android_library( name = "interpretable_android", srcs = INTERPRABLE_SOURCES, + compatible_with = [], tags = [ ], deps = [ @@ -676,6 +687,7 @@ java_library( cel_android_library( name = "standard_functions_android", srcs = ["CelStandardFunctions.java"], + compatible_with = [], tags = [ ], deps = [ @@ -751,6 +763,7 @@ java_library( cel_android_library( name = "function_binding_android", srcs = FUNCTION_BINDING_SOURCES, + compatible_with = [], tags = [ ], deps = [ @@ -779,6 +792,7 @@ java_library( cel_android_library( name = "function_resolver_android", srcs = ["CelFunctionResolver.java"], + compatible_with = [], deps = [ ":evaluation_exception", ":resolved_overload_android", @@ -809,6 +823,7 @@ cel_android_library( "CelFunctionOverload.java", "OptimizedFunctionOverload.java", ], + compatible_with = [], deps = [ ":evaluation_exception", ":unknown_attributes_android", @@ -1021,6 +1036,7 @@ java_library( cel_android_library( name = "lite_program_impl_android", srcs = LITE_PROGRAM_IMPL_SOURCES, + compatible_with = [], deps = [ ":activation_android", ":evaluation_exception", @@ -1037,6 +1053,7 @@ cel_android_library( cel_android_library( name = "lite_runtime_impl_android", srcs = LITE_RUNTIME_IMPL_SOURCES, + compatible_with = [], tags = [ ], deps = [ @@ -1081,6 +1098,7 @@ cel_android_library( srcs = [ "CelLiteRuntimeFactory.java", ], + compatible_with = [], tags = [ ], deps = [ @@ -1136,6 +1154,7 @@ java_library( cel_android_library( name = "unknown_attributes_android", srcs = UNKNOWN_ATTRIBUTE_SOURCES, + compatible_with = [], tags = [ ], deps = [ @@ -1169,6 +1188,7 @@ java_library( cel_android_library( name = "cel_value_runtime_type_provider_android", srcs = ["CelValueRuntimeTypeProvider.java"], + compatible_with = [], deps = [ ":runtime_type_provider_android", ":unknown_attributes_android", @@ -1205,6 +1225,7 @@ java_library( cel_android_library( name = "interpreter_util_android", srcs = ["InterpreterUtil.java"], + compatible_with = [], visibility = ["//visibility:private"], deps = [ ":accumulated_unknowns_android", @@ -1232,6 +1253,7 @@ java_library( cel_android_library( name = "evaluation_listener_android", srcs = ["CelEvaluationListener.java"], + compatible_with = [], visibility = ["//visibility:private"], deps = [ "//common/ast:ast_android", @@ -1243,6 +1265,7 @@ cel_android_library( cel_android_library( name = "lite_runtime_android", srcs = LITE_RUNTIME_SOURCES, + compatible_with = [], tags = [ ], deps = [ @@ -1286,6 +1309,7 @@ java_library( cel_android_library( name = "accumulated_unknowns_android", srcs = ["AccumulatedUnknowns.java"], + compatible_with = [], visibility = ["//visibility:private"], deps = [ ":unknown_attributes_android", @@ -1315,6 +1339,7 @@ java_library( cel_android_library( name = "resolved_overload_android", srcs = ["CelResolvedOverload.java"], + compatible_with = [], tags = [ ], deps = [ @@ -1346,6 +1371,7 @@ java_library( cel_android_library( name = "partial_vars_android", srcs = ["PartialVars.java"], + compatible_with = [], tags = [ ], deps = [ @@ -1374,6 +1400,7 @@ java_library( cel_android_library( name = "program_android", srcs = ["Program.java"], + compatible_with = [], tags = [ ], deps = [ @@ -1401,6 +1428,7 @@ java_library( cel_android_library( name = "internal_function_binder_andriod", srcs = ["InternalFunctionBinder.java"], + compatible_with = [], tags = [ ], deps = [ diff --git a/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel index 0a76b6135..980ba7698 100644 --- a/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel @@ -27,6 +27,7 @@ java_library( cel_android_library( name = "standard_function_android", srcs = ["CelStandardFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -64,6 +65,7 @@ java_library( cel_android_library( name = "add_android", srcs = ["AddOperator.java"], + compatible_with = [], tags = [ ], deps = [ @@ -107,6 +109,7 @@ java_library( cel_android_library( name = "subtract_android", srcs = ["SubtractOperator.java"], + compatible_with = [], tags = [ ], deps = [ @@ -145,6 +148,7 @@ java_library( cel_android_library( name = "bool_android", srcs = ["BoolFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -179,6 +183,7 @@ java_library( cel_android_library( name = "bytes_android", srcs = ["BytesFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -211,6 +216,7 @@ java_library( cel_android_library( name = "contains_android", srcs = ["ContainsFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -242,6 +248,7 @@ java_library( cel_android_library( name = "double_android", srcs = ["DoubleFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -276,6 +283,7 @@ java_library( cel_android_library( name = "duration_android", srcs = ["DurationFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -309,6 +317,7 @@ java_library( cel_android_library( name = "dyn_android", srcs = ["DynFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -339,6 +348,7 @@ java_library( cel_android_library( name = "ends_with_android", srcs = ["EndsWithFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -370,6 +380,7 @@ java_library( cel_android_library( name = "equals_android", srcs = ["EqualsOperator.java"], + compatible_with = [], tags = [ ], deps = [ @@ -403,6 +414,7 @@ java_library( cel_android_library( name = "get_day_of_year_android", srcs = ["GetDayOfYearFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -437,6 +449,7 @@ java_library( cel_android_library( name = "get_day_of_month_android", srcs = ["GetDayOfMonthFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -471,6 +484,7 @@ java_library( cel_android_library( name = "get_day_of_week_android", srcs = ["GetDayOfWeekFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -505,6 +519,7 @@ java_library( cel_android_library( name = "get_date_android", srcs = ["GetDateFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -539,6 +554,7 @@ java_library( cel_android_library( name = "get_full_year_android", srcs = ["GetFullYearFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -574,6 +590,7 @@ java_library( cel_android_library( name = "get_hours_android", srcs = ["GetHoursFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -610,6 +627,7 @@ java_library( cel_android_library( name = "get_milliseconds_android", srcs = ["GetMillisecondsFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -646,6 +664,7 @@ java_library( cel_android_library( name = "get_minutes_android", srcs = ["GetMinutesFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -681,6 +700,7 @@ java_library( cel_android_library( name = "get_month_android", srcs = ["GetMonthFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -716,6 +736,7 @@ java_library( cel_android_library( name = "get_seconds_android", srcs = ["GetSecondsFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -755,6 +776,7 @@ java_library( cel_android_library( name = "greater_android", srcs = ["GreaterOperator.java"], + compatible_with = [], tags = [ ], deps = [ @@ -797,6 +819,7 @@ java_library( cel_android_library( name = "greater_equals_android", srcs = ["GreaterEqualsOperator.java"], + compatible_with = [], tags = [ ], deps = [ @@ -834,6 +857,7 @@ java_library( cel_android_library( name = "in_android", srcs = ["InOperator.java"], + compatible_with = [], tags = [ ], deps = [ @@ -867,6 +891,7 @@ java_library( cel_android_library( name = "index_android", srcs = ["IndexOperator.java"], + compatible_with = [], tags = [ ], deps = [ @@ -904,6 +929,7 @@ java_library( cel_android_library( name = "int_android", srcs = ["IntFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -945,6 +971,7 @@ java_library( cel_android_library( name = "less_android", srcs = ["LessOperator.java"], + compatible_with = [], tags = [ ], deps = [ @@ -987,6 +1014,7 @@ java_library( cel_android_library( name = "less_equals_android", srcs = ["LessEqualsOperator.java"], + compatible_with = [], tags = [ ], deps = [ @@ -1024,6 +1052,7 @@ java_library( cel_android_library( name = "logical_not_android", srcs = ["LogicalNotOperator.java"], + compatible_with = [], tags = [ ], deps = [ @@ -1057,6 +1086,7 @@ java_library( cel_android_library( name = "matches_android", srcs = ["MatchesFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -1092,6 +1122,7 @@ java_library( cel_android_library( name = "modulo_android", srcs = ["ModuloOperator.java"], + compatible_with = [], tags = [ ], deps = [ @@ -1128,6 +1159,7 @@ java_library( cel_android_library( name = "multiply_android", srcs = ["MultiplyOperator.java"], + compatible_with = [], tags = [ ], deps = [ @@ -1164,6 +1196,7 @@ java_library( cel_android_library( name = "divide_android", srcs = ["DivideOperator.java"], + compatible_with = [], tags = [ ], deps = [ @@ -1200,6 +1233,7 @@ java_library( cel_android_library( name = "negate_android", srcs = ["NegateOperator.java"], + compatible_with = [], tags = [ ], deps = [ @@ -1234,6 +1268,7 @@ java_library( cel_android_library( name = "not_equals_android", srcs = ["NotEqualsOperator.java"], + compatible_with = [], tags = [ ], deps = [ @@ -1267,6 +1302,7 @@ java_library( cel_android_library( name = "size_android", srcs = ["SizeFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -1299,6 +1335,7 @@ java_library( cel_android_library( name = "starts_with_android", srcs = ["StartsWithFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -1334,6 +1371,7 @@ java_library( cel_android_library( name = "string_android", srcs = ["StringFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -1373,6 +1411,7 @@ java_library( cel_android_library( name = "timestamp_android", srcs = ["TimestampFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -1409,6 +1448,7 @@ java_library( cel_android_library( name = "type_android", srcs = ["TypeFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -1444,6 +1484,7 @@ java_library( cel_android_library( name = "uint_android", srcs = ["UintFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -1479,6 +1520,7 @@ java_library( cel_android_library( name = "not_strictly_false_android", srcs = ["NotStrictlyFalseFunction.java"], + compatible_with = [], tags = [ ], deps = [ @@ -1509,6 +1551,7 @@ java_library( cel_android_library( name = "standard_overload_android", srcs = ["CelStandardOverload.java"], + compatible_with = [], tags = [ ], deps = [ diff --git a/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel index 99e9fd59c..bccba6b6e 100644 --- a/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel @@ -1,9 +1,11 @@ load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") -package(default_applicable_licenses = [ - "//:license", -]) +package( + default_applicable_licenses = [ + "//:license", + ], +) java_library( name = "tests", diff --git a/runtime/standard/BUILD.bazel b/runtime/standard/BUILD.bazel index 4ca87e5e0..19825575c 100644 --- a/runtime/standard/BUILD.bazel +++ b/runtime/standard/BUILD.bazel @@ -13,6 +13,7 @@ java_library( cel_android_library( name = "standard_function_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:standard_function_android"], ) @@ -23,6 +24,7 @@ java_library( cel_android_library( name = "add_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:add_android"], ) @@ -33,6 +35,7 @@ java_library( cel_android_library( name = "subtract_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:subtract_android"], ) @@ -43,6 +46,7 @@ java_library( cel_android_library( name = "bool_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:bool_android"], ) @@ -53,6 +57,7 @@ java_library( cel_android_library( name = "bytes_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:bytes_android"], ) @@ -63,6 +68,7 @@ java_library( cel_android_library( name = "contains_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:contains_android"], ) @@ -73,6 +79,7 @@ java_library( cel_android_library( name = "double_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:double_android"], ) @@ -83,6 +90,7 @@ java_library( cel_android_library( name = "duration_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:duration_android"], ) @@ -93,6 +101,7 @@ java_library( cel_android_library( name = "dyn_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:dyn_android"], ) @@ -103,6 +112,7 @@ java_library( cel_android_library( name = "ends_with_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:ends_with_android"], ) @@ -113,6 +123,7 @@ java_library( cel_android_library( name = "equals_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:equals_android"], ) @@ -123,6 +134,7 @@ java_library( cel_android_library( name = "get_day_of_year_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_day_of_year_android"], ) @@ -133,6 +145,7 @@ java_library( cel_android_library( name = "get_day_of_month_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_day_of_month_android"], ) @@ -143,6 +156,7 @@ java_library( cel_android_library( name = "get_day_of_week_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_day_of_week_android"], ) @@ -153,6 +167,7 @@ java_library( cel_android_library( name = "get_date_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_date_android"], ) @@ -163,6 +178,7 @@ java_library( cel_android_library( name = "get_full_year_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_full_year_android"], ) @@ -173,6 +189,7 @@ java_library( cel_android_library( name = "get_hours_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_hours_android"], ) @@ -183,6 +200,7 @@ java_library( cel_android_library( name = "get_milliseconds_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_milliseconds_android"], ) @@ -193,6 +211,7 @@ java_library( cel_android_library( name = "get_minutes_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_minutes_android"], ) @@ -203,6 +222,7 @@ java_library( cel_android_library( name = "get_month_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_month_android"], ) @@ -213,6 +233,7 @@ java_library( cel_android_library( name = "get_seconds_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_seconds_android"], ) @@ -223,6 +244,7 @@ java_library( cel_android_library( name = "greater_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:greater_android"], ) @@ -233,6 +255,7 @@ java_library( cel_android_library( name = "greater_equals_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:greater_equals_android"], ) @@ -243,6 +266,7 @@ java_library( cel_android_library( name = "in_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:in_android"], ) @@ -253,6 +277,7 @@ java_library( cel_android_library( name = "index_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:index_android"], ) @@ -263,6 +288,7 @@ java_library( cel_android_library( name = "int_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:int_android"], ) @@ -273,6 +299,7 @@ java_library( cel_android_library( name = "less_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:less_android"], ) @@ -283,6 +310,7 @@ java_library( cel_android_library( name = "less_equals_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:less_equals_android"], ) @@ -293,6 +321,7 @@ java_library( cel_android_library( name = "logical_not_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:logical_not_android"], ) @@ -303,6 +332,7 @@ java_library( cel_android_library( name = "matches_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:matches_android"], ) @@ -313,6 +343,7 @@ java_library( cel_android_library( name = "modulo_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:modulo_android"], ) @@ -323,6 +354,7 @@ java_library( cel_android_library( name = "multiply_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:multiply_android"], ) @@ -333,6 +365,7 @@ java_library( cel_android_library( name = "divide_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:divide_android"], ) @@ -343,6 +376,7 @@ java_library( cel_android_library( name = "negate_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:negate_android"], ) @@ -353,6 +387,7 @@ java_library( cel_android_library( name = "not_equals_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:not_equals_android"], ) @@ -363,6 +398,7 @@ java_library( cel_android_library( name = "size_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:size_android"], ) @@ -373,6 +409,7 @@ java_library( cel_android_library( name = "starts_with_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:starts_with_android"], ) @@ -383,6 +420,7 @@ java_library( cel_android_library( name = "string_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:string_android"], ) @@ -393,6 +431,7 @@ java_library( cel_android_library( name = "timestamp_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:timestamp_android"], ) @@ -403,6 +442,7 @@ java_library( cel_android_library( name = "uint_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:uint_android"], ) @@ -413,6 +453,7 @@ java_library( cel_android_library( name = "type_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:type_android"], ) @@ -423,6 +464,7 @@ java_library( cel_android_library( name = "not_strictly_false_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:not_strictly_false_android"], ) @@ -433,5 +475,6 @@ java_library( cel_android_library( name = "standard_overload_android", + compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:standard_overload_android"], ) diff --git a/testing/src/main/java/dev/cel/testing/compiled/BUILD.bazel b/testing/src/main/java/dev/cel/testing/compiled/BUILD.bazel index 5ef4d8878..2547e8102 100644 --- a/testing/src/main/java/dev/cel/testing/compiled/BUILD.bazel +++ b/testing/src/main/java/dev/cel/testing/compiled/BUILD.bazel @@ -26,6 +26,7 @@ java_library( cel_android_library( name = "compiled_expr_utils_android", srcs = ["CompiledExprUtils.java"], + compatible_with = [], tags = [ ], deps = [ diff --git a/validator/src/test/java/dev/cel/validator/BUILD.bazel b/validator/src/test/java/dev/cel/validator/BUILD.bazel index 38cf87363..d7384bf74 100644 --- a/validator/src/test/java/dev/cel/validator/BUILD.bazel +++ b/validator/src/test/java/dev/cel/validator/BUILD.bazel @@ -1,7 +1,9 @@ load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") -package(default_applicable_licenses = ["//:license"]) +package( + default_applicable_licenses = ["//:license"], +) java_library( name = "tests", diff --git a/validator/src/test/java/dev/cel/validator/validators/BUILD.bazel b/validator/src/test/java/dev/cel/validator/validators/BUILD.bazel index cb35e7a6b..adfd406c8 100644 --- a/validator/src/test/java/dev/cel/validator/validators/BUILD.bazel +++ b/validator/src/test/java/dev/cel/validator/validators/BUILD.bazel @@ -1,7 +1,9 @@ load("@rules_java//java:defs.bzl", "java_library") load("//:testing.bzl", "junit4_test_suites") -package(default_applicable_licenses = ["//:license"]) +package( + default_applicable_licenses = ["//:license"], +) java_library( name = "tests", From 98ce0bff2803e68ae937afd4d7ac5de1a44584ad Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 27 May 2026 18:45:20 -0700 Subject: [PATCH 084/204] Internal Changes PiperOrigin-RevId: 922464205 --- cel_android_rules.bzl | 4 +- common/BUILD.bazel | 4 -- common/ast/BUILD.bazel | 2 - common/internal/BUILD.bazel | 7 --- .../src/main/java/dev/cel/common/BUILD.bazel | 7 --- .../main/java/dev/cel/common/ast/BUILD.bazel | 2 - .../java/dev/cel/common/internal/BUILD.bazel | 7 --- .../java/dev/cel/common/types/BUILD.bazel | 6 --- .../java/dev/cel/common/values/BUILD.bazel | 11 ----- common/types/BUILD.bazel | 5 --- common/values/BUILD.bazel | 10 ----- extensions/BUILD.bazel | 1 - .../main/java/dev/cel/extensions/BUILD.bazel | 2 - .../main/java/dev/cel/parser/gen/BUILD.bazel | 4 +- runtime/BUILD.bazel | 16 ------- .../src/main/java/dev/cel/runtime/BUILD.bazel | 28 ------------ .../java/dev/cel/runtime/standard/BUILD.bazel | 43 ------------------- runtime/standard/BUILD.bazel | 43 ------------------- .../java/dev/cel/testing/compiled/BUILD.bazel | 1 - 19 files changed, 4 insertions(+), 199 deletions(-) diff --git a/cel_android_rules.bzl b/cel_android_rules.bzl index 5a94a7ef5..9bd2fd8bc 100644 --- a/cel_android_rules.bzl +++ b/cel_android_rules.bzl @@ -33,11 +33,13 @@ def cel_android_library(name, **kwargs): # By default, set visibility to android_allow_list, unless if overridden at the call site. provided_visibility_or_default = kwargs.get("visibility", ["//:android_allow_list"]) - filtered_kwargs = {k: v for k, v in kwargs.items() if k != "visibility"} + provided_compatible_with_or_default = kwargs.get("compatible_with", []) + filtered_kwargs = {k: v for k, v in kwargs.items() if k not in ["visibility", "compatible_with"]} android_library( name = name, visibility = provided_visibility_or_default, + compatible_with = provided_compatible_with_or_default, javacopts = all_javacopts, **filtered_kwargs ) diff --git a/common/BUILD.bazel b/common/BUILD.bazel index 9e189a9a7..4e0d7485c 100644 --- a/common/BUILD.bazel +++ b/common/BUILD.bazel @@ -29,7 +29,6 @@ java_library( cel_android_library( name = "proto_ast_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common:proto_ast_android"], ) @@ -77,7 +76,6 @@ java_library( cel_android_library( name = "cel_source_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common:cel_source_android"], ) @@ -94,7 +92,6 @@ java_library( cel_android_library( name = "cel_ast_android", - compatible_with = [], exports = [ "//common/src/main/java/dev/cel/common:cel_ast_android", ], @@ -128,6 +125,5 @@ java_library( cel_android_library( name = "operator_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common:operator_android"], ) diff --git a/common/ast/BUILD.bazel b/common/ast/BUILD.bazel index 3b2016f7a..276db0322 100644 --- a/common/ast/BUILD.bazel +++ b/common/ast/BUILD.bazel @@ -13,7 +13,6 @@ java_library( cel_android_library( name = "ast_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/ast:ast_android"], ) @@ -24,7 +23,6 @@ java_library( cel_android_library( name = "expr_converter_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/ast:expr_converter_android"], ) diff --git a/common/internal/BUILD.bazel b/common/internal/BUILD.bazel index 9a4fa0e3e..7c33e56b9 100644 --- a/common/internal/BUILD.bazel +++ b/common/internal/BUILD.bazel @@ -23,7 +23,6 @@ java_library( cel_android_library( name = "comparison_functions_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/internal:comparison_functions_android"], ) @@ -80,7 +79,6 @@ java_library( cel_android_library( name = "well_known_proto_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/internal:well_known_proto_android"], ) @@ -106,7 +104,6 @@ java_library( cel_android_library( name = "cel_lite_descriptor_pool_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/internal:cel_lite_descriptor_pool_android"], ) @@ -117,7 +114,6 @@ java_library( cel_android_library( name = "default_lite_descriptor_pool_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/internal:default_lite_descriptor_pool_android"], ) @@ -129,7 +125,6 @@ java_library( cel_android_library( name = "internal_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/internal:internal_android"], ) @@ -140,7 +135,6 @@ java_library( cel_android_library( name = "proto_time_utils_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/internal:proto_time_utils_android"], ) @@ -151,7 +145,6 @@ java_library( cel_android_library( name = "date_time_helpers_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers_android"], ) diff --git a/common/src/main/java/dev/cel/common/BUILD.bazel b/common/src/main/java/dev/cel/common/BUILD.bazel index 9f9efd165..38548744c 100644 --- a/common/src/main/java/dev/cel/common/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/BUILD.bazel @@ -133,7 +133,6 @@ java_library( cel_android_library( name = "proto_ast_android", srcs = PROTO_AST_SOURCE, - compatible_with = [], tags = [ ], deps = [ @@ -248,7 +247,6 @@ java_library( cel_android_library( name = "cel_source_android", srcs = ["CelSource.java"], - compatible_with = [], tags = [ ], deps = [ @@ -279,7 +277,6 @@ java_library( cel_android_library( name = "cel_source_helper_android", srcs = ["CelSourceHelper.java"], - compatible_with = [], deps = [ ":source_location_android", "//common/annotations", @@ -308,7 +305,6 @@ java_library( cel_android_library( name = "cel_ast_android", srcs = ["CelAbstractSyntaxTree.java"], - compatible_with = [], tags = [ ], deps = [ @@ -338,7 +334,6 @@ java_library( cel_android_library( name = "source_android", srcs = SOURCE_SOURCES, - compatible_with = [], visibility = ["//visibility:private"], deps = [ "//common/annotations", @@ -350,7 +345,6 @@ cel_android_library( cel_android_library( name = "source_location_android", srcs = ["CelSourceLocation.java"], - compatible_with = [], visibility = ["//visibility:private"], deps = [ "//:auto_value", @@ -382,7 +376,6 @@ java_library( cel_android_library( name = "operator_android", srcs = ["Operator.java"], - compatible_with = [], tags = [ ], deps = ["@maven_android//:com_google_guava_guava"], diff --git a/common/src/main/java/dev/cel/common/ast/BUILD.bazel b/common/src/main/java/dev/cel/common/ast/BUILD.bazel index 25ee88990..3fc709a07 100644 --- a/common/src/main/java/dev/cel/common/ast/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/ast/BUILD.bazel @@ -76,7 +76,6 @@ java_library( cel_android_library( name = "expr_converter_android", srcs = EXPR_CONVERTER_SOURCES, - compatible_with = [], tags = [ ], deps = [ @@ -143,7 +142,6 @@ java_library( cel_android_library( name = "ast_android", srcs = AST_SOURCES, - compatible_with = [], tags = [ ], deps = [ diff --git a/common/src/main/java/dev/cel/common/internal/BUILD.bazel b/common/src/main/java/dev/cel/common/internal/BUILD.bazel index 3af687192..58b15b103 100644 --- a/common/src/main/java/dev/cel/common/internal/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/internal/BUILD.bazel @@ -67,7 +67,6 @@ java_library( cel_android_library( name = "internal_android", srcs = INTERNAL_SOURCES, - compatible_with = [], tags = [ ], deps = [ @@ -99,7 +98,6 @@ java_library( cel_android_library( name = "comparison_functions_android", srcs = ["ComparisonFunctions.java"], - compatible_with = [], tags = [ ], deps = [ @@ -277,7 +275,6 @@ java_library( cel_android_library( name = "well_known_proto_android", srcs = ["WellKnownProto.java"], - compatible_with = [], tags = [ ], deps = [ @@ -344,7 +341,6 @@ java_library( cel_android_library( name = "cel_lite_descriptor_pool_android", srcs = ["CelLiteDescriptorPool.java"], - compatible_with = [], tags = [ ], deps = [ @@ -374,7 +370,6 @@ java_library( cel_android_library( name = "default_lite_descriptor_pool_android", srcs = ["DefaultLiteDescriptorPool.java"], - compatible_with = [], tags = [ ], deps = [ @@ -427,7 +422,6 @@ java_library( cel_android_library( name = "proto_time_utils_android", srcs = ["ProtoTimeUtils.java"], - compatible_with = [], tags = [ ], deps = [ @@ -455,7 +449,6 @@ java_library( cel_android_library( name = "date_time_helpers_android", srcs = ["DateTimeHelpers.java"], - compatible_with = [], tags = [ ], deps = [ diff --git a/common/src/main/java/dev/cel/common/types/BUILD.bazel b/common/src/main/java/dev/cel/common/types/BUILD.bazel index 4eebe5d37..de65d0b1f 100644 --- a/common/src/main/java/dev/cel/common/types/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/types/BUILD.bazel @@ -99,7 +99,6 @@ java_library( cel_android_library( name = "cel_proto_types_android", srcs = ["CelProtoTypes.java"], - compatible_with = [], tags = [ ], deps = [ @@ -205,7 +204,6 @@ cel_android_library( srcs = [ "DefaultTypeProvider.java", ], - compatible_with = [], tags = [ ], deps = [ @@ -219,7 +217,6 @@ cel_android_library( cel_android_library( name = "cel_types_android", srcs = ["CelTypes.java"], - compatible_with = [], tags = [ ], deps = [ @@ -233,7 +230,6 @@ cel_android_library( cel_android_library( name = "type_providers_android", srcs = CEL_TYPE_PROVIDER_SOURCES, - compatible_with = [], tags = [ ], deps = [ @@ -245,7 +241,6 @@ cel_android_library( cel_android_library( name = "types_android", srcs = CEL_TYPE_SOURCES, - compatible_with = [], tags = [ ], deps = [ @@ -260,7 +255,6 @@ cel_android_library( cel_android_library( name = "cel_internal_types_android", srcs = CEL_INTERNAL_TYPE_SOURCES, - compatible_with = [], deps = [ "//:auto_value", "//common/annotations", diff --git a/common/src/main/java/dev/cel/common/values/BUILD.bazel b/common/src/main/java/dev/cel/common/values/BUILD.bazel index fdb2496d3..5ccc498fd 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -43,7 +43,6 @@ java_library( cel_android_library( name = "cel_value_android", srcs = ["CelValue.java"], - compatible_with = [], tags = [ ], deps = [ @@ -67,7 +66,6 @@ java_library( cel_android_library( name = "cel_value_provider_android", srcs = ["CelValueProvider.java"], - compatible_with = [], tags = [ ], deps = [ @@ -97,7 +95,6 @@ cel_android_library( srcs = [ "CombinedCelValueProvider.java", ], - compatible_with = [], tags = [ ], deps = [ @@ -129,7 +126,6 @@ cel_android_library( srcs = [ "CombinedCelValueConverter.java", ], - compatible_with = [], tags = [ ], deps = [ @@ -155,7 +151,6 @@ cel_android_library( srcs = [ "CelPreAdaptedList.java", ], - compatible_with = [], tags = [ ], deps = ["//common/annotations"], @@ -199,7 +194,6 @@ java_library( cel_android_library( name = "mutable_map_value_android", srcs = ["MutableMapValue.java"], - compatible_with = [], tags = [ ], deps = [ @@ -216,7 +210,6 @@ cel_android_library( cel_android_library( name = "values_android", srcs = CEL_VALUES_SOURCES, - compatible_with = [], tags = [ ], deps = [ @@ -264,7 +257,6 @@ java_library( cel_android_library( name = "base_proto_cel_value_converter_android", srcs = ["BaseProtoCelValueConverter.java"], - compatible_with = [], tags = [ ], deps = [ @@ -351,7 +343,6 @@ cel_android_library( "ProtoLiteCelValueConverter.java", "ProtoMessageLiteValue.java", ], - compatible_with = [], tags = [ ], deps = [ @@ -394,7 +385,6 @@ java_library( cel_android_library( name = "proto_message_lite_value_provider_android", srcs = ["ProtoMessageLiteValueProvider.java"], - compatible_with = [], tags = [ ], deps = [ @@ -427,7 +417,6 @@ java_library( cel_android_library( name = "base_proto_message_value_provider_android", srcs = ["BaseProtoMessageValueProvider.java"], - compatible_with = [], tags = [ ], deps = [ diff --git a/common/types/BUILD.bazel b/common/types/BUILD.bazel index a36485227..df249ddbc 100644 --- a/common/types/BUILD.bazel +++ b/common/types/BUILD.bazel @@ -64,30 +64,25 @@ java_library( cel_android_library( name = "cel_types_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/types:cel_types_android"], ) cel_android_library( name = "types_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/types:types_android"], ) cel_android_library( name = "type_providers_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/types:type_providers_android"], ) cel_android_library( name = "cel_proto_types_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/types:cel_proto_types_android"], ) cel_android_library( name = "default_type_provider_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/types:default_type_provider_android"], ) diff --git a/common/values/BUILD.bazel b/common/values/BUILD.bazel index 509ff2460..9853289a9 100644 --- a/common/values/BUILD.bazel +++ b/common/values/BUILD.bazel @@ -14,7 +14,6 @@ java_library( cel_android_library( name = "cel_value_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/values:cel_value_android"], ) @@ -25,7 +24,6 @@ java_library( cel_android_library( name = "cel_value_provider_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/values:cel_value_provider_android"], ) @@ -36,7 +34,6 @@ java_library( cel_android_library( name = "combined_cel_value_provider_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/values:combined_cel_value_provider_android"], ) @@ -48,7 +45,6 @@ java_library( cel_android_library( name = "combined_cel_value_converter_android", - compatible_with = [], visibility = ["//:internal"], exports = ["//common/src/main/java/dev/cel/common/values:combined_cel_value_converter_android"], ) @@ -60,7 +56,6 @@ java_library( cel_android_library( name = "values_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/values:values_android"], ) @@ -72,7 +67,6 @@ java_library( cel_android_library( name = "mutable_map_value_android", - compatible_with = [], visibility = ["//:internal"], exports = ["//common/src/main/java/dev/cel/common/values:mutable_map_value_android"], ) @@ -84,7 +78,6 @@ java_library( cel_android_library( name = "base_proto_cel_value_converter_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/values:base_proto_cel_value_converter_android"], ) @@ -111,7 +104,6 @@ java_library( cel_android_library( name = "proto_message_lite_value_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/values:proto_message_lite_value_android"], ) @@ -122,7 +114,6 @@ java_library( cel_android_library( name = "proto_message_lite_value_provider_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/values:proto_message_lite_value_provider_android"], ) @@ -133,6 +124,5 @@ java_library( cel_android_library( name = "base_proto_message_value_provider_android", - compatible_with = [], exports = ["//common/src/main/java/dev/cel/common/values:base_proto_message_value_provider_android"], ) diff --git a/extensions/BUILD.bazel b/extensions/BUILD.bazel index 0419a7303..dea4cd760 100644 --- a/extensions/BUILD.bazel +++ b/extensions/BUILD.bazel @@ -24,7 +24,6 @@ java_library( cel_android_library( name = "lite_extensions_android", - compatible_with = [], exports = ["//extensions/src/main/java/dev/cel/extensions:lite_extensions_android"], ) diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel index d60835595..73bab08c9 100644 --- a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel @@ -66,7 +66,6 @@ java_library( cel_android_library( name = "lite_extensions_android", srcs = ["CelLiteExtensions.java"], - compatible_with = [], tags = [ ], deps = [ @@ -248,7 +247,6 @@ java_library( cel_android_library( name = "sets_runtime_impl_android", srcs = ["SetsExtensionsRuntimeImpl.java"], - compatible_with = [], visibility = ["//visibility:private"], deps = [ ":sets_function", diff --git a/parser/src/main/java/dev/cel/parser/gen/BUILD.bazel b/parser/src/main/java/dev/cel/parser/gen/BUILD.bazel index 8b912168c..d4365e34d 100644 --- a/parser/src/main/java/dev/cel/parser/gen/BUILD.bazel +++ b/parser/src/main/java/dev/cel/parser/gen/BUILD.bazel @@ -7,9 +7,7 @@ that causes build failures on filesystems with case-insensitive paths (e.g. macO load("@rules_java//java:defs.bzl", "java_library") load("//:antlr.bzl", "antlr4_java_combined") -package( - default_applicable_licenses = ["//:license"], -) +package(default_applicable_licenses = ["//:license"]) antlr4_java_combined( name = "cel_g4", diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index 802a6c858..d1cb99b64 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -47,7 +47,6 @@ java_library( cel_android_library( name = "dispatcher_android", - compatible_with = [], visibility = ["//:internal"], exports = [ "//runtime/src/main/java/dev/cel/runtime:dispatcher_android", @@ -71,7 +70,6 @@ java_library( cel_android_library( name = "activation_android", - compatible_with = [], visibility = ["//:internal"], exports = [ "//runtime/src/main/java/dev/cel/runtime:activation_android", @@ -93,7 +91,6 @@ java_library( cel_android_library( name = "function_binding_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime:function_binding_android"], ) @@ -110,7 +107,6 @@ java_library( cel_android_library( name = "late_function_binding_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime:late_function_binding_android"], ) @@ -152,7 +148,6 @@ java_library( cel_android_library( name = "interpretable_android", - compatible_with = [], visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime:interpretable_android"], ) @@ -165,7 +160,6 @@ java_library( cel_android_library( name = "runtime_helpers_android", - compatible_with = [], visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime:runtime_helpers_android"], ) @@ -184,7 +178,6 @@ java_library( cel_android_library( name = "runtime_equality_android", - compatible_with = [], visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime:runtime_equality_android"], ) @@ -203,7 +196,6 @@ java_library( cel_android_library( name = "type_resolver_android", - compatible_with = [], visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime:type_resolver_android"], ) @@ -221,7 +213,6 @@ java_library( cel_android_library( name = "unknown_attributes_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime:unknown_attributes_android"], ) @@ -242,7 +233,6 @@ java_library( cel_android_library( name = "standard_functions_android", - compatible_with = [], exports = [ "//runtime/src/main/java/dev/cel/runtime:standard_functions_android", ], @@ -250,13 +240,11 @@ cel_android_library( cel_android_library( name = "lite_runtime_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime:lite_runtime_android"], ) cel_android_library( name = "lite_runtime_factory_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime:lite_runtime_factory_android"], ) @@ -276,7 +264,6 @@ java_library( cel_android_library( name = "lite_runtime_impl_android", - compatible_with = [], visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime:lite_runtime_impl_android"], ) @@ -289,7 +276,6 @@ java_library( cel_android_library( name = "resolved_overload_android", - compatible_with = [], visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime:resolved_overload_android"], ) @@ -302,7 +288,6 @@ java_library( cel_android_library( name = "internal_function_binder_android", - compatible_with = [], visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime:internal_function_binder_andriod"], ) @@ -315,7 +300,6 @@ java_library( cel_android_library( name = "program_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime:program_android"], ) diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 5dfe6bc02..5178ae27c 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -82,7 +82,6 @@ java_library( cel_android_library( name = "runtime_type_provider_android", srcs = ["RuntimeTypeProvider.java"], - compatible_with = [], visibility = ["//visibility:private"], deps = [ ":base_android", @@ -140,7 +139,6 @@ java_library( cel_android_library( name = "dispatcher_android", srcs = DISPATCHER_SOURCES, - compatible_with = [], tags = [ ], deps = [ @@ -177,7 +175,6 @@ java_library( cel_android_library( name = "activation_android", srcs = ["Activation.java"], - compatible_with = [], tags = [ ], deps = [ @@ -225,7 +222,6 @@ java_library( cel_android_library( name = "type_resolver_android", srcs = ["TypeResolver.java"], - compatible_with = [], tags = [ ], deps = [ @@ -276,7 +272,6 @@ java_library( cel_android_library( name = "base_android", srcs = BASE_SOURCES, - compatible_with = [], visibility = ["//visibility:private"], deps = [ ":function_overload_android", @@ -331,7 +326,6 @@ java_library( cel_android_library( name = "interpreter_android", srcs = INTERPRETER_SOURCES, - compatible_with = [], visibility = ["//visibility:private"], deps = [ ":accumulated_unknowns_android", @@ -390,7 +384,6 @@ java_library( cel_android_library( name = "runtime_equality_android", srcs = ["RuntimeEquality.java"], - compatible_with = [], tags = [ ], deps = [ @@ -428,7 +421,6 @@ java_library( cel_android_library( name = "runtime_helpers_android", srcs = ["RuntimeHelpers.java"], - compatible_with = [], tags = [ ], deps = [ @@ -522,7 +514,6 @@ java_library( cel_android_library( name = "late_function_binding_android", srcs = LATE_FUNCTION_BINDING_SOURCES, - compatible_with = [], tags = [ ], deps = [ @@ -546,7 +537,6 @@ java_library( cel_android_library( name = "lite_runtime_library_android", srcs = ["CelLiteRuntimeLibrary.java"], - compatible_with = [], deps = [":lite_runtime_android"], ) @@ -613,7 +603,6 @@ java_library( cel_android_library( name = "interpretable_android", srcs = INTERPRABLE_SOURCES, - compatible_with = [], tags = [ ], deps = [ @@ -687,7 +676,6 @@ java_library( cel_android_library( name = "standard_functions_android", srcs = ["CelStandardFunctions.java"], - compatible_with = [], tags = [ ], deps = [ @@ -763,7 +751,6 @@ java_library( cel_android_library( name = "function_binding_android", srcs = FUNCTION_BINDING_SOURCES, - compatible_with = [], tags = [ ], deps = [ @@ -792,7 +779,6 @@ java_library( cel_android_library( name = "function_resolver_android", srcs = ["CelFunctionResolver.java"], - compatible_with = [], deps = [ ":evaluation_exception", ":resolved_overload_android", @@ -823,7 +809,6 @@ cel_android_library( "CelFunctionOverload.java", "OptimizedFunctionOverload.java", ], - compatible_with = [], deps = [ ":evaluation_exception", ":unknown_attributes_android", @@ -1036,7 +1021,6 @@ java_library( cel_android_library( name = "lite_program_impl_android", srcs = LITE_PROGRAM_IMPL_SOURCES, - compatible_with = [], deps = [ ":activation_android", ":evaluation_exception", @@ -1053,7 +1037,6 @@ cel_android_library( cel_android_library( name = "lite_runtime_impl_android", srcs = LITE_RUNTIME_IMPL_SOURCES, - compatible_with = [], tags = [ ], deps = [ @@ -1098,7 +1081,6 @@ cel_android_library( srcs = [ "CelLiteRuntimeFactory.java", ], - compatible_with = [], tags = [ ], deps = [ @@ -1154,7 +1136,6 @@ java_library( cel_android_library( name = "unknown_attributes_android", srcs = UNKNOWN_ATTRIBUTE_SOURCES, - compatible_with = [], tags = [ ], deps = [ @@ -1188,7 +1169,6 @@ java_library( cel_android_library( name = "cel_value_runtime_type_provider_android", srcs = ["CelValueRuntimeTypeProvider.java"], - compatible_with = [], deps = [ ":runtime_type_provider_android", ":unknown_attributes_android", @@ -1225,7 +1205,6 @@ java_library( cel_android_library( name = "interpreter_util_android", srcs = ["InterpreterUtil.java"], - compatible_with = [], visibility = ["//visibility:private"], deps = [ ":accumulated_unknowns_android", @@ -1253,7 +1232,6 @@ java_library( cel_android_library( name = "evaluation_listener_android", srcs = ["CelEvaluationListener.java"], - compatible_with = [], visibility = ["//visibility:private"], deps = [ "//common/ast:ast_android", @@ -1265,7 +1243,6 @@ cel_android_library( cel_android_library( name = "lite_runtime_android", srcs = LITE_RUNTIME_SOURCES, - compatible_with = [], tags = [ ], deps = [ @@ -1309,7 +1286,6 @@ java_library( cel_android_library( name = "accumulated_unknowns_android", srcs = ["AccumulatedUnknowns.java"], - compatible_with = [], visibility = ["//visibility:private"], deps = [ ":unknown_attributes_android", @@ -1339,7 +1315,6 @@ java_library( cel_android_library( name = "resolved_overload_android", srcs = ["CelResolvedOverload.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1371,7 +1346,6 @@ java_library( cel_android_library( name = "partial_vars_android", srcs = ["PartialVars.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1400,7 +1374,6 @@ java_library( cel_android_library( name = "program_android", srcs = ["Program.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1428,7 +1401,6 @@ java_library( cel_android_library( name = "internal_function_binder_andriod", srcs = ["InternalFunctionBinder.java"], - compatible_with = [], tags = [ ], deps = [ diff --git a/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel index 980ba7698..0a76b6135 100644 --- a/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/standard/BUILD.bazel @@ -27,7 +27,6 @@ java_library( cel_android_library( name = "standard_function_android", srcs = ["CelStandardFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -65,7 +64,6 @@ java_library( cel_android_library( name = "add_android", srcs = ["AddOperator.java"], - compatible_with = [], tags = [ ], deps = [ @@ -109,7 +107,6 @@ java_library( cel_android_library( name = "subtract_android", srcs = ["SubtractOperator.java"], - compatible_with = [], tags = [ ], deps = [ @@ -148,7 +145,6 @@ java_library( cel_android_library( name = "bool_android", srcs = ["BoolFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -183,7 +179,6 @@ java_library( cel_android_library( name = "bytes_android", srcs = ["BytesFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -216,7 +211,6 @@ java_library( cel_android_library( name = "contains_android", srcs = ["ContainsFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -248,7 +242,6 @@ java_library( cel_android_library( name = "double_android", srcs = ["DoubleFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -283,7 +276,6 @@ java_library( cel_android_library( name = "duration_android", srcs = ["DurationFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -317,7 +309,6 @@ java_library( cel_android_library( name = "dyn_android", srcs = ["DynFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -348,7 +339,6 @@ java_library( cel_android_library( name = "ends_with_android", srcs = ["EndsWithFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -380,7 +370,6 @@ java_library( cel_android_library( name = "equals_android", srcs = ["EqualsOperator.java"], - compatible_with = [], tags = [ ], deps = [ @@ -414,7 +403,6 @@ java_library( cel_android_library( name = "get_day_of_year_android", srcs = ["GetDayOfYearFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -449,7 +437,6 @@ java_library( cel_android_library( name = "get_day_of_month_android", srcs = ["GetDayOfMonthFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -484,7 +471,6 @@ java_library( cel_android_library( name = "get_day_of_week_android", srcs = ["GetDayOfWeekFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -519,7 +505,6 @@ java_library( cel_android_library( name = "get_date_android", srcs = ["GetDateFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -554,7 +539,6 @@ java_library( cel_android_library( name = "get_full_year_android", srcs = ["GetFullYearFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -590,7 +574,6 @@ java_library( cel_android_library( name = "get_hours_android", srcs = ["GetHoursFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -627,7 +610,6 @@ java_library( cel_android_library( name = "get_milliseconds_android", srcs = ["GetMillisecondsFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -664,7 +646,6 @@ java_library( cel_android_library( name = "get_minutes_android", srcs = ["GetMinutesFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -700,7 +681,6 @@ java_library( cel_android_library( name = "get_month_android", srcs = ["GetMonthFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -736,7 +716,6 @@ java_library( cel_android_library( name = "get_seconds_android", srcs = ["GetSecondsFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -776,7 +755,6 @@ java_library( cel_android_library( name = "greater_android", srcs = ["GreaterOperator.java"], - compatible_with = [], tags = [ ], deps = [ @@ -819,7 +797,6 @@ java_library( cel_android_library( name = "greater_equals_android", srcs = ["GreaterEqualsOperator.java"], - compatible_with = [], tags = [ ], deps = [ @@ -857,7 +834,6 @@ java_library( cel_android_library( name = "in_android", srcs = ["InOperator.java"], - compatible_with = [], tags = [ ], deps = [ @@ -891,7 +867,6 @@ java_library( cel_android_library( name = "index_android", srcs = ["IndexOperator.java"], - compatible_with = [], tags = [ ], deps = [ @@ -929,7 +904,6 @@ java_library( cel_android_library( name = "int_android", srcs = ["IntFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -971,7 +945,6 @@ java_library( cel_android_library( name = "less_android", srcs = ["LessOperator.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1014,7 +987,6 @@ java_library( cel_android_library( name = "less_equals_android", srcs = ["LessEqualsOperator.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1052,7 +1024,6 @@ java_library( cel_android_library( name = "logical_not_android", srcs = ["LogicalNotOperator.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1086,7 +1057,6 @@ java_library( cel_android_library( name = "matches_android", srcs = ["MatchesFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1122,7 +1092,6 @@ java_library( cel_android_library( name = "modulo_android", srcs = ["ModuloOperator.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1159,7 +1128,6 @@ java_library( cel_android_library( name = "multiply_android", srcs = ["MultiplyOperator.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1196,7 +1164,6 @@ java_library( cel_android_library( name = "divide_android", srcs = ["DivideOperator.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1233,7 +1200,6 @@ java_library( cel_android_library( name = "negate_android", srcs = ["NegateOperator.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1268,7 +1234,6 @@ java_library( cel_android_library( name = "not_equals_android", srcs = ["NotEqualsOperator.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1302,7 +1267,6 @@ java_library( cel_android_library( name = "size_android", srcs = ["SizeFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1335,7 +1299,6 @@ java_library( cel_android_library( name = "starts_with_android", srcs = ["StartsWithFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1371,7 +1334,6 @@ java_library( cel_android_library( name = "string_android", srcs = ["StringFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1411,7 +1373,6 @@ java_library( cel_android_library( name = "timestamp_android", srcs = ["TimestampFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1448,7 +1409,6 @@ java_library( cel_android_library( name = "type_android", srcs = ["TypeFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1484,7 +1444,6 @@ java_library( cel_android_library( name = "uint_android", srcs = ["UintFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1520,7 +1479,6 @@ java_library( cel_android_library( name = "not_strictly_false_android", srcs = ["NotStrictlyFalseFunction.java"], - compatible_with = [], tags = [ ], deps = [ @@ -1551,7 +1509,6 @@ java_library( cel_android_library( name = "standard_overload_android", srcs = ["CelStandardOverload.java"], - compatible_with = [], tags = [ ], deps = [ diff --git a/runtime/standard/BUILD.bazel b/runtime/standard/BUILD.bazel index 19825575c..4ca87e5e0 100644 --- a/runtime/standard/BUILD.bazel +++ b/runtime/standard/BUILD.bazel @@ -13,7 +13,6 @@ java_library( cel_android_library( name = "standard_function_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:standard_function_android"], ) @@ -24,7 +23,6 @@ java_library( cel_android_library( name = "add_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:add_android"], ) @@ -35,7 +33,6 @@ java_library( cel_android_library( name = "subtract_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:subtract_android"], ) @@ -46,7 +43,6 @@ java_library( cel_android_library( name = "bool_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:bool_android"], ) @@ -57,7 +53,6 @@ java_library( cel_android_library( name = "bytes_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:bytes_android"], ) @@ -68,7 +63,6 @@ java_library( cel_android_library( name = "contains_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:contains_android"], ) @@ -79,7 +73,6 @@ java_library( cel_android_library( name = "double_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:double_android"], ) @@ -90,7 +83,6 @@ java_library( cel_android_library( name = "duration_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:duration_android"], ) @@ -101,7 +93,6 @@ java_library( cel_android_library( name = "dyn_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:dyn_android"], ) @@ -112,7 +103,6 @@ java_library( cel_android_library( name = "ends_with_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:ends_with_android"], ) @@ -123,7 +113,6 @@ java_library( cel_android_library( name = "equals_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:equals_android"], ) @@ -134,7 +123,6 @@ java_library( cel_android_library( name = "get_day_of_year_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_day_of_year_android"], ) @@ -145,7 +133,6 @@ java_library( cel_android_library( name = "get_day_of_month_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_day_of_month_android"], ) @@ -156,7 +143,6 @@ java_library( cel_android_library( name = "get_day_of_week_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_day_of_week_android"], ) @@ -167,7 +153,6 @@ java_library( cel_android_library( name = "get_date_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_date_android"], ) @@ -178,7 +163,6 @@ java_library( cel_android_library( name = "get_full_year_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_full_year_android"], ) @@ -189,7 +173,6 @@ java_library( cel_android_library( name = "get_hours_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_hours_android"], ) @@ -200,7 +183,6 @@ java_library( cel_android_library( name = "get_milliseconds_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_milliseconds_android"], ) @@ -211,7 +193,6 @@ java_library( cel_android_library( name = "get_minutes_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_minutes_android"], ) @@ -222,7 +203,6 @@ java_library( cel_android_library( name = "get_month_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_month_android"], ) @@ -233,7 +213,6 @@ java_library( cel_android_library( name = "get_seconds_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:get_seconds_android"], ) @@ -244,7 +223,6 @@ java_library( cel_android_library( name = "greater_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:greater_android"], ) @@ -255,7 +233,6 @@ java_library( cel_android_library( name = "greater_equals_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:greater_equals_android"], ) @@ -266,7 +243,6 @@ java_library( cel_android_library( name = "in_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:in_android"], ) @@ -277,7 +253,6 @@ java_library( cel_android_library( name = "index_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:index_android"], ) @@ -288,7 +263,6 @@ java_library( cel_android_library( name = "int_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:int_android"], ) @@ -299,7 +273,6 @@ java_library( cel_android_library( name = "less_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:less_android"], ) @@ -310,7 +283,6 @@ java_library( cel_android_library( name = "less_equals_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:less_equals_android"], ) @@ -321,7 +293,6 @@ java_library( cel_android_library( name = "logical_not_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:logical_not_android"], ) @@ -332,7 +303,6 @@ java_library( cel_android_library( name = "matches_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:matches_android"], ) @@ -343,7 +313,6 @@ java_library( cel_android_library( name = "modulo_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:modulo_android"], ) @@ -354,7 +323,6 @@ java_library( cel_android_library( name = "multiply_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:multiply_android"], ) @@ -365,7 +333,6 @@ java_library( cel_android_library( name = "divide_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:divide_android"], ) @@ -376,7 +343,6 @@ java_library( cel_android_library( name = "negate_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:negate_android"], ) @@ -387,7 +353,6 @@ java_library( cel_android_library( name = "not_equals_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:not_equals_android"], ) @@ -398,7 +363,6 @@ java_library( cel_android_library( name = "size_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:size_android"], ) @@ -409,7 +373,6 @@ java_library( cel_android_library( name = "starts_with_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:starts_with_android"], ) @@ -420,7 +383,6 @@ java_library( cel_android_library( name = "string_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:string_android"], ) @@ -431,7 +393,6 @@ java_library( cel_android_library( name = "timestamp_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:timestamp_android"], ) @@ -442,7 +403,6 @@ java_library( cel_android_library( name = "uint_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:uint_android"], ) @@ -453,7 +413,6 @@ java_library( cel_android_library( name = "type_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:type_android"], ) @@ -464,7 +423,6 @@ java_library( cel_android_library( name = "not_strictly_false_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:not_strictly_false_android"], ) @@ -475,6 +433,5 @@ java_library( cel_android_library( name = "standard_overload_android", - compatible_with = [], exports = ["//runtime/src/main/java/dev/cel/runtime/standard:standard_overload_android"], ) diff --git a/testing/src/main/java/dev/cel/testing/compiled/BUILD.bazel b/testing/src/main/java/dev/cel/testing/compiled/BUILD.bazel index 2547e8102..5ef4d8878 100644 --- a/testing/src/main/java/dev/cel/testing/compiled/BUILD.bazel +++ b/testing/src/main/java/dev/cel/testing/compiled/BUILD.bazel @@ -26,7 +26,6 @@ java_library( cel_android_library( name = "compiled_expr_utils_android", srcs = ["CompiledExprUtils.java"], - compatible_with = [], tags = [ ], deps = [ From 659bfb9159c2c98e63e3c7b74b745d2dab1cf864 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 1 Jun 2026 14:41:53 -0700 Subject: [PATCH 085/204] Remove duplicate policy tests that's already covered by conformance tests PiperOrigin-RevId: 924921276 --- .../dev/cel/testing/testrunner/BUILD.bazel | 107 ------------------ testing/testrunner/cel_java_test.bzl | 2 +- 2 files changed, 1 insertion(+), 108 deletions(-) diff --git a/testing/src/test/java/dev/cel/testing/testrunner/BUILD.bazel b/testing/src/test/java/dev/cel/testing/testrunner/BUILD.bazel index 69c53e5d9..9141832cb 100644 --- a/testing/src/test/java/dev/cel/testing/testrunner/BUILD.bazel +++ b/testing/src/test/java/dev/cel/testing/testrunner/BUILD.bazel @@ -155,33 +155,6 @@ java_test( ], ) -cel_java_test( - name = "test_runner_sample_yaml", - cel_expr = "@cel_policy//conformance:testdata/nested_rule/policy.yaml", - proto_deps = [ - "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", - "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", - ], - test_src = ":user_test", - test_suite = "@cel_policy//conformance:testdata/nested_rule/tests.yaml", - deps = [ - "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", - "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", - ], -) - -cel_java_test( - name = "unknown_set_yaml", - cel_expr = "@cel_policy//conformance:testdata/nested_rule/policy.yaml", - proto_deps = [ - "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", - "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", - ], - test_data_path = "//testing/src/test/resources/policy", - test_src = ":user_test", - test_suite = "nested_rule/testrunner_unknown_output_tests.yaml", -) - cel_java_test( name = "custom_variable_binding_test_runner_sample", cel_expr = "custom_variable_bindings/policy.yaml", @@ -208,53 +181,6 @@ cel_java_test( test_suite = "nested_rule/eval_error_tests.yaml", ) -cel_java_test( - name = "context_pb_user_test_runner_sample", - cel_expr = "@cel_policy//conformance:testdata/context_pb/policy.yaml", - config = "@cel_policy//conformance:testdata/context_pb/config.yaml", - proto_deps = [ - "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", - "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", - ], - test_src = ":context_pb_user_test", - test_suite = "@cel_policy//conformance:testdata/context_pb/tests.yaml", - deps = [ - "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", - "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", - ], -) - -cel_java_test( - name = "additional_config_test_runner_sample", - cel_expr = "@cel_policy//conformance:testdata/nested_rule/policy.yaml", - config = "@cel_policy//conformance:testdata/nested_rule/config.yaml", - proto_deps = [ - "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", - "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", - ], - test_src = ":env_config_user_test", - test_suite = "@cel_policy//conformance:testdata/nested_rule/tests.textproto", - deps = [ - "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", - "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", - ], -) - -cel_java_test( - name = "test_runner_sample", - cel_expr = "@cel_policy//conformance:testdata/nested_rule/policy.yaml", - proto_deps = [ - "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", - "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", - ], - test_src = ":user_test", - test_suite = "@cel_policy//conformance:testdata/nested_rule/tests.textproto", - deps = [ - "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", - "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", - ], -) - cel_java_test( name = "test_runner_sample_with_expr_value_output", cel_expr = "expr_value_output/policy.yaml", @@ -280,39 +206,6 @@ cel_java_test( ], ) -cel_java_test( - name = "context_message_user_test_runner_textproto_sample", - cel_expr = "@cel_policy//conformance:testdata/context_pb/policy.yaml", - config = "@cel_policy//conformance:testdata/context_pb/config.yaml", - proto_deps = [ - "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", - "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", - ], - test_data_path = "//testing/src/test/resources/policy", - test_src = ":context_pb_user_test", - test_suite = "context_pb/context_msg_tests.textproto", - deps = [ - "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", - "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", - ], -) - -cel_java_test( - name = "context_pb_user_test_runner_textproto_sample", - cel_expr = "@cel_policy//conformance:testdata/context_pb/policy.yaml", - config = "@cel_policy//conformance:testdata/context_pb/config.yaml", - proto_deps = [ - "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_proto", - "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", - ], - test_src = ":context_pb_user_test", - test_suite = "@cel_policy//conformance:testdata/context_pb/tests.textproto", - deps = [ - "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", - "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", - ], -) - cel_java_test( name = "raw_expression_test", cel_expr = "2 + 2 == 4", diff --git a/testing/testrunner/cel_java_test.bzl b/testing/testrunner/cel_java_test.bzl index 450b62af3..d2dd796c0 100644 --- a/testing/testrunner/cel_java_test.bzl +++ b/testing/testrunner/cel_java_test.bzl @@ -21,7 +21,7 @@ load("@bazel_skylib//lib:paths.bzl", "paths") load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") def _is_label(s): - return s.startswith("//") or s.startswith(":") or s.startswith("@") + return s.startswith("//") or s.startswith(":") def cel_java_test( name, From 2793c0049d31f31db9f507ea7a6a63652529ef2d Mon Sep 17 00:00:00 2001 From: Andrew Parmet Date: Sat, 9 May 2026 09:51:53 -0400 Subject: [PATCH 086/204] Handle FIXED32/FIXED64 as unsigned in ProtoCelValueConverter The CEL spec maps proto fixed32/fixed64 to CEL's uint. cel-java's checker (DescriptorMappings) and its legacy DescriptorMessageProvider path (ProtoAdapter) both correctly map them. The CelValue path via ProtoCelValueConverter.fromProtoMessageFieldToCelValue missed FIXED32/FIXED64 and fell through to normalizePrimitive, producing Integer/Long. Overload resolution at runtime then failed with "No matching overload" when the checker said uint but the runtime served a signed integer. Add the missing case labels alongside UINT32/UINT64 and extend the ProtoMessageValue test suite to cover fixed32/fixed64 selection. --- .../common/values/ProtoCelValueConverter.java | 2 ++ .../common/values/ProtoMessageValueTest.java | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java index 948df759c..89e2e029c 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java @@ -154,11 +154,13 @@ public Object fromProtoMessageFieldToCelValue(Message message, FieldDescriptor f return toRuntimeValue(result); case UINT32: + case FIXED32: if (!fieldDescriptor.isRepeated()) { return UnsignedLong.valueOf((int) result); } break; case UINT64: + case FIXED64: if (!fieldDescriptor.isRepeated()) { return UnsignedLong.fromLongBits((long) result); } diff --git a/common/src/test/java/dev/cel/common/values/ProtoMessageValueTest.java b/common/src/test/java/dev/cel/common/values/ProtoMessageValueTest.java index 365dd32b4..1e428606b 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoMessageValueTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoMessageValueTest.java @@ -303,6 +303,29 @@ public void selectField_durationOutOfRange_success(int seconds, int nanos) { .isEqualTo(Duration.ofSeconds(seconds, nanos)); } + @Test + public void selectField_fixed32_returnsUnsignedLong() { + TestAllTypes testAllTypes = TestAllTypes.newBuilder().setSingleFixed32(1).build(); + + ProtoMessageValue protoMessageValue = + ProtoMessageValue.create( + testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER, false); + + assertThat(protoMessageValue.select("single_fixed32")).isEqualTo(UnsignedLong.valueOf(1L)); + } + + @Test + public void selectField_fixed64_returnsUnsignedLong() { + TestAllTypes testAllTypes = + TestAllTypes.newBuilder().setSingleFixed64(UnsignedLong.MAX_VALUE.longValue()).build(); + + ProtoMessageValue protoMessageValue = + ProtoMessageValue.create( + testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER, false); + + assertThat(protoMessageValue.select("single_fixed64")).isEqualTo(UnsignedLong.MAX_VALUE); + } + @SuppressWarnings("ImmutableEnumChecker") // Test only private enum SelectFieldJsonValueTestCase { NULL(Value.newBuilder().build(), NullValue.NULL_VALUE), From 95faaa88a1db8912e73af14a13a61bcbe795538a Mon Sep 17 00:00:00 2001 From: Andrew Parmet Date: Sat, 9 May 2026 09:53:24 -0400 Subject: [PATCH 087/204] Preserve FieldMask as a message in the CelValue runtime path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BaseProtoCelValueConverter.fromWellKnownProto converts FieldMask to a comma-separated string of its paths. That's correct in JSON-assignment contexts (handled separately by CelProtoJsonAdapter), but it breaks CEL expressions that access fields on the FieldMask itself — e.g. `fieldMask.paths` — because the string has no field `paths`. The CEL spec's WKT conversion table does not list FieldMask. cel-go treats it as a regular message, as does cel-java's legacy ProtoLiteAdapter.adaptValueToWellKnownProto for non-JSON contexts. Override fromWellKnownProto in ProtoCelValueConverter to preserve FieldMask as a ProtoMessageValue. JSON-assignment paths (e.g. TestAllTypes{single_value: FieldMask{...}}) are unaffected because EvalCreateStruct and CelValueRuntimeTypeProvider.createMessage both unwrap StructValue results before the outer assignment runs, and the outer assignment goes through CelProtoJsonAdapter.adaptValueToJsonValue which handles FieldMask → string conversion at that layer. --- .../common/values/ProtoCelValueConverter.java | 3 +++ .../common/values/ProtoMessageValueTest.java | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java index 948df759c..89d1b708f 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java @@ -71,6 +71,9 @@ protected Object fromWellKnownProto(MessageLiteOrBuilder msg, WellKnownProto wel "Unpacking failed for message: " + message.getDescriptorForType().getFullName(), e); } return toRuntimeValue(unpackedMessage); + case FIELD_MASK: + return ProtoMessageValue.create( + (Message) message, celDescriptorPool, this, celOptions.enableJsonFieldNames()); default: return super.fromWellKnownProto(message, wellKnownProto); } diff --git a/common/src/test/java/dev/cel/common/values/ProtoMessageValueTest.java b/common/src/test/java/dev/cel/common/values/ProtoMessageValueTest.java index 365dd32b4..c25a8ea06 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoMessageValueTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoMessageValueTest.java @@ -303,6 +303,24 @@ public void selectField_durationOutOfRange_success(int seconds, int nanos) { .isEqualTo(Duration.ofSeconds(seconds, nanos)); } + @Test + public void selectField_fieldMask_returnsProtoMessageValue() { + TestAllTypes testAllTypes = + TestAllTypes.newBuilder() + .setFieldMask( + com.google.protobuf.FieldMask.newBuilder().addPaths("foo").addPaths("bar")) + .build(); + + ProtoMessageValue protoMessageValue = + ProtoMessageValue.create( + testAllTypes, DefaultDescriptorPool.INSTANCE, PROTO_CEL_VALUE_CONVERTER, false); + + Object selected = protoMessageValue.select("field_mask"); + assertThat(selected).isInstanceOf(ProtoMessageValue.class); + assertThat(((ProtoMessageValue) selected).select("paths")) + .isEqualTo(ImmutableList.of("foo", "bar")); + } + @SuppressWarnings("ImmutableEnumChecker") // Test only private enum SelectFieldJsonValueTestCase { NULL(Value.newBuilder().build(), NullValue.NULL_VALUE), From c71923c449deaab440353d0dbd98a83d920cfc84 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 2 Jun 2026 10:00:29 -0700 Subject: [PATCH 088/204] Add conformance test cases for policy variable scoping fixes PiperOrigin-RevId: 925421169 --- .../src/test/java/dev/cel/conformance/policy/BUILD.bazel | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel index e4d80eccf..7b8427a2b 100644 --- a/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel @@ -32,5 +32,10 @@ java_library( cel_policy_conformance_test_java( name = "policy_conformance_tests", + skip_tests = [ + "nested_rules_variable_shadowing", + "variable_type_propagation", + "unconditional_rules", + ], testdata = "@cel_policy//conformance:testdata", ) From 8f3d87157c026a4a8c4af62ca9b9f9ba0062cbb5 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 2 Jun 2026 11:25:15 -0700 Subject: [PATCH 089/204] Fix dead code reachability logic erroneously flagging on unconditional nested rules Port of https://github.com/google/cel-go/pull/1323 PiperOrigin-RevId: 925478882 --- .../dev/cel/conformance/policy/BUILD.bazel | 5 ---- .../java/dev/cel/policy/CelCompiledRule.java | 15 ++++++----- .../dev/cel/policy/CelPolicyCompilerImpl.java | 11 ++++++-- .../cel/policy/CelPolicyCompilerImplTest.java | 26 +++++++++++++++++++ 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel index 7b8427a2b..e4d80eccf 100644 --- a/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel @@ -32,10 +32,5 @@ java_library( cel_policy_conformance_test_java( name = "policy_conformance_tests", - skip_tests = [ - "nested_rules_variable_shadowing", - "variable_type_propagation", - "unconditional_rules", - ], testdata = "@cel_policy//conformance:testdata", ) diff --git a/policy/src/main/java/dev/cel/policy/CelCompiledRule.java b/policy/src/main/java/dev/cel/policy/CelCompiledRule.java index 36f1685fc..af40bd74f 100644 --- a/policy/src/main/java/dev/cel/policy/CelCompiledRule.java +++ b/policy/src/main/java/dev/cel/policy/CelCompiledRule.java @@ -52,14 +52,17 @@ public boolean hasOptionalOutput() { for (CelCompiledMatch match : matches()) { if (match.result().kind().equals(CelCompiledMatch.Result.Kind.RULE) && match.result().rule().hasOptionalOutput()) { - return true; - } - - if (match.isConditionTriviallyTrue()) { + // If the nested rule is unconditional, the matching may fallthrough to the next match + // in this context (unwrapping the optional value from the nested rule). + if (!match.isConditionTriviallyTrue()) { + return true; + } + isOptionalOutput = true; + } else if (match.isConditionTriviallyTrue()) { return false; + } else { + isOptionalOutput = true; } - - isOptionalOutput = true; } return isOptionalOutput; diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java index 7841b9827..37a79f98a 100644 --- a/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java +++ b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java @@ -249,14 +249,21 @@ private CelCompiledRule compileRuleImpl( } private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext compilerContext) { - boolean ruleHasOptional = compiledRule.hasOptionalOutput(); ImmutableList compiledMatches = compiledRule.matches(); int matchCount = compiledMatches.size(); for (int i = matchCount - 1; i >= 0; i--) { CelCompiledMatch compiledMatch = compiledMatches.get(i); boolean isTriviallyTrue = compiledMatch.isConditionTriviallyTrue(); - if (isTriviallyTrue && !ruleHasOptional && i != matchCount - 1) { + // If the match is a single output or a nested rule that always returns a value, it is + // exhaustive. If the condition is trivially true, then all subsequent branches are + // unreachable. + boolean isExhaustive = + isTriviallyTrue + && (compiledMatch.result().kind().equals(Kind.OUTPUT) + || !compiledMatch.result().rule().hasOptionalOutput()); + + if (isExhaustive && i != matchCount - 1) { if (compiledMatch.result().kind().equals(Kind.OUTPUT)) { compilerContext.addIssue( compiledMatch.sourceId(), diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java index e9c2afed5..73950069f 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java @@ -111,6 +111,32 @@ public void compileYamlPolicy_withImportsOnNestedRules() throws Exception { assertThat(ast.getResultType()).isEqualTo(OptionalType.create(SimpleType.BOOL)); } + @Test + public void compileYamlPolicy_nestedRuleOptionalFallbackDivergence() throws Exception { + Cel cel = newCel().toCelBuilder().addVar("input_val", SimpleType.INT).build(); + String policySource = + "name: grandparent_policy\n" + + "rule:\n" + + " match:\n" + + " - condition: 'input_val == 2'\n" // conditional grandparent match + + " rule:\n" + + " id: parent_rule\n" + + " match:\n" + + " - condition: 'input_val == 1'\n" // conditional parent match + + " rule:\n" + + " id: nested_rule\n" + + " match:\n" + + " - condition: 'input_val == 3'\n" + + " output: 'true'\n" + + " - output: 'true'\n" // fallback (optional) + + " - output: 'true'\n"; + CelPolicy policy = POLICY_PARSER.parse(policySource); + CelAbstractSyntaxTree ast = + CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy); + + assertThat(ast.getResultType()).isEqualTo(OptionalType.create(SimpleType.BOOL)); + } + @Test public void compileYamlPolicy_containsCompilationError_throws( @TestParameter TestErrorYamlPolicy testCase) throws Exception { From 984193b7ee46cd7648988248d37c3da111caef4b Mon Sep 17 00:00:00 2001 From: Andrew Parmet Date: Tue, 2 Jun 2026 16:13:23 -0400 Subject: [PATCH 090/204] Wrap FieldMask via shared message-to-struct hook on the base converter Move the FieldMask case into BaseProtoCelValueConverter, delegating to a new fromProtoMessageToStructValue hook that both the full and lite converters implement. This reuses the existing non-well-known message wrapping path (removing the duplicated ProtoMessageValue.create call) and extends the fix to the lite runtime. Adds a lite-converter test. --- .../values/BaseProtoCelValueConverter.java | 20 +++++++------------ .../common/values/ProtoCelValueConverter.java | 12 ++++++----- .../values/ProtoLiteCelValueConverter.java | 12 +++++++++++ .../ProtoLiteCelValueConverterTest.java | 12 +++++++++++ 4 files changed, 38 insertions(+), 18 deletions(-) diff --git a/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java b/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java index 6851deed5..b39fa6831 100644 --- a/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java @@ -17,8 +17,6 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; -import com.google.common.base.CaseFormat; -import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -29,7 +27,6 @@ import com.google.protobuf.BytesValue; import com.google.protobuf.DoubleValue; import com.google.protobuf.Duration; -import com.google.protobuf.FieldMask; import com.google.protobuf.FloatValue; import com.google.protobuf.Int32Value; import com.google.protobuf.Int64Value; @@ -44,8 +41,6 @@ import dev.cel.common.annotations.Internal; import dev.cel.common.internal.ProtoTimeUtils; import dev.cel.common.internal.WellKnownProto; -import java.util.ArrayList; -import java.util.List; /** * {@code BaseProtoCelValueConverter} contains the common logic for converting between native Java @@ -73,6 +68,12 @@ public Object toRuntimeValue(Object value) { return super.toRuntimeValue(value); } + /** Wraps a protobuf message as a navigable struct value. */ + protected StructValue fromProtoMessageToStructValue(MessageLiteOrBuilder message) { + throw new UnsupportedOperationException( + "This converter does not support wrapping protobuf messages as struct values."); + } + protected Object fromWellKnownProto(MessageLiteOrBuilder message, WellKnownProto wellKnownProto) { switch (wellKnownProto) { case JSON_VALUE: @@ -104,14 +105,7 @@ protected Object fromWellKnownProto(MessageLiteOrBuilder message, WellKnownProto case UINT64_VALUE: return UnsignedLong.fromLongBits(((UInt64Value) message).getValue()); case FIELD_MASK: - FieldMask fieldMask = (FieldMask) message; - List paths = new ArrayList<>(fieldMask.getPathsCount()); - for (String path : fieldMask.getPathsList()) { - if (!path.isEmpty()) { - paths.add(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, path)); - } - } - return normalizePrimitive(Joiner.on(",").join(paths)); + return fromProtoMessageToStructValue(message); case EMPTY: return ImmutableMap.of(); default: diff --git a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java index 89d1b708f..20f4ee0d1 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java @@ -71,14 +71,17 @@ protected Object fromWellKnownProto(MessageLiteOrBuilder msg, WellKnownProto wel "Unpacking failed for message: " + message.getDescriptorForType().getFullName(), e); } return toRuntimeValue(unpackedMessage); - case FIELD_MASK: - return ProtoMessageValue.create( - (Message) message, celDescriptorPool, this, celOptions.enableJsonFieldNames()); default: return super.fromWellKnownProto(message, wellKnownProto); } } + @Override + protected ProtoMessageValue fromProtoMessageToStructValue(MessageLiteOrBuilder message) { + return ProtoMessageValue.create( + (Message) message, celDescriptorPool, this, celOptions.enableJsonFieldNames()); + } + @Override public Object toRuntimeValue(Object value) { if (value instanceof EnumValueDescriptor) { @@ -102,8 +105,7 @@ public Object toRuntimeValue(Object value) { WellKnownProto wellKnownProto = WellKnownProto.getByTypeName(message.getDescriptorForType().getFullName()).orElse(null); if (wellKnownProto == null) { - return ProtoMessageValue.create( - message, celDescriptorPool, this, celOptions.enableJsonFieldNames()); + return fromProtoMessageToStructValue(message); } return fromWellKnownProto(message, wellKnownProto); diff --git a/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java index 3fbb0ad75..3c226f7bb 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java @@ -28,6 +28,7 @@ import com.google.protobuf.CodedInputStream; import com.google.protobuf.ExtensionRegistryLite; import com.google.protobuf.MessageLite; +import com.google.protobuf.MessageLiteOrBuilder; import com.google.protobuf.WireFormat; import dev.cel.common.annotations.Internal; import dev.cel.common.internal.CelLiteDescriptorPool; @@ -184,6 +185,17 @@ public Object toRuntimeValue(Object value) { return super.toRuntimeValue(value); } + @Override + protected ProtoMessageLiteValue fromProtoMessageToStructValue(MessageLiteOrBuilder message) { + MessageLite msg = (MessageLite) message; + MessageLiteDescriptor descriptor = + descriptorPool + .findDescriptor(msg) + .orElseThrow( + () -> new NoSuchElementException("Could not find a descriptor for: " + msg)); + return ProtoMessageLiteValue.create(msg, descriptor.getProtoTypeName(), this); + } + private Object getDefaultValue(FieldLiteDescriptor fieldDescriptor) { EncodingType encodingType = fieldDescriptor.getEncodingType(); switch (encodingType) { diff --git a/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java b/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java index cec3e0fbf..3b66171e4 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java @@ -27,6 +27,7 @@ import com.google.protobuf.DoubleValue; import com.google.protobuf.Duration; import com.google.protobuf.ExtensionRegistryLite; +import com.google.protobuf.FieldMask; import com.google.protobuf.FloatValue; import com.google.protobuf.Int32Value; import com.google.protobuf.Int64Value; @@ -104,6 +105,17 @@ public void fromProtoMessageToCelValue_withWellKnownProto_convertsToPrimitivesFr assertThat(adaptedValue).isEqualTo(testCase.value); } + @Test + public void fromProtoMessageToCelValue_fieldMask_returnsProtoMessageLiteValue() { + FieldMask fieldMask = FieldMask.newBuilder().addPaths("foo").addPaths("bar").build(); + + Object adaptedValue = PROTO_LITE_CEL_VALUE_CONVERTER.toRuntimeValue(fieldMask); + + assertThat(adaptedValue).isInstanceOf(ProtoMessageLiteValue.class); + assertThat(((ProtoMessageLiteValue) adaptedValue).select("paths")) + .isEqualTo(ImmutableList.of("foo", "bar")); + } + /** Test cases for repeated_int64: 1L,2L,3L */ @SuppressWarnings("ImmutableEnumChecker") // Test only private enum RepeatedFieldBytesTestCase { From 7f26293ffe9bfb15d3dc3e355b6c957a45b3c60c Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 2 Jun 2026 13:25:52 -0700 Subject: [PATCH 091/204] Internal Changes PiperOrigin-RevId: 925544807 --- checker/src/test/java/dev/cel/checker/BUILD.bazel | 4 ++-- .../src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel | 2 +- runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/checker/src/test/java/dev/cel/checker/BUILD.bazel b/checker/src/test/java/dev/cel/checker/BUILD.bazel index 3eb64bd11..22b70210d 100644 --- a/checker/src/test/java/dev/cel/checker/BUILD.bazel +++ b/checker/src/test/java/dev/cel/checker/BUILD.bazel @@ -13,8 +13,6 @@ java_library( srcs = glob(["*Test.java"]), resources = ["//checker/src/test/resources:baselines"], deps = [ - # "//java/com/google/testing/testsize:annotations", - "//:auto_value", "//checker", "//checker:cel_ident_decl", "//checker:checker_builder", @@ -44,9 +42,11 @@ java_library( "//common/types:type_providers", "//compiler", "//compiler:compiler_builder", + # "//java/com/google/testing/testsize:annotations", "//parser:macro", "//testing:adorner", "//testing:cel_baseline_test_case", + "//:auto_value", "@maven//:junit_junit", "@maven//:com_google_testparameterinjector_test_parameter_injector", "//:java_truth", diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel index 393014a7d..53d72de67 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -11,7 +11,6 @@ java_library( srcs = glob(["*.java"]), resources = ["//optimizer/src/test/resources:baselines"], deps = [ - # "//java/com/google/testing/testsize:annotations", "//bundle:cel", "//common:cel_ast", "//common:cel_source", @@ -24,6 +23,7 @@ java_library( "//common/types", "//extensions", "//extensions:optional_library", + # "//java/com/google/testing/testsize:annotations", "//optimizer", "//optimizer:optimization_exception", "//optimizer:optimizer_builder", diff --git a/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel index bccba6b6e..29f08eb74 100644 --- a/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel @@ -12,13 +12,13 @@ java_library( testonly = True, srcs = glob(["*Test.java"]), deps = [ - # "//java/com/google/testing/testsize:annotations", "//bundle:cel", "//common:cel_ast", "//common:container", "//common:options", "//common/testing", "//common/types", + # "//java/com/google/testing/testsize:annotations", "//runtime", "//runtime:unknown_attributes", "//runtime:unknown_options", From b9e76392320dff2157df97824829fa83bbd2eecc Mon Sep 17 00:00:00 2001 From: Andrew Parmet Date: Tue, 2 Jun 2026 19:58:49 -0400 Subject: [PATCH 092/204] Revert "Wrap FieldMask via shared message-to-struct hook on the base converter" This reverts commit 984193b7ee46cd7648988248d37c3da111caef4b. --- .../values/BaseProtoCelValueConverter.java | 20 ++++++++++++------- .../common/values/ProtoCelValueConverter.java | 12 +++++------ .../values/ProtoLiteCelValueConverter.java | 12 ----------- .../ProtoLiteCelValueConverterTest.java | 12 ----------- 4 files changed, 18 insertions(+), 38 deletions(-) diff --git a/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java b/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java index b39fa6831..6851deed5 100644 --- a/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java @@ -17,6 +17,8 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; +import com.google.common.base.CaseFormat; +import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -27,6 +29,7 @@ import com.google.protobuf.BytesValue; import com.google.protobuf.DoubleValue; import com.google.protobuf.Duration; +import com.google.protobuf.FieldMask; import com.google.protobuf.FloatValue; import com.google.protobuf.Int32Value; import com.google.protobuf.Int64Value; @@ -41,6 +44,8 @@ import dev.cel.common.annotations.Internal; import dev.cel.common.internal.ProtoTimeUtils; import dev.cel.common.internal.WellKnownProto; +import java.util.ArrayList; +import java.util.List; /** * {@code BaseProtoCelValueConverter} contains the common logic for converting between native Java @@ -68,12 +73,6 @@ public Object toRuntimeValue(Object value) { return super.toRuntimeValue(value); } - /** Wraps a protobuf message as a navigable struct value. */ - protected StructValue fromProtoMessageToStructValue(MessageLiteOrBuilder message) { - throw new UnsupportedOperationException( - "This converter does not support wrapping protobuf messages as struct values."); - } - protected Object fromWellKnownProto(MessageLiteOrBuilder message, WellKnownProto wellKnownProto) { switch (wellKnownProto) { case JSON_VALUE: @@ -105,7 +104,14 @@ protected Object fromWellKnownProto(MessageLiteOrBuilder message, WellKnownProto case UINT64_VALUE: return UnsignedLong.fromLongBits(((UInt64Value) message).getValue()); case FIELD_MASK: - return fromProtoMessageToStructValue(message); + FieldMask fieldMask = (FieldMask) message; + List paths = new ArrayList<>(fieldMask.getPathsCount()); + for (String path : fieldMask.getPathsList()) { + if (!path.isEmpty()) { + paths.add(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, path)); + } + } + return normalizePrimitive(Joiner.on(",").join(paths)); case EMPTY: return ImmutableMap.of(); default: diff --git a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java index 20f4ee0d1..89d1b708f 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java @@ -71,17 +71,14 @@ protected Object fromWellKnownProto(MessageLiteOrBuilder msg, WellKnownProto wel "Unpacking failed for message: " + message.getDescriptorForType().getFullName(), e); } return toRuntimeValue(unpackedMessage); + case FIELD_MASK: + return ProtoMessageValue.create( + (Message) message, celDescriptorPool, this, celOptions.enableJsonFieldNames()); default: return super.fromWellKnownProto(message, wellKnownProto); } } - @Override - protected ProtoMessageValue fromProtoMessageToStructValue(MessageLiteOrBuilder message) { - return ProtoMessageValue.create( - (Message) message, celDescriptorPool, this, celOptions.enableJsonFieldNames()); - } - @Override public Object toRuntimeValue(Object value) { if (value instanceof EnumValueDescriptor) { @@ -105,7 +102,8 @@ public Object toRuntimeValue(Object value) { WellKnownProto wellKnownProto = WellKnownProto.getByTypeName(message.getDescriptorForType().getFullName()).orElse(null); if (wellKnownProto == null) { - return fromProtoMessageToStructValue(message); + return ProtoMessageValue.create( + message, celDescriptorPool, this, celOptions.enableJsonFieldNames()); } return fromWellKnownProto(message, wellKnownProto); diff --git a/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java index 3c226f7bb..3fbb0ad75 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java @@ -28,7 +28,6 @@ import com.google.protobuf.CodedInputStream; import com.google.protobuf.ExtensionRegistryLite; import com.google.protobuf.MessageLite; -import com.google.protobuf.MessageLiteOrBuilder; import com.google.protobuf.WireFormat; import dev.cel.common.annotations.Internal; import dev.cel.common.internal.CelLiteDescriptorPool; @@ -185,17 +184,6 @@ public Object toRuntimeValue(Object value) { return super.toRuntimeValue(value); } - @Override - protected ProtoMessageLiteValue fromProtoMessageToStructValue(MessageLiteOrBuilder message) { - MessageLite msg = (MessageLite) message; - MessageLiteDescriptor descriptor = - descriptorPool - .findDescriptor(msg) - .orElseThrow( - () -> new NoSuchElementException("Could not find a descriptor for: " + msg)); - return ProtoMessageLiteValue.create(msg, descriptor.getProtoTypeName(), this); - } - private Object getDefaultValue(FieldLiteDescriptor fieldDescriptor) { EncodingType encodingType = fieldDescriptor.getEncodingType(); switch (encodingType) { diff --git a/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java b/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java index 3b66171e4..cec3e0fbf 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java @@ -27,7 +27,6 @@ import com.google.protobuf.DoubleValue; import com.google.protobuf.Duration; import com.google.protobuf.ExtensionRegistryLite; -import com.google.protobuf.FieldMask; import com.google.protobuf.FloatValue; import com.google.protobuf.Int32Value; import com.google.protobuf.Int64Value; @@ -105,17 +104,6 @@ public void fromProtoMessageToCelValue_withWellKnownProto_convertsToPrimitivesFr assertThat(adaptedValue).isEqualTo(testCase.value); } - @Test - public void fromProtoMessageToCelValue_fieldMask_returnsProtoMessageLiteValue() { - FieldMask fieldMask = FieldMask.newBuilder().addPaths("foo").addPaths("bar").build(); - - Object adaptedValue = PROTO_LITE_CEL_VALUE_CONVERTER.toRuntimeValue(fieldMask); - - assertThat(adaptedValue).isInstanceOf(ProtoMessageLiteValue.class); - assertThat(((ProtoMessageLiteValue) adaptedValue).select("paths")) - .isEqualTo(ImmutableList.of("foo", "bar")); - } - /** Test cases for repeated_int64: 1L,2L,3L */ @SuppressWarnings("ImmutableEnumChecker") // Test only private enum RepeatedFieldBytesTestCase { From abbd7cf0ef4b6c4f3fe6f27d0cac41842216fb62 Mon Sep 17 00:00:00 2001 From: Andrew Parmet Date: Tue, 2 Jun 2026 20:00:11 -0400 Subject: [PATCH 093/204] Override fromWellKnownProto in ProtoLiteCelValueConverter for FieldMask Mirror the full converter: intercept FIELD_MASK in the lite converter and wrap it as a ProtoMessageLiteValue so it is navigable as a message on the lite runtime. Adds a lite-converter test. --- .../values/ProtoLiteCelValueConverter.java | 18 +++++++++++++++++- .../values/ProtoLiteCelValueConverterTest.java | 12 ++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java index 3fbb0ad75..64d6ec1d4 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java @@ -28,6 +28,7 @@ import com.google.protobuf.CodedInputStream; import com.google.protobuf.ExtensionRegistryLite; import com.google.protobuf.MessageLite; +import com.google.protobuf.MessageLiteOrBuilder; import com.google.protobuf.WireFormat; import dev.cel.common.annotations.Internal; import dev.cel.common.internal.CelLiteDescriptorPool; @@ -178,12 +179,27 @@ public Object toRuntimeValue(Object value) { return ProtoMessageLiteValue.create(msg, descriptor.getProtoTypeName(), this); } - return super.fromWellKnownProto(msg, wellKnownProto); + return fromWellKnownProto(msg, wellKnownProto); } return super.toRuntimeValue(value); } + @Override + protected Object fromWellKnownProto(MessageLiteOrBuilder msg, WellKnownProto wellKnownProto) { + if (wellKnownProto == WellKnownProto.FIELD_MASK) { + MessageLite message = (MessageLite) msg; + MessageLiteDescriptor descriptor = + descriptorPool + .findDescriptor(message) + .orElseThrow( + () -> new NoSuchElementException("Could not find a descriptor for: " + message)); + return ProtoMessageLiteValue.create(message, descriptor.getProtoTypeName(), this); + } + + return super.fromWellKnownProto(msg, wellKnownProto); + } + private Object getDefaultValue(FieldLiteDescriptor fieldDescriptor) { EncodingType encodingType = fieldDescriptor.getEncodingType(); switch (encodingType) { diff --git a/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java b/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java index cec3e0fbf..3b66171e4 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java @@ -27,6 +27,7 @@ import com.google.protobuf.DoubleValue; import com.google.protobuf.Duration; import com.google.protobuf.ExtensionRegistryLite; +import com.google.protobuf.FieldMask; import com.google.protobuf.FloatValue; import com.google.protobuf.Int32Value; import com.google.protobuf.Int64Value; @@ -104,6 +105,17 @@ public void fromProtoMessageToCelValue_withWellKnownProto_convertsToPrimitivesFr assertThat(adaptedValue).isEqualTo(testCase.value); } + @Test + public void fromProtoMessageToCelValue_fieldMask_returnsProtoMessageLiteValue() { + FieldMask fieldMask = FieldMask.newBuilder().addPaths("foo").addPaths("bar").build(); + + Object adaptedValue = PROTO_LITE_CEL_VALUE_CONVERTER.toRuntimeValue(fieldMask); + + assertThat(adaptedValue).isInstanceOf(ProtoMessageLiteValue.class); + assertThat(((ProtoMessageLiteValue) adaptedValue).select("paths")) + .isEqualTo(ImmutableList.of("foo", "bar")); + } + /** Test cases for repeated_int64: 1L,2L,3L */ @SuppressWarnings("ImmutableEnumChecker") // Test only private enum RepeatedFieldBytesTestCase { From c5d86d9c46cdabb35c32bfd098bc6dd3f5b040c4 Mon Sep 17 00:00:00 2001 From: Andrew Parmet Date: Tue, 2 Jun 2026 20:39:10 -0400 Subject: [PATCH 094/204] Remove redundant base FieldMask handler now that both converters intercept it --- .../common/values/BaseProtoCelValueConverter.java | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java b/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java index 6851deed5..9fc218abe 100644 --- a/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java @@ -17,8 +17,6 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; -import com.google.common.base.CaseFormat; -import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -29,7 +27,6 @@ import com.google.protobuf.BytesValue; import com.google.protobuf.DoubleValue; import com.google.protobuf.Duration; -import com.google.protobuf.FieldMask; import com.google.protobuf.FloatValue; import com.google.protobuf.Int32Value; import com.google.protobuf.Int64Value; @@ -44,8 +41,6 @@ import dev.cel.common.annotations.Internal; import dev.cel.common.internal.ProtoTimeUtils; import dev.cel.common.internal.WellKnownProto; -import java.util.ArrayList; -import java.util.List; /** * {@code BaseProtoCelValueConverter} contains the common logic for converting between native Java @@ -103,15 +98,6 @@ protected Object fromWellKnownProto(MessageLiteOrBuilder message, WellKnownProto return UnsignedLong.valueOf(((UInt32Value) message).getValue()); case UINT64_VALUE: return UnsignedLong.fromLongBits(((UInt64Value) message).getValue()); - case FIELD_MASK: - FieldMask fieldMask = (FieldMask) message; - List paths = new ArrayList<>(fieldMask.getPathsCount()); - for (String path : fieldMask.getPathsList()) { - if (!path.isEmpty()) { - paths.add(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, path)); - } - } - return normalizePrimitive(Joiner.on(",").join(paths)); case EMPTY: return ImmutableMap.of(); default: From 02052f3dd2fdbd61e5330d16ed7ac6cbc26beee8 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 9 Jun 2026 09:18:56 -0700 Subject: [PATCH 095/204] Safely ignore enums in native type extensions Ref: https://github.com/google/cel-java/issues/1077 PiperOrigin-RevId: 929234496 --- .../extensions/CelNativeTypesExtensions.java | 8 ++++ .../main/java/dev/cel/extensions/README.md | 1 + .../CelNativeTypesExtensionsTest.java | 47 +++++++++++++++++-- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java index ae9483f7c..44fcdd1f6 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java @@ -82,6 +82,10 @@ public final class CelNativeTypesExtensions implements CelCompilerLibrary, CelRu private static final ImmutableSet OBJECT_METHOD_NAMES = stream(Object.class.getDeclaredMethods()).map(Method::getName).collect(toImmutableSet()); + // Set of all standard java.lang.Enum method names. + private static final ImmutableSet ENUM_METHOD_NAMES = + stream(Enum.class.getDeclaredMethods()).map(Method::getName).collect(toImmutableSet()); + private static final ImmutableMap, CelType> JAVA_TO_CEL_TYPE_MAP = ImmutableMap., CelType>builder() .put(boolean.class, SimpleType.BOOL) @@ -606,6 +610,10 @@ private static boolean isGetter(Method method) { if (OBJECT_METHOD_NAMES.contains(name)) { return false; } + if (Enum.class.isAssignableFrom(method.getDeclaringClass()) + && ENUM_METHOD_NAMES.contains(name)) { + return false; + } if (name.startsWith("get")) { return name.length() > 3; } diff --git a/extensions/src/main/java/dev/cel/extensions/README.md b/extensions/src/main/java/dev/cel/extensions/README.md index fcf019d15..5b75bb48d 100644 --- a/extensions/src/main/java/dev/cel/extensions/README.md +++ b/extensions/src/main/java/dev/cel/extensions/README.md @@ -1122,6 +1122,7 @@ The type-mapping between Java and CEL is as follows: * This is only supported for the planner runtime (e.g., `CelRuntimeFactory.plannerRuntimeBuilder()`). * Native Java arrays (except `byte[]`) are not supported. Use `java.util.List` instead. +* Java `enum` properties are not currently supported and will be safely ignored during scanning. * If there is a name collision with a Protobuf type, the protobuf type will take precedence. * Instantiating new struct values (e.g., `Account{id: 1234}`) requires the class to have a no-argument constructor (public, protected, package-private, or private). * Final fields are supported only in a **read-only** capacity; they cannot be populated when instantiating new struct values. diff --git a/extensions/src/test/java/dev/cel/extensions/CelNativeTypesExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelNativeTypesExtensionsTest.java index dcd3e811c..5485989af 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelNativeTypesExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelNativeTypesExtensionsTest.java @@ -89,7 +89,8 @@ public final class CelNativeTypesExtensionsTest { TestNestedSimplePojo.class, TestGetterFieldTypeMismatchPojo.class, TestAbstractPojo.class, - TestURLPojo.class); + TestURLPojo.class, + PojoWithEnum.class); private static final Cel CEL = CelFactory.plannerCelBuilder() @@ -564,16 +565,24 @@ public void nativeTypes_prefixLessGetter_success() throws Exception { .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) .addLibraries(extensions) .build(); - CelAbstractSyntaxTree ast = + CelAbstractSyntaxTree valueAst = celCompiler .compile( "dev.cel.extensions.CelNativeTypesExtensionsTest.TestPrefixLessGetterPojo{}.value") .getAst(); - CelRuntime.Program program = celRuntime.createProgram(ast); + CelAbstractSyntaxTree nameAst = + celCompiler + .compile( + "dev.cel.extensions.CelNativeTypesExtensionsTest.TestPrefixLessGetterPojo{}.name") + .getAst(); + CelRuntime.Program valueProgram = celRuntime.createProgram(valueAst); + CelRuntime.Program nameProgram = celRuntime.createProgram(nameAst); - Object result = program.eval(); + Object valueResult = valueProgram.eval(); + Object nameResult = nameProgram.eval(); - assertThat(result).isEqualTo("hello"); + assertThat(valueResult).isEqualTo("hello"); + assertThat(nameResult).isEqualTo("my_name"); } @Test @@ -1201,10 +1210,15 @@ public static final class TestPrivateFieldPojo { public static class TestPrefixLessGetterPojo { private String value = "hello"; + private String name = "my_name"; public String value() { return value; } + + public String name() { + return name; + } } public static class TestParentPojo { @@ -1346,4 +1360,27 @@ public String getMismatchField() { return "mismatch"; } } + + public enum TestEnum { + FOO, + BAR; + } + + public static class PojoWithEnum { + private TestEnum enumVal = TestEnum.FOO; + + public TestEnum getEnumVal() { + return enumVal; + } + + public void setEnumVal(TestEnum val) { + this.enumVal = val; + } + } + + @Test + public void nativeTypes_enumSafelyIgnored() throws Exception { + assertThat(eval("PojoWithEnum{}.enumVal")).isNotNull(); + } + } From 359d39a9338134341711ea37205b36856a432407 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 9 Jun 2026 11:28:44 -0700 Subject: [PATCH 096/204] Expand support to arrays to Native type extensions Closes https://github.com/google/cel-java/issues/1059 PiperOrigin-RevId: 929309516 --- .../main/java/dev/cel/extensions/BUILD.bazel | 1 + .../extensions/CelNativeTypesExtensions.java | 70 +++++++++- .../main/java/dev/cel/extensions/README.md | 4 +- .../test/java/dev/cel/extensions/BUILD.bazel | 1 + .../CelNativeTypesExtensionsTest.java | 121 +++++++++++++++--- 5 files changed, 174 insertions(+), 23 deletions(-) diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel index 73bab08c9..b25fdf16d 100644 --- a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel @@ -329,6 +329,7 @@ java_library( deps = [ "//checker:checker_builder", "//common/exceptions:attribute_not_found", + "//common/exceptions:invalid_argument", "//common/internal:reflection_util", "//common/types", "//common/types:type_providers", diff --git a/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java index 44fcdd1f6..f150a8437 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java @@ -28,6 +28,7 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.checker.CelCheckerBuilder; import dev.cel.common.exceptions.CelAttributeNotFoundException; +import dev.cel.common.exceptions.CelInvalidArgumentException; import dev.cel.common.internal.ReflectionUtil; import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypeProvider; @@ -47,6 +48,7 @@ import dev.cel.runtime.CelRuntimeLibrary; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -289,6 +291,15 @@ private static CelType mapJavaTypeToCelType( return celType; } + if (type.isArray()) { + TypeToken token = TypeToken.of(genericType); + TypeToken componentToken = + Preconditions.checkNotNull( + token.getComponentType(), "Array component type cannot be null"); + return ListType.create( + mapJavaTypeToCelType(componentToken.getRawType(), componentToken.getType(), classMap)); + } + if (type.isInterface() && !List.class.isAssignableFrom(type) && !Map.class.isAssignableFrom(type)) { @@ -416,6 +427,14 @@ private void discover(Type type) { TypeToken token = TypeToken.of(type); Class rawType = token.getRawType(); + if (rawType.isArray()) { + TypeToken componentToken = + Preconditions.checkNotNull( + token.getComponentType(), "Array component type cannot be null"); + discover(componentToken.getType()); + return; + } + if (List.class.isAssignableFrom(rawType)) { discover(ReflectionUtil.resolveGenericParameter(token, List.class, 0)); return; @@ -775,6 +794,9 @@ private static Object getDefaultValue(Class targetType) { if (Map.class.isAssignableFrom(targetType)) { return ImmutableMap.of(); } + if (targetType.isArray()) { + return Array.newInstance(targetType.getComponentType(), 0); + } try { Constructor constructor = targetType.getDeclaredConstructor(); @@ -822,6 +844,10 @@ public Object toRuntimeValue(Object value) { return new PojoStructValue(value, accessors, registry.classToTypeMap.get(clazz)); } + if (clazz.isArray() && clazz != byte[].class) { + return convertArrayToList(value); + } + return super.toRuntimeValue(value); } @@ -844,8 +870,14 @@ Object toNative(Object value, Class targetType, Type genericType) { return ((CelByteString) value).toByteArray(); } - if (List.class.isAssignableFrom(targetType) && value instanceof List) { - return convertListToNative((List) value, targetType, genericType); + if (value instanceof List) { + List listValue = (List) value; + if (List.class.isAssignableFrom(targetType)) { + return convertListToNative(listValue, targetType, genericType); + } + if (targetType.isArray()) { + return convertListToArray(listValue, targetType, genericType); + } } if (Map.class.isAssignableFrom(targetType) && value instanceof Map) { @@ -857,7 +889,7 @@ Object toNative(Object value, Class targetType, Type genericType) { // Safe reflection collection cast. @SuppressWarnings("unchecked") - private Object convertListToNative(List list, Class targetType, Type genericType) { + private List convertListToNative(List list, Class targetType, Type genericType) { TypeToken token = TypeToken.of(genericType); Type elementType = ReflectionUtil.resolveGenericParameter(token, List.class, 0); Class componentType = ReflectionUtil.getRawType(elementType); @@ -909,7 +941,7 @@ private Object convertListToNative(List list, Class targetType, Type gener // Safe reflection collection cast. @SuppressWarnings("unchecked") - private Object convertMapToNative(Map map, Class targetType, Type genericType) { + private Map convertMapToNative(Map map, Class targetType, Type genericType) { TypeToken token = TypeToken.of(genericType); Type keyType = ReflectionUtil.resolveGenericParameter(token, Map.class, 0); Type valueType = ReflectionUtil.resolveGenericParameter(token, Map.class, 1); @@ -970,6 +1002,36 @@ private Object convertMapToNative(Map map, Class targetType, Type gener return builder.buildOrThrow(); } + private Object convertListToArray(List list, Class targetType, Type genericType) { + Class componentType = targetType.getComponentType(); + Object array = Array.newInstance(componentType, list.size()); + TypeToken token = TypeToken.of(genericType); + TypeToken componentToken = + Preconditions.checkNotNull( + token.getComponentType(), "Array component type cannot be null"); + Type componentGenericType = componentToken.getType(); + + for (int i = 0; i < list.size(); i++) { + Object element = list.get(i); + Object converted = toNative(element, componentType, componentGenericType); + Array.set(array, i, converted); + } + return array; + } + + private ImmutableList convertArrayToList(Object array) { + int length = Array.getLength(array); + ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(length); + for (int i = 0; i < length; i++) { + Object element = Array.get(array, i); + if (element == null) { + throw new CelInvalidArgumentException(String.format("Element at index %d is null.", i)); + } + builder.add(toRuntimeValue(element)); + } + return builder.build(); + } + private Object downcastPrimitives(Object value, Class targetType) { Class wrappedTargetType = Primitives.wrap(targetType); if (wrappedTargetType == Integer.class && value instanceof Long) { diff --git a/extensions/src/main/java/dev/cel/extensions/README.md b/extensions/src/main/java/dev/cel/extensions/README.md index 5b75bb48d..e6ee73aba 100644 --- a/extensions/src/main/java/dev/cel/extensions/README.md +++ b/extensions/src/main/java/dev/cel/extensions/README.md @@ -1114,14 +1114,14 @@ The type-mapping between Java and CEL is as follows: | `String` | `string` | | `java.time.Duration` | `duration` | | `java.time.Instant` | `timestamp` | -| `java.util.List` | `list` | +| `java.util.List`, `T[]` (except `byte[]`) | `list` | | `java.util.Map` | `map` | | `java.util.Optional` | `optional_type` | ### Notes * This is only supported for the planner runtime (e.g., `CelRuntimeFactory.plannerRuntimeBuilder()`). -* Native Java arrays (except `byte[]`) are not supported. Use `java.util.List` instead. +* Native Java arrays are supported. `byte[]` maps to `bytes`, while other arrays map to `list`. * Java `enum` properties are not currently supported and will be safely ignored during scanning. * If there is a name collision with a Protobuf type, the protobuf type will take precedence. * Instantiating new struct values (e.g., `Account{id: 1234}`) requires the class to have a no-argument constructor (public, protected, package-private, or private). diff --git a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel index 31720917f..9fda186cf 100644 --- a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel @@ -20,6 +20,7 @@ java_library( "//common/exceptions:attribute_not_found", "//common/exceptions:divide_by_zero", "//common/exceptions:index_out_of_bounds", + "//common/exceptions:invalid_argument", "//common/types", "//common/types:type_providers", "//common/values", diff --git a/extensions/src/test/java/dev/cel/extensions/CelNativeTypesExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelNativeTypesExtensionsTest.java index 5485989af..0b378f0d7 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelNativeTypesExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelNativeTypesExtensionsTest.java @@ -30,6 +30,7 @@ import dev.cel.common.CelContainer; import dev.cel.common.CelValidationException; import dev.cel.common.exceptions.CelAttributeNotFoundException; +import dev.cel.common.exceptions.CelInvalidArgumentException; import dev.cel.common.types.CelType; import dev.cel.common.types.ListType; import dev.cel.common.types.MapType; @@ -90,7 +91,8 @@ public final class CelNativeTypesExtensionsTest { TestGetterFieldTypeMismatchPojo.class, TestAbstractPojo.class, TestURLPojo.class, - PojoWithEnum.class); + PojoWithEnum.class, + TestArrayPojo.class); private static final Cel CEL = CelFactory.plannerCelBuilder() @@ -323,10 +325,10 @@ public void nativeTypes_anonymousClass_throwsException() { @Test public void nativeTypes_createStruct_privateConstructor() throws Exception { - Object result = eval("TestPrivateConstructorPojo{value:" + " 'hello'}"); + TestPrivateConstructorPojo result = + (TestPrivateConstructorPojo) eval("TestPrivateConstructorPojo{value:" + " 'hello'}"); - assertThat(result).isInstanceOf(TestPrivateConstructorPojo.class); - assertThat(((TestPrivateConstructorPojo) result).value).isEqualTo("hello"); + assertThat(result.value).isEqualTo("hello"); } @Test @@ -375,10 +377,9 @@ public void nativeTypes_missingNoArgConstructor_throws() throws Exception { @Test public void nativeTypes_createWithDeepConversion() throws Exception { - Object result = eval("TestDeepConversionPojo{ints: [1, 2], floats: {'a': 1.0, 'b': 2.0}}"); - - assertThat(result).isInstanceOf(TestDeepConversionPojo.class); - TestDeepConversionPojo pojo = (TestDeepConversionPojo) result; + TestDeepConversionPojo pojo = + (TestDeepConversionPojo) + eval("TestDeepConversionPojo{ints: [1, 2], floats: {'a': 1.0, 'b': 2.0}}"); assertThat(pojo.ints.get(0)).isEqualTo(1); assertThat(pojo.floats).containsEntry("a", 1.0f); } @@ -398,11 +399,92 @@ public void nativeTypes_unsupportedTypeSet_throwsOnRegistration() throws Excepti } @Test - public void nativeTypes_arrayType_throwsOnRegistration() throws Exception { - IllegalArgumentException e = + public void nativeTypes_arrayType_construction() throws Exception { + String expr = + "TestArrayPojo{" + + " strings: ['a', 'b']," + + " ints: [1, 2]," + + " nesteds: [TestNestedType{value: 'nested'}]," + + " matrix: [[1, 2], [3, 4]]," + + " nestedMatrix: [[TestNestedType{value: 'm1'}], [TestNestedType{value: 'm2'}]]," + + " byteArrays: [b'foo', b'bar']" + + "}"; + + TestArrayPojo pojo = (TestArrayPojo) eval(expr); + + assertThat(pojo.strings).isEqualTo(new String[] {"a", "b"}); + assertThat(pojo.ints).isEqualTo(new int[] {1, 2}); + assertThat(pojo.nesteds).hasLength(1); + assertThat(pojo.nesteds[0].value).isEqualTo("nested"); + assertThat(pojo.matrix).hasLength(2); + assertThat(pojo.matrix[0]).isEqualTo(new int[] {1, 2}); + assertThat(pojo.matrix[1]).isEqualTo(new int[] {3, 4}); + assertThat(pojo.nestedMatrix).hasLength(2); + assertThat(pojo.nestedMatrix[0][0].value).isEqualTo("m1"); + assertThat(pojo.nestedMatrix[1][0].value).isEqualTo("m2"); + assertThat(pojo.byteArrays).hasLength(2); + assertThat(pojo.byteArrays[0]).isEqualTo("foo".getBytes(UTF_8)); + assertThat(pojo.byteArrays[1]).isEqualTo("bar".getBytes(UTF_8)); + } + + @Test + public void nativeTypes_arrayType_selection() throws Exception { + CelNativeTypesExtensions extensions = CelExtensions.nativeTypes(TestArrayPojo.class); + Cel cel = + CelFactory.plannerCelBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addCompilerLibraries(extensions) + .addRuntimeLibraries(extensions) + .addVar("pojo", StructTypeReference.create(TestArrayPojo.class.getCanonicalName())) + .build(); + String expr = + "pojo.strings[1] == 'b'" + + " && pojo.ints[0] == 1" + + " && pojo.nesteds[0].value == 'nested'" + + " && pojo.matrix[1][0] == 3" + + " && pojo.nestedMatrix[1][0].value == 'm2'" + + " && pojo.byteArrays[1] == b'bar'"; + CelAbstractSyntaxTree ast = cel.compile(expr).getAst(); + CelRuntime.Program program = cel.createProgram(ast); + + TestArrayPojo input = new TestArrayPojo(); + input.strings = new String[] {"a", "b"}; + input.ints = new int[] {1, 2}; + TestNestedType nested = new TestNestedType(); + nested.value = "nested"; + input.nesteds = new TestNestedType[] {nested}; + input.matrix = new int[][] {{1, 2}, {3, 4}}; + TestNestedType m1 = new TestNestedType(); + m1.value = "m1"; + TestNestedType m2 = new TestNestedType(); + m2.value = "m2"; + input.nestedMatrix = new TestNestedType[][] {{m1}, {m2}}; + input.byteArrays = new byte[][] {"foo".getBytes(UTF_8), "bar".getBytes(UTF_8)}; + + assertThat(program.eval(ImmutableMap.of("pojo", input))).isEqualTo(true); + } + + @Test + public void nativeTypes_arrayWithNullElement_throws() throws Exception { + CelNativeTypesExtensions extensions = CelExtensions.nativeTypes(TestArrayPojo.class); + Cel cel = + CelFactory.plannerCelBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addCompilerLibraries(extensions) + .addRuntimeLibraries(extensions) + .addVar("pojo", StructTypeReference.create(TestArrayPojo.class.getCanonicalName())) + .build(); + CelAbstractSyntaxTree ast = cel.compile("pojo.strings").getAst(); + CelRuntime.Program program = cel.createProgram(ast); + + TestArrayPojo input = new TestArrayPojo(); + input.strings = new String[] {"a", null, "c"}; + + CelEvaluationException e = assertThrows( - IllegalArgumentException.class, () -> CelExtensions.nativeTypes(TestArrayPojo.class)); - assertThat(e).hasMessageThat().contains("Unsupported type for property 'values'"); + CelEvaluationException.class, () -> program.eval(ImmutableMap.of("pojo", input))); + assertThat(e).hasCauseThat().isInstanceOf(CelInvalidArgumentException.class); + assertThat(e).hasCauseThat().hasMessageThat().contains("Element at index 1 is null."); } @Test @@ -662,10 +744,7 @@ public void nativeTypes_createWithUint_fromUnsignedLong() throws Exception { .getAst(); CelRuntime.Program program = celRuntime.createProgram(ast); - Object result = program.eval(); - - assertThat(result).isInstanceOf(TestAllTypesPublicFieldsPojo.class); - TestAllTypesPublicFieldsPojo pojo = (TestAllTypesPublicFieldsPojo) result; + TestAllTypesPublicFieldsPojo pojo = (TestAllTypesPublicFieldsPojo) program.eval(); assertThat(pojo.uintVal).isEqualTo(UnsignedLong.fromLongBits(42L)); } @@ -782,6 +861,8 @@ public void nativeTypes_nullSafeTraversal() throws Exception { assertThat(cel.createProgram(cel.compile("pojo.int64Val").getAst()).eval(vars)).isEqualTo(0L); assertThat(cel.createProgram(cel.compile("pojo.nestedVal.value").getAst()).eval(vars)) .isEqualTo(""); + assertThat(cel.createProgram(cel.compile("size(pojo.arrayVal) == 0").getAst()).eval(vars)) + .isEqualTo(true); CelAbstractSyntaxTree abstractPojoAst = cel.compile("pojo.abstractPojo.value").getAst(); CelRuntime.Program abstractPojoProgram = cel.createProgram(abstractPojoAst); CelEvaluationException e = @@ -942,6 +1023,7 @@ public String get() { public double doubleVal; public float floatVal; public byte[] bytesVal; + public String[] arrayVal; public Duration durationVal; public Instant timestampVal; public TestNestedType nestedVal; @@ -1259,7 +1341,12 @@ public static class TestWildcardPojo { } public static class TestArrayPojo { - public String[] values; + public String[] strings; + public int[] ints; + public TestNestedType[] nesteds; + public int[][] matrix; + public TestNestedType[][] nestedMatrix; + public byte[][] byteArrays; } public static class TestOptionalUrlPojo { From c2b60394d0673eaf0a05fd07b0ffd93da3ec9c04 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 10 Jun 2026 21:15:56 -0700 Subject: [PATCH 097/204] Internal Changes PiperOrigin-RevId: 930255571 --- policy/src/test/java/dev/cel/policy/BUILD.bazel | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/policy/src/test/java/dev/cel/policy/BUILD.bazel b/policy/src/test/java/dev/cel/policy/BUILD.bazel index 6a76cf3b0..5157e0c74 100644 --- a/policy/src/test/java/dev/cel/policy/BUILD.bazel +++ b/policy/src/test/java/dev/cel/policy/BUILD.bazel @@ -8,7 +8,9 @@ package( java_library( name = "tests", testonly = True, - srcs = glob(["*.java"]), + srcs = glob( + ["*.java"], + ), data = [ "@cel_policy//conformance:testdata", ], From f31ad58815ccce1735854e7ea79c7375966b5c48 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 11 Jun 2026 12:09:09 -0700 Subject: [PATCH 098/204] Internal Changes PiperOrigin-RevId: 930666358 --- runtime/standard/BUILD.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/standard/BUILD.bazel b/runtime/standard/BUILD.bazel index 4ca87e5e0..c1c040f1d 100644 --- a/runtime/standard/BUILD.bazel +++ b/runtime/standard/BUILD.bazel @@ -3,7 +3,7 @@ load("//:cel_android_rules.bzl", "cel_android_library") package( default_applicable_licenses = ["//:license"], - default_visibility = ["//:internal"], + default_visibility = ["//visibility:public"], ) java_library( From d917045111f09db82aeddaeae9b63d4d0554f447 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 12 Jun 2026 10:41:21 -0700 Subject: [PATCH 099/204] Ensure chained optional field selection does not repeatedly wrap the optional type Fixes https://github.com/google/cel-java/issues/1083 PiperOrigin-RevId: 931223677 --- .../extensions/CelOptionalLibraryTest.java | 35 +++++++++++++++++++ .../dev/cel/runtime/DefaultInterpreter.java | 8 +++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java index 34c7f89f9..2ba12910f 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java @@ -44,6 +44,7 @@ import dev.cel.common.values.CelByteString; import dev.cel.common.values.NullValue; import dev.cel.compiler.CelCompiler; +import dev.cel.expr.conformance.proto3.NestedTestAllTypes; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import dev.cel.parser.CelMacro; @@ -603,6 +604,16 @@ public void optionalFieldSelection_onMap_returnsOptionalValue() throws Exception assertThat(result).hasValue(2L); } + @Test + public void optionalFieldSelection_onMap_chained_returnsSinglyWrappedOptional() throws Exception { + Cel cel = newCelBuilder().setResultType(OptionalType.create(SimpleType.STRING)).build(); + CelAbstractSyntaxTree ast = compile(cel, "{'foo': {'bar': 'baz'}}.?foo.?bar"); + + Optional result = (Optional) cel.createProgram(ast).eval(); + + assertThat(result).hasValue("baz"); + } + @Test public void optionalFieldSelection_onProtoMessage_returnsOptionalEmpty() throws Exception { Cel cel = @@ -619,6 +630,30 @@ public void optionalFieldSelection_onProtoMessage_returnsOptionalEmpty() throws assertThat(result).isEmpty(); } + @Test + public void optionalFieldSelection_onProtoMessage_chained_returnsSinglyWrappedOptional() + throws Exception { + Cel cel = + newCelBuilder() + .setResultType(OptionalType.create(SimpleType.INT)) + .addVar( + "msg", StructTypeReference.create(NestedTestAllTypes.getDescriptor().getFullName())) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "msg.?payload.?single_int32"); + + Optional result = + (Optional) + cel.createProgram(ast) + .eval( + ImmutableMap.of( + "msg", + NestedTestAllTypes.newBuilder() + .setPayload(TestAllTypes.newBuilder().setSingleInt32(5).build()) + .build())); + + assertThat(result).hasValue(5L); + } + @Test public void optionalFieldSelection_onProtoMessage_returnsOptionalValue() throws Exception { Cel cel = diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java index 9abc3716c..fdab71c3d 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java @@ -783,8 +783,12 @@ private Optional maybeEvalOptionalSelectField( } IntermediateResult result = evalFieldSelect(frame, expr, operand, field, false); - return Optional.of( - IntermediateResult.create(result.attribute(), Optional.of(result.value()))); + // Ensure only one level of optional is wrapped when chaining optional field selections. + Object resultValue = result.value(); + if (!(resultValue instanceof Optional)) { + resultValue = Optional.of(resultValue); + } + return Optional.of(IntermediateResult.create(result.attribute(), resultValue)); } private IntermediateResult evalBoolean(ExecutionFrame frame, CelExpr expr, boolean strict) From 6e267b49876ddbef184b450f1d9298d1b64cf421 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Sat, 13 Jun 2026 12:51:07 -0700 Subject: [PATCH 100/204] Release 0.13.1 PiperOrigin-RevId: 931719670 --- MODULE.bazel | 2 +- README.md | 4 ++-- publish/cel_version.bzl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 6689158c6..fcaf041ba 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -46,7 +46,7 @@ TRUTH_VERSION = "1.4.4" PROTOBUF_JAVA_VERSION = "4.33.5" -CEL_VERSION = "0.13.0" +CEL_VERSION = "0.13.1" # Compile only artifacts [ diff --git a/README.md b/README.md index 88a69ee85..2d8688fe0 100644 --- a/README.md +++ b/README.md @@ -62,14 +62,14 @@ CEL-Java is available in Maven Central Repository. [Download the JARs here][8] o dev.cel cel - 0.13.0 + 0.13.1 ``` **Gradle** ```gradle -implementation 'dev.cel:cel:0.13.0' +implementation 'dev.cel:cel:0.13.1' ``` Then run this example: diff --git a/publish/cel_version.bzl b/publish/cel_version.bzl index 70fa1a010..4ceb4bfa4 100644 --- a/publish/cel_version.bzl +++ b/publish/cel_version.bzl @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. """Maven artifact version for CEL.""" -CEL_VERSION = "0.13.0" +CEL_VERSION = "0.13.1" From a5817b4a267b17d962ea7054831acec575756f72 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 15 Jun 2026 16:36:09 -0700 Subject: [PATCH 101/204] Make OpaqueValue subclassable and resolve type in TypeResolver PiperOrigin-RevId: 932735464 --- .../src/main/java/dev/cel/bundle/CelImpl.java | 3 + .../dev/cel/common/values/OpaqueValue.java | 52 ++++- .../java/dev/cel/common/values/BUILD.bazel | 1 + .../cel/common/values/OpaqueValueTest.java | 183 ++++++++++++++++++ .../cel/common/values/StructValueTest.java | 16 ++ .../src/main/java/dev/cel/runtime/BUILD.bazel | 4 + .../java/dev/cel/runtime/CelRuntimeImpl.java | 2 +- .../dev/cel/runtime/CelRuntimeLegacyImpl.java | 8 +- .../cel/runtime/DescriptorTypeResolver.java | 22 ++- .../java/dev/cel/runtime/LiteRuntimeImpl.java | 2 +- .../java/dev/cel/runtime/TypeResolver.java | 19 +- .../cel/runtime/DefaultInterpreterTest.java | 6 +- .../dev/cel/runtime/TypeResolverTest.java | 4 +- .../runtime/planner/ProgramPlannerTest.java | 4 +- 14 files changed, 303 insertions(+), 23 deletions(-) diff --git a/bundle/src/main/java/dev/cel/bundle/CelImpl.java b/bundle/src/main/java/dev/cel/bundle/CelImpl.java index f6b985065..f0db128c1 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelImpl.java +++ b/bundle/src/main/java/dev/cel/bundle/CelImpl.java @@ -327,6 +327,9 @@ public Builder setTypeProvider(TypeProvider typeProvider) { @Override public CelBuilder setTypeProvider(CelTypeProvider celTypeProvider) { compilerBuilder.setTypeProvider(celTypeProvider); + if (runtimeBuilder instanceof CelRuntimeImpl.Builder) { + runtimeBuilder.setTypeProvider(celTypeProvider); + } return this; } diff --git a/common/src/main/java/dev/cel/common/values/OpaqueValue.java b/common/src/main/java/dev/cel/common/values/OpaqueValue.java index 3350d05d4..8b3ac4574 100644 --- a/common/src/main/java/dev/cel/common/values/OpaqueValue.java +++ b/common/src/main/java/dev/cel/common/values/OpaqueValue.java @@ -14,13 +14,32 @@ package dev.cel.common.values; +import static com.google.common.base.Preconditions.checkNotNull; + import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.Immutable; import dev.cel.common.types.OpaqueType; -/** OpaqueValue is the value representation of OpaqueType. */ -@AutoValue -@AutoValue.CopyAnnotations -@SuppressWarnings("Immutable") // Java Object is mutable. +/** + * OpaqueValue is the value representation of an {@link OpaqueType}. + * + *

Users may provide a custom opaque type that CEL can understand. Note that this is only + * supported for the Planner runtime. There are two primary modes of extending this class: + * + *

    + *
  • Direct Extension (Recommended): A domain object directly extends {@code OpaqueValue} + * and returns {@code this} for its {@link #value()} method. This approach allows the CEL + * engine to evaluate the object natively without stripping its type information, eliminating + * the need to register a custom {@link CelValueConverter}. + *
  • Wrapping: A domain object is wrapped into an {@code OpaqueValue} via the {@link + * #create(String, Object)} factory method. This is required when users cannot modify their + * existing POJOs to extend {@code OpaqueValue}. However, because the CEL runtime aggressively + * unwraps objects during evaluation, this mode necessitates implementing and registering a + * custom {@code CelValueConverter} that maps the unwrapped native Java object back into its + * corresponding {@code OpaqueValue}. + *
+ */ +@Immutable public abstract class OpaqueValue extends CelValue { @Override @@ -31,7 +50,30 @@ public boolean isZeroValue() { @Override public abstract OpaqueType celType(); + /** + * Creates an {@code OpaqueValue} by wrapping a domain object. + * + *

This method should only be used for the "Wrapping" extension mode (see class Javadoc) when + * users cannot modify their POJOs to directly extend {@code OpaqueValue}. Using this method + * necessitates implementing and registering a custom {@link CelValueConverter}. + * + * @param name The name of the opaque type. + * @param value The raw Java object to wrap. + */ public static OpaqueValue create(String name, Object value) { - return new AutoValue_OpaqueValue(value, OpaqueType.create(name)); + return new AutoValue_OpaqueValue_OpaqueValueWrapper( + checkNotNull(value), OpaqueType.create(name)); + } + + @AutoValue + @AutoValue.CopyAnnotations + @Immutable + @SuppressWarnings("Immutable") + abstract static class OpaqueValueWrapper extends OpaqueValue { + @Override + public abstract Object value(); + + @Override + public abstract OpaqueType celType(); } } diff --git a/common/src/test/java/dev/cel/common/values/BUILD.bazel b/common/src/test/java/dev/cel/common/values/BUILD.bazel index cd7c24a63..76c761567 100644 --- a/common/src/test/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/values/BUILD.bazel @@ -35,6 +35,7 @@ java_library( "//testing/protos:test_all_types_cel_java_proto3", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_guava_guava_testlib", "@maven//:com_google_protobuf_protobuf_java", diff --git a/common/src/test/java/dev/cel/common/values/OpaqueValueTest.java b/common/src/test/java/dev/cel/common/values/OpaqueValueTest.java index d97bcd28a..326572842 100644 --- a/common/src/test/java/dev/cel/common/values/OpaqueValueTest.java +++ b/common/src/test/java/dev/cel/common/values/OpaqueValueTest.java @@ -17,7 +17,17 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.Immutable; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelFactory; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypeProvider; import dev.cel.common.types.OpaqueType; +import java.util.Map; +import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -37,4 +47,177 @@ public void opaqueValue_construct() { public void create_nullValue_throws() { assertThrows(NullPointerException.class, () -> OpaqueValue.create("opaque_type_name", null)); } + + private static final OpaqueType CUSTOM_OPAQUE_TYPE = OpaqueType.create("custom_opaque_type"); + + private static final CelTypeProvider CUSTOM_OPAQUE_TYPE_PROVIDER = + new CelTypeProvider() { + @Override + public ImmutableList types() { + return ImmutableList.of(CUSTOM_OPAQUE_TYPE); + } + + @Override + public Optional findType(String typeName) { + return typeName.equals(CUSTOM_OPAQUE_TYPE.name()) + ? Optional.of(CUSTOM_OPAQUE_TYPE) + : Optional.empty(); + } + }; + + private static final CelValueProvider CUSTOM_OPAQUE_VALUE_PROVIDER = + new CelValueProvider() { + @Override + public Optional newValue(String structType, Map fields) { + return Optional.empty(); + } + + @Override + public CelValueConverter celValueConverter() { + return new CelValueConverter() { + @Override + public Object toRuntimeValue(Object value) { + if (value instanceof CustomOpaqueObject) { + CustomOpaqueObject customOpaqueObject = (CustomOpaqueObject) value; + return new CelCustomOpaqueValue(customOpaqueObject); + } + return super.toRuntimeValue(value); + } + }; + } + }; + + private static final CelValueProvider WRAPPED_CUSTOM_OPAQUE_VALUE_PROVIDER = + new CelValueProvider() { + @Override + public Optional newValue(String structType, Map fields) { + return Optional.empty(); + } + + @Override + public CelValueConverter celValueConverter() { + return new CelValueConverter() { + @Override + public Object toRuntimeValue(Object value) { + if (value instanceof CustomOpaqueObject) { + CustomOpaqueObject customOpaqueObject = (CustomOpaqueObject) value; + return OpaqueValue.create(CUSTOM_OPAQUE_TYPE.name(), customOpaqueObject); + } + return super.toRuntimeValue(value); + } + }; + } + }; + + @Immutable + private static class CustomOpaqueObject { + private final String value; + + CustomOpaqueObject(String value) { + this.value = value; + } + + String getValue() { + return value; + } + } + + @Immutable + private static class CelCustomOpaqueValue extends OpaqueValue { + private final CustomOpaqueObject obj; + + CelCustomOpaqueValue(CustomOpaqueObject obj) { + this.obj = obj; + } + + @Override + public CustomOpaqueObject value() { + return obj; + } + + @Override + public OpaqueType celType() { + return CUSTOM_OPAQUE_TYPE; + } + } + + @Test + public void evaluate_customOpaqueValue_asVariable() throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .addVar("opaque_var", CUSTOM_OPAQUE_TYPE) + .setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER) + .setValueProvider(CUSTOM_OPAQUE_VALUE_PROVIDER) + .build(); + CelAbstractSyntaxTree ast = cel.compile("opaque_var").getAst(); + + CustomOpaqueObject rawValue = new CustomOpaqueObject("hello"); + Object result = cel.createProgram(ast).eval(ImmutableMap.of("opaque_var", rawValue)); + + assertThat(result).isInstanceOf(CustomOpaqueObject.class); + assertThat(((CustomOpaqueObject) result).getValue()).isEqualTo("hello"); + } + + @Test + public void evaluate_typeOfCustomOpaqueValue() throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .addVar("opaque_var", CUSTOM_OPAQUE_TYPE) + .setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER) + .setValueProvider(CUSTOM_OPAQUE_VALUE_PROVIDER) + .build(); + CelAbstractSyntaxTree ast = cel.compile("type(opaque_var) == custom_opaque_type").getAst(); + + CustomOpaqueObject rawValue = new CustomOpaqueObject("hello"); + Object result = cel.createProgram(ast).eval(ImmutableMap.of("opaque_var", rawValue)); + + assertThat(result).isEqualTo(true); + } + + @Test + public void evaluate_typeOfCustomOpaqueValue_wrapped() throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .addVar("opaque_var", CUSTOM_OPAQUE_TYPE) + .setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER) + .setValueProvider(WRAPPED_CUSTOM_OPAQUE_VALUE_PROVIDER) + .build(); + CelAbstractSyntaxTree ast = cel.compile("type(opaque_var) == custom_opaque_type").getAst(); + + CustomOpaqueObject rawValue = new CustomOpaqueObject("hello"); + Object result = cel.createProgram(ast).eval(ImmutableMap.of("opaque_var", rawValue)); + + assertThat(result).isEqualTo(true); + } + + @Immutable + private static class SelfReturningOpaqueObject extends OpaqueValue { + SelfReturningOpaqueObject() {} + + @Override + public Object value() { + return this; + } + + @Override + public OpaqueType celType() { + return CUSTOM_OPAQUE_TYPE; + } + } + + @Test + public void evaluate_selfReturningOpaqueValue_noConverter() throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .addVar("opaque_var", CUSTOM_OPAQUE_TYPE) + .setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER) + .build(); + CelAbstractSyntaxTree ast = cel.compile("type(opaque_var) == custom_opaque_type").getAst(); + + SelfReturningOpaqueObject rawValue = new SelfReturningOpaqueObject(); + Object result = cel.createProgram(ast).eval(ImmutableMap.of("opaque_var", rawValue)); + + assertThat(result).isEqualTo(true); + } } + diff --git a/common/src/test/java/dev/cel/common/values/StructValueTest.java b/common/src/test/java/dev/cel/common/values/StructValueTest.java index f25db8e87..978222869 100644 --- a/common/src/test/java/dev/cel/common/values/StructValueTest.java +++ b/common/src/test/java/dev/cel/common/values/StructValueTest.java @@ -128,6 +128,22 @@ public void celTypeTest() { assertThat(value.celType()).isEqualTo(CUSTOM_STRUCT_TYPE); } + @Test + public void evaluate_typeOfCustomStruct() throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) + .addVar("a", CUSTOM_STRUCT_TYPE) + .setTypeProvider(CUSTOM_STRUCT_TYPE_PROVIDER) + .setValueProvider(CUSTOM_STRUCT_VALUE_PROVIDER) + .build(); + CelAbstractSyntaxTree ast = cel.compile("type(a) == custom_struct").getAst(); + + Object result = cel.createProgram(ast).eval(ImmutableMap.of("a", new CelCustomStructValue(20))); + + assertThat(result).isEqualTo(true); + } + @Test public void evaluate_usingCustomClass_createNewStruct() throws Exception { Cel cel = diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 5178ae27c..17160e346 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -213,6 +213,7 @@ java_library( "//common/types:type_providers", "//common/values", "//common/values:cel_byte_string", + "//common/values:cel_value", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", @@ -229,6 +230,7 @@ cel_android_library( "//common/types:type_providers_android", "//common/types:types_android", "//common/values:cel_byte_string", + "//common/values:cel_value_android", "//common/values:values_android", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -247,6 +249,7 @@ java_library( "//common/annotations", "//common/types", "//common/types:type_providers", + "//common/values", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", @@ -896,6 +899,7 @@ java_library( "//common/internal:proto_message_factory", "//common/types:cel_types", "//common/types:type_providers", + "//common/values", "//common/values:cel_value_provider", "//common/values:proto_message_value_provider", "//runtime/standard:int", diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index adfba967b..b02f64b61 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -521,7 +521,7 @@ public CelRuntime build() { } DescriptorTypeResolver descriptorTypeResolver = - DescriptorTypeResolver.create(combinedTypeProvider); + DescriptorTypeResolver.create(combinedTypeProvider, celValueConverter); TypeFunction typeFunction = TypeFunction.create(descriptorTypeResolver); mutableFunctionBindings.putAll(functionBindings()); diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java index 33702b2c6..b9ce022cf 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java @@ -42,6 +42,7 @@ import dev.cel.common.internal.ProtoMessageFactory; import dev.cel.common.types.CelTypeProvider; import dev.cel.common.types.CelTypes; +import dev.cel.common.values.CelValueConverter; import dev.cel.common.values.CelValueProvider; import dev.cel.common.values.ProtoMessageValueProvider; import dev.cel.runtime.standard.IntFunction.IntOverload; @@ -330,6 +331,7 @@ public CelRuntimeLegacyImpl build() { customBinding.getDefinition()); } + CelValueConverter celValueConverter = CelValueConverter.getDefaultInstance(); RuntimeTypeProvider runtimeTypeProvider; if (options.enableCelValue()) { @@ -340,13 +342,17 @@ public CelRuntimeLegacyImpl build() { } runtimeTypeProvider = CelValueRuntimeTypeProvider.newInstance(messageValueProvider); + celValueConverter = messageValueProvider.celValueConverter(); } else { runtimeTypeProvider = new DescriptorMessageProvider(runtimeTypeFactory, options); + if (celValueProvider != null) { + celValueConverter = celValueProvider.celValueConverter(); + } } DefaultInterpreter interpreter = new DefaultInterpreter( - DescriptorTypeResolver.create(), + DescriptorTypeResolver.create(celValueConverter), runtimeTypeProvider, dispatcherBuilder.build(), options); diff --git a/runtime/src/main/java/dev/cel/runtime/DescriptorTypeResolver.java b/runtime/src/main/java/dev/cel/runtime/DescriptorTypeResolver.java index 63fcb87b6..3d5208e2e 100644 --- a/runtime/src/main/java/dev/cel/runtime/DescriptorTypeResolver.java +++ b/runtime/src/main/java/dev/cel/runtime/DescriptorTypeResolver.java @@ -23,6 +23,7 @@ import dev.cel.common.types.CelTypeProvider; import dev.cel.common.types.StructTypeReference; import dev.cel.common.types.TypeType; +import dev.cel.common.values.CelValueConverter; import java.util.NoSuchElementException; import java.util.Optional; import org.jspecify.annotations.Nullable; @@ -42,9 +43,13 @@ public final class DescriptorTypeResolver extends TypeResolver { /** * Creates a {@code DescriptorTypeResolver}. All protobuf messages are resolved as a type of * {@link StructTypeReference}. + * + * @deprecated This only exists to maintain support for the legacy runtime. Use {@link + * #create(CelTypeProvider, CelValueConverter)} instead. */ - public static DescriptorTypeResolver create() { - return new DescriptorTypeResolver(); + @Deprecated + static DescriptorTypeResolver create(CelValueConverter celValueConverter) { + return new DescriptorTypeResolver(null, celValueConverter); } /** @@ -52,8 +57,9 @@ public static DescriptorTypeResolver create() { * in the provided {@link CelTypeProvider}, the message is resolved as a concrete {@code * ProtoMessageType} instead of a {@link StructTypeReference}. */ - public static DescriptorTypeResolver create(CelTypeProvider typeProvider) { - return new DescriptorTypeResolver(typeProvider); + public static DescriptorTypeResolver create( + CelTypeProvider typeProvider, CelValueConverter celValueConverter) { + return new DescriptorTypeResolver(typeProvider, celValueConverter); } @Override @@ -81,11 +87,9 @@ public TypeType resolveObjectType(Object obj, CelType typeCheckedType) { return super.resolveObjectType(obj, typeCheckedType); } - private DescriptorTypeResolver() { - this(null); - } - - private DescriptorTypeResolver(@Nullable CelTypeProvider typeProvider) { + private DescriptorTypeResolver( + @Nullable CelTypeProvider typeProvider, CelValueConverter celValueConverter) { + super(celValueConverter); this.typeProvider = typeProvider; } } diff --git a/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java index d58eb3be4..8ce2d7733 100644 --- a/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java @@ -177,7 +177,7 @@ public CelLiteRuntime build() { Interpreter interpreter = new DefaultInterpreter( - TypeResolver.create(), + TypeResolver.create(celValueProvider.celValueConverter()), CelValueRuntimeTypeProvider.newInstance(celValueProvider), dispatcherBuilder.build(), celOptions); diff --git a/runtime/src/main/java/dev/cel/runtime/TypeResolver.java b/runtime/src/main/java/dev/cel/runtime/TypeResolver.java index c2ebf521c..691810837 100644 --- a/runtime/src/main/java/dev/cel/runtime/TypeResolver.java +++ b/runtime/src/main/java/dev/cel/runtime/TypeResolver.java @@ -36,6 +36,8 @@ import dev.cel.common.types.StructTypeReference; import dev.cel.common.types.TypeType; import dev.cel.common.values.CelByteString; +import dev.cel.common.values.CelValue; +import dev.cel.common.values.CelValueConverter; import java.time.Instant; import java.util.ArrayList; import java.util.Collection; @@ -53,8 +55,10 @@ @Internal public class TypeResolver { - static TypeResolver create() { - return new TypeResolver(); + private final CelValueConverter celValueConverter; + + static TypeResolver create(CelValueConverter celValueConverter) { + return new TypeResolver(celValueConverter); } // Sentinel runtime value representing the special "type" ident. This ensures following to be @@ -147,6 +151,13 @@ public TypeType resolveObjectType(Object obj, CelType typeCheckedType) { return wellKnownTypeType.get(); } + if (celValueConverter != null) { + Object celVal = celValueConverter.toRuntimeValue(obj); + if (celVal instanceof CelValue) { + return TypeType.create(((CelValue) celVal).celType()); + } + } + if (obj instanceof MessageLiteOrBuilder) { // TODO: Replace with CelLiteDescriptor throw new UnsupportedOperationException("Not implemented yet"); @@ -193,5 +204,7 @@ private static CelType adaptStructType(StructType typeOfType) { return newTypeOfType; } - protected TypeResolver() {} + protected TypeResolver(CelValueConverter celValueConverter) { + this.celValueConverter = checkNotNull(celValueConverter); + } } diff --git a/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java index bd0e96856..a23e3b2fb 100644 --- a/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java +++ b/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java @@ -24,6 +24,7 @@ import dev.cel.common.CelOptions; import dev.cel.common.CelOverloadDecl; import dev.cel.common.types.SimpleType; +import dev.cel.common.values.CelValueConverter; import dev.cel.compiler.CelCompiler; import dev.cel.compiler.CelCompilerFactory; import dev.cel.parser.CelStandardMacro; @@ -98,7 +99,10 @@ public Object adapt(String messageName, Object message) { notStrictlyFalseBinding.getDefinition()); DefaultInterpreter defaultInterpreter = new DefaultInterpreter( - new TypeResolver(), emptyProvider, dispatcherBuilder.build(), CelOptions.DEFAULT); + TypeResolver.create(CelValueConverter.getDefaultInstance()), + emptyProvider, + dispatcherBuilder.build(), + CelOptions.DEFAULT); DefaultInterpretable interpretable = (DefaultInterpretable) defaultInterpreter.createInterpretable(ast); diff --git a/runtime/src/test/java/dev/cel/runtime/TypeResolverTest.java b/runtime/src/test/java/dev/cel/runtime/TypeResolverTest.java index c5a11b680..0437a31e5 100644 --- a/runtime/src/test/java/dev/cel/runtime/TypeResolverTest.java +++ b/runtime/src/test/java/dev/cel/runtime/TypeResolverTest.java @@ -28,6 +28,7 @@ import dev.cel.common.types.OptionalType; import dev.cel.common.types.SimpleType; import dev.cel.common.types.TypeType; +import dev.cel.common.values.CelValueConverter; import java.util.ArrayList; import java.util.HashMap; import java.util.Optional; @@ -36,7 +37,8 @@ @RunWith(TestParameterInjector.class) public class TypeResolverTest { - private static final TypeResolver TYPE_RESOLVER = TypeResolver.create(); + private static final TypeResolver TYPE_RESOLVER = + TypeResolver.create(CelValueConverter.getDefaultInstance()); @Test public void resolveWellKnownObjectType_sentinelRuntimeType() { diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index c58ae782b..c749028ff 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -180,7 +180,9 @@ private static DefaultDispatcher newDispatcher() { addBindingsToDispatcher( builder, stdFunctions.newFunctionBindings(RUNTIME_EQUALITY, CEL_OPTIONS)); - TypeFunction typeFunction = TypeFunction.create(DescriptorTypeResolver.create(TYPE_PROVIDER)); + TypeFunction typeFunction = + TypeFunction.create( + DescriptorTypeResolver.create(TYPE_PROVIDER, CelValueConverter.getDefaultInstance())); addBindingsToDispatcher( builder, typeFunction.newFunctionBindings(CEL_OPTIONS, RUNTIME_EQUALITY)); From 8e46771e1a1b100a067de4eba5d4a8a36e27f019 Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Tue, 16 Jun 2026 13:28:02 -0700 Subject: [PATCH 102/204] Update from google/cel-java to cel-expr/cel-java PiperOrigin-RevId: 933268215 --- README.md | 10 +++++----- codelab/README.md | 10 +++++----- publish/pom_template.xml | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 2d8688fe0..21c589bef 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ > github.com/cel-expr/cel-java!** > > Please update your links and dependencies. See the [pinned -> issue](https://github.com/google/cel-java/issues/1066) for details. +> issue](https://github.com/cel-exp/cel-java/issues/1066) for details. The Common Expression Language (CEL) is a non-Turing complete language designed for simplicity, speed, safety, and portability. CEL's C-like [syntax][1] looks @@ -389,8 +389,8 @@ Released under the [Apache License](LICENSE). [4]: https://github.com/google/re2j [5]: https://github.com/protocolbuffers/protobuf/tree/master/java [6]: https://github.com/antlr/antlr4/tree/master/runtime/Java -[7]: https://github.com/google/cel-java/issues +[7]: https://github.com/cel-exp/cel-java/issues [8]: https://search.maven.org/search?q=g:dev.cel -[9]: https://github.com/google/cel-java/blob/main/compiler/src/main/java/dev/cel/compiler/CelCompilerBuilder.java -[10]: https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros -[11]: https://github.com/google/cel-java/blob/main/extensions/src/main/java/dev/cel/extensions/README.md +[9]: https://github.com/cel-expr/cel-java/blob/main/compiler/src/main/java/dev/cel/compiler/CelCompilerBuilder.java +[10]: https://github.com/cel-expr/cel-spec/blob/master/doc/langdef.md#macros +[11]: https://github.com/cel-expr/cel-java/blob/main/extensions/src/main/java/dev/cel/extensions/README.md diff --git a/codelab/README.md b/codelab/README.md index f7d248b13..83257d186 100644 --- a/codelab/README.md +++ b/codelab/README.md @@ -50,7 +50,7 @@ The code for this codelab lives in the `codelab` folder of the cel-java repo. Th Clone and cd into the repo: ``` -git clone git@github.com:google/cel-java.git +git clone git@github.com:cel-expr/cel-java.git cd cel-java ``` @@ -74,10 +74,10 @@ Tests run: 5, Failures: 5 Each exercise is laid out as `ExerciseN.java` and is accompanied by failing tests. Throughout this codelab, we will modify the main exercise code to make these tests pass. -- Codelab code: https://github.com/google/cel-java/tree/main/codelab/src/main/codelab -- Test code for the main codelab: https://github.com/google/cel-java/tree/main/codelab/src/test/codelab -- Codelab solution code: https://github.com/google/cel-java/tree/main/codelab/src/main/codelab/solutions -- Test code for the solution: https://github.com/google/cel-java/tree/main/codelab/src/test/codelab/solutions +- Codelab code: https://github.com/cel-expr/cel-java/tree/main/codelab/src/main/codelab +- Test code for the main codelab: https://github.com/cel-expr/cel-java/tree/main/codelab/src/test/codelab +- Codelab solution code: https://github.com/cel-expr/cel-java/tree/main/codelab/src/main/codelab/solutions +- Test code for the solution: https://github.com/cel-expr/cel-java/tree/main/codelab/src/test/codelab/solutions We will also be using `google.rpc.context.AttributeContext` in [attribute_context.proto](https://github.com/googleapis/googleapis/blob/master/google/rpc/context/attribute_context.proto) to help with defining inputs for exercises. diff --git a/publish/pom_template.xml b/publish/pom_template.xml index 01e86b98c..ec5c08be6 100644 --- a/publish/pom_template.xml +++ b/publish/pom_template.xml @@ -53,12 +53,12 @@ - scm:git:git://github.com/google/cel-java.git + scm:git:git://github.com/cel-expr/cel-java.git - https://github.com/google/cel-java/tree/main + https://github.com/cel-expr/cel-java/tree/main - https://github.com/google/cel-java + https://github.com/cel-expr/cel-java CEL_VERSION From 54acafef8b44487729de2068aaa86f8b6d91fcae Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 16 Jun 2026 14:12:27 -0700 Subject: [PATCH 103/204] Remove README warning regarding repo move PiperOrigin-RevId: 933292598 --- README.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/README.md b/README.md index 21c589bef..e279e9aa5 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,5 @@ # Common Expression Language for Java -> [!WARNING] -> **On June 16, 2026, this repository will move to -> github.com/cel-expr/cel-java!** -> -> Please update your links and dependencies. See the [pinned -> issue](https://github.com/cel-exp/cel-java/issues/1066) for details. - The Common Expression Language (CEL) is a non-Turing complete language designed for simplicity, speed, safety, and portability. CEL's C-like [syntax][1] looks nearly identical to equivalent expressions in C++, Go, Java, and TypeScript. @@ -389,7 +382,7 @@ Released under the [Apache License](LICENSE). [4]: https://github.com/google/re2j [5]: https://github.com/protocolbuffers/protobuf/tree/master/java [6]: https://github.com/antlr/antlr4/tree/master/runtime/Java -[7]: https://github.com/cel-exp/cel-java/issues +[7]: https://github.com/cel-expr/cel-java/issues [8]: https://search.maven.org/search?q=g:dev.cel [9]: https://github.com/cel-expr/cel-java/blob/main/compiler/src/main/java/dev/cel/compiler/CelCompilerBuilder.java [10]: https://github.com/cel-expr/cel-spec/blob/master/doc/langdef.md#macros From 089a8e215400576fe37d8454b135be3edea6b1b0 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 16 Jun 2026 16:10:21 -0700 Subject: [PATCH 104/204] Add safe guards to ensure we don't build protoc from source PiperOrigin-RevId: 933353791 --- .bazelrc | 8 +++++++- java_lite_proto_cel_library_impl.bzl | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.bazelrc b/.bazelrc index 968597053..8724ec1da 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,7 +1,13 @@ common --enable_bzlmod # Use built-in protoc -common --incompatible_enable_proto_toolchain_resolution --@com_google_protobuf//bazel/toolchains:prefer_prebuilt_protoc +common --incompatible_enable_proto_toolchain_resolution +common --@com_google_protobuf//bazel/toolchains:prefer_prebuilt_protoc=true +# Ensure that we don't accidentally build protobuf or gRPC +common --per_file_copt=external/.*protobuf.*@--PROTOBUF_WAS_NOT_SUPPOSED_TO_BE_BUILT +common --host_per_file_copt=external/.*protobuf.*@--PROTOBUF_WAS_NOT_SUPPOSED_TO_BE_BUILT +common --per_file_copt=external/.*grpc.*@--GRPC_WAS_NOT_SUPPOSED_TO_BE_BUILT +common --host_per_file_copt=external/.*grpc.*@--GRPC_WAS_NOT_SUPPOSED_TO_BE_BUILT build --java_runtime_version=remotejdk_11 build --java_language_version=11 diff --git a/java_lite_proto_cel_library_impl.bzl b/java_lite_proto_cel_library_impl.bzl index bd31f053d..1c5254a8c 100644 --- a/java_lite_proto_cel_library_impl.bzl +++ b/java_lite_proto_cel_library_impl.bzl @@ -20,7 +20,7 @@ This is an implementation detail. Clients should use 'java_lite_proto_cel_librar load("@rules_java//java:defs.bzl", "java_library") load("//publish:cel_version.bzl", "CEL_VERSION") load("@com_google_protobuf//bazel:java_lite_proto_library.bzl", "java_lite_proto_library") -load("@rules_proto//proto:defs.bzl", "ProtoInfo") +load("@com_google_protobuf//bazel/common:proto_info.bzl", "ProtoInfo") def java_lite_proto_cel_library_impl( name, From 55244c781d373b3848fd1b211a68a8bd13d96a47 Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Tue, 16 Jun 2026 16:36:08 -0700 Subject: [PATCH 105/204] Update from google/cel-spec to cel-expr/cel-spec (Part 2) PiperOrigin-RevId: 933365198 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e279e9aa5..e2424a85c 100644 --- a/README.md +++ b/README.md @@ -376,7 +376,7 @@ Java 8 or newer is required. Released under the [Apache License](LICENSE). -[1]: https://github.com/google/cel-spec +[1]: https://github.com/cel-expr/cel-spec [2]: https://groups.google.com/forum/#!forum/cel-java-discuss [3]: https://github.com/google/guava [4]: https://github.com/google/re2j From 62b1c1c5399fef27a24bb763650576cf5d0150b3 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 17 Jun 2026 13:29:17 -0700 Subject: [PATCH 106/204] Parallelize github actions workflow for Java8 builds PiperOrigin-RevId: 933893214 --- .github/workflows/workflow.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index b172788c3..94effd33b 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -15,13 +15,13 @@ concurrency: cancel-in-progress: true jobs: - Static-Checks: + Bazel-Build-Java8: runs-on: ubuntu-latest timeout-minutes: 30 steps: - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." - run: echo "🐧 Job is running on a ${{ runner.os }} server!" - - run: echo "🔎 The name of your branch is ${GITHUB_REF} and your repository is ${{ github.repository }}." + - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." - name: Check out repository code uses: actions/checkout@v6 - name: Setup Bazel @@ -33,13 +33,18 @@ jobs: disk-cache: ${{ github.workflow }} # Share repository cache between workflows. repository-cache: true - # Never write to the cache, strictly read-only - cache-save: false + # Prevent PRs from polluting cache + cache-save: ${{ github.event_name != 'pull_request' }} + - name: Bazel Output Version + run: bazelisk --version + - name: Java 8 Build + run: bazel build ... --java_language_version=8 --java_runtime_version=8 --build_tag_filters=-conformance_maven - name: Unwanted Dependencies run: .github/workflows/unwanted_deps.sh - name: Cross-artifact Duplicate Classes Check run: .github/workflows/cross_artifact_dependencies_check.sh - run: echo "🍏 This job's status is ${{ job.status }}." + Bazel-Tests: runs-on: ubuntu-latest timeout-minutes: 30 @@ -62,8 +67,6 @@ jobs: cache-save: ${{ github.event_name != 'pull_request' }} - name: Bazel Output Version run: bazelisk --version - - name: Java 8 Build - run: bazel build ... --java_language_version=8 --java_runtime_version=8 --build_tag_filters=-conformance_maven - name: Bazel Test # Exclude codelab exercises as they are intentionally made to fail # Exclude maven conformance tests. They are only executed when there's version change. From bb0b9581b5f8b11bdf2b885d16f5b202351d3a66 Mon Sep 17 00:00:00 2001 From: Justin King Date: Wed, 17 Jun 2026 15:48:14 -0700 Subject: [PATCH 107/204] Refine iteration variable validation in CEL macros. This change introduces stricter validation for iteration variables in standard CEL macros like `all`, `exists`, `map`, and `filter`. Iteration variables must now be simple identifiers, disallowing names that start with a dot (e.g., `.x`) and preventing the use of the internal accumulator variable `__result__`. The validation logic is now shared between the standard macros and the comprehensions extension. PiperOrigin-RevId: 933966053 --- .../CelComprehensionsExtensions.java | 9 ++- .../java/dev/cel/parser/CelStandardMacro.java | 51 +++++++++++---- .../parser/CelParserParameterizedTest.java | 10 +++ .../src/test/resources/parser_errors.baseline | 64 ++++++++++++++++++- 4 files changed, 117 insertions(+), 17 deletions(-) diff --git a/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java index 3bf47c4a6..14968099c 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java @@ -479,9 +479,8 @@ private static Optional transformMapEntryMacro( private static CelExpr validatedIterationVariable( CelMacroExprFactory exprFactory, CelExpr argument) { - CelExpr arg = checkNotNull(argument); - if (arg.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { + if (!isSimpleIdentifier(arg)) { return reportArgumentError(exprFactory, arg); } else if (arg.exprKind().ident().name().equals("__result__")) { return reportAccumulatorOverwriteError(exprFactory, arg); @@ -490,6 +489,12 @@ private static CelExpr validatedIterationVariable( } } + private static boolean isSimpleIdentifier(CelExpr expr) { + return expr.getKind() == CelExpr.ExprKind.Kind.IDENT + && !expr.ident().name().isEmpty() + && !expr.ident().name().startsWith("."); + } + private static CelExpr reportArgumentError(CelMacroExprFactory exprFactory, CelExpr argument) { return exprFactory.reportError( CelIssue.formatError( diff --git a/parser/src/main/java/dev/cel/parser/CelStandardMacro.java b/parser/src/main/java/dev/cel/parser/CelStandardMacro.java index 20d30bc17..275159569 100644 --- a/parser/src/main/java/dev/cel/parser/CelStandardMacro.java +++ b/parser/src/main/java/dev/cel/parser/CelStandardMacro.java @@ -122,9 +122,9 @@ private static Optional expandAllMacro( checkNotNull(exprFactory); checkNotNull(target); checkArgument(arguments.size() == 2); - CelExpr arg0 = checkNotNull(arguments.get(0)); + CelExpr arg0 = validatedIterationVariable(exprFactory, arguments.get(0)); if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { - return Optional.of(reportArgumentError(exprFactory, arg0)); + return Optional.of(arg0); } CelExpr arg1 = checkNotNull(arguments.get(1)); CelExpr accuInit = exprFactory.newBoolLiteral(true); @@ -155,9 +155,9 @@ private static Optional expandExistsMacro( checkNotNull(exprFactory); checkNotNull(target); checkArgument(arguments.size() == 2); - CelExpr arg0 = checkNotNull(arguments.get(0)); + CelExpr arg0 = validatedIterationVariable(exprFactory, arguments.get(0)); if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { - return Optional.of(reportArgumentError(exprFactory, arg0)); + return Optional.of(arg0); } CelExpr arg1 = checkNotNull(arguments.get(1)); CelExpr accuInit = exprFactory.newBoolLiteral(false); @@ -190,9 +190,9 @@ private static Optional expandExistsOneMacro( checkNotNull(exprFactory); checkNotNull(target); checkArgument(arguments.size() == 2); - CelExpr arg0 = checkNotNull(arguments.get(0)); + CelExpr arg0 = validatedIterationVariable(exprFactory, arguments.get(0)); if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { - return Optional.of(reportArgumentError(exprFactory, arg0)); + return Optional.of(arg0); } CelExpr arg1 = checkNotNull(arguments.get(1)); CelExpr accuInit = exprFactory.newIntLiteral(0); @@ -228,12 +228,9 @@ private static Optional expandMapMacro( checkNotNull(exprFactory); checkNotNull(target); checkArgument(arguments.size() == 2 || arguments.size() == 3); - CelExpr arg0 = checkNotNull(arguments.get(0)); + CelExpr arg0 = validatedIterationVariable(exprFactory, arguments.get(0)); if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { - return Optional.of( - exprFactory.reportError( - CelIssue.formatError( - exprFactory.getSourceLocation(arg0), "argument is not an identifier"))); + return Optional.of(arg0); } CelExpr arg1; CelExpr arg2; @@ -276,9 +273,9 @@ private static Optional expandFilterMacro( checkNotNull(exprFactory); checkNotNull(target); checkArgument(arguments.size() == 2); - CelExpr arg0 = checkNotNull(arguments.get(0)); + CelExpr arg0 = validatedIterationVariable(exprFactory, arguments.get(0)); if (arg0.exprKind().getKind() != CelExpr.ExprKind.Kind.IDENT) { - return Optional.of(reportArgumentError(exprFactory, arg0)); + return Optional.of(arg0); } CelExpr arg1 = checkNotNull(arguments.get(1)); CelExpr accuInit = exprFactory.newList(); @@ -305,9 +302,37 @@ private static Optional expandFilterMacro( exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()))); } + private static CelExpr validatedIterationVariable( + CelMacroExprFactory exprFactory, CelExpr argument) { + CelExpr arg = checkNotNull(argument); + if (!isSimpleIdentifier(arg)) { + return reportArgumentError(exprFactory, arg); + } else if (arg.exprKind().ident().name().equals("__result__")) { + return reportAccumulatorOverwriteError(exprFactory, arg); + } else { + return arg; + } + } + + private static boolean isSimpleIdentifier(CelExpr expr) { + return expr.getKind() == CelExpr.ExprKind.Kind.IDENT + && !expr.ident().name().isEmpty() + && !expr.ident().name().startsWith("."); + } + private static CelExpr reportArgumentError(CelMacroExprFactory exprFactory, CelExpr argument) { return exprFactory.reportError( CelIssue.formatError( exprFactory.getSourceLocation(argument), "The argument must be a simple name")); } + + private static CelExpr reportAccumulatorOverwriteError( + CelMacroExprFactory exprFactory, CelExpr argument) { + return exprFactory.reportError( + CelIssue.formatError( + exprFactory.getSourceLocation(argument), + String.format( + "The iteration variable %s overwrites accumulator variable", + argument.ident().name()))); + } } diff --git a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java index b7474041d..019cea520 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java @@ -245,6 +245,16 @@ public void parser_errors() { runTest(PARSER, "1 + $"); runTest(PARSER, "1.all(2, 3)"); runTest(PARSER, "1.exists(2, 3)"); + runTest(PARSER, "[].all(__result__, x)"); + runTest(PARSER, "[].exists(__result__, x)"); + runTest(PARSER, "[].exists_one(__result__, x)"); + runTest(PARSER, "[].map(__result__, x, x)"); + runTest(PARSER, "[].filter(__result__, x)"); + runTest(PARSER, "[].all(.x, x)"); + runTest(PARSER, "[].exists(.x, x)"); + runTest(PARSER, "[].exists_one(.x, x)"); + runTest(PARSER, "[].map(.x, x, x)"); + runTest(PARSER, "[].filter(.x, x)"); runTest(PARSER, "1 + +"); runTest(PARSER, "\"\\xFh\""); runTest(PARSER, "\"\\a\\b\\f\\n\\r\\t\\v\\'\\\"\\\\\\? Illegal escape \\>\""); diff --git a/parser/src/test/resources/parser_errors.baseline b/parser/src/test/resources/parser_errors.baseline index 998bbd487..bb4ab3ed3 100644 --- a/parser/src/test/resources/parser_errors.baseline +++ b/parser/src/test/resources/parser_errors.baseline @@ -52,6 +52,66 @@ E: ERROR: :1:10: The argument must be a simple name | 1.exists(2, 3) | .........^ +I: [].all(__result__, x) +=====> +E: ERROR: :1:8: The iteration variable __result__ overwrites accumulator variable + | [].all(__result__, x) + | .......^ + +I: [].exists(__result__, x) +=====> +E: ERROR: :1:11: The iteration variable __result__ overwrites accumulator variable + | [].exists(__result__, x) + | ..........^ + +I: [].exists_one(__result__, x) +=====> +E: ERROR: :1:15: The iteration variable __result__ overwrites accumulator variable + | [].exists_one(__result__, x) + | ..............^ + +I: [].map(__result__, x, x) +=====> +E: ERROR: :1:8: The iteration variable __result__ overwrites accumulator variable + | [].map(__result__, x, x) + | .......^ + +I: [].filter(__result__, x) +=====> +E: ERROR: :1:11: The iteration variable __result__ overwrites accumulator variable + | [].filter(__result__, x) + | ..........^ + +I: [].all(.x, x) +=====> +E: ERROR: :1:9: The argument must be a simple name + | [].all(.x, x) + | ........^ + +I: [].exists(.x, x) +=====> +E: ERROR: :1:12: The argument must be a simple name + | [].exists(.x, x) + | ...........^ + +I: [].exists_one(.x, x) +=====> +E: ERROR: :1:16: The argument must be a simple name + | [].exists_one(.x, x) + | ...............^ + +I: [].map(.x, x, x) +=====> +E: ERROR: :1:9: The argument must be a simple name + | [].map(.x, x, x) + | ........^ + +I: [].filter(.x, x) +=====> +E: ERROR: :1:12: The argument must be a simple name + | [].filter(.x, x) + | ...........^ + I: 1 + + =====> E: ERROR: :1:5: mismatched input '+' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} @@ -219,7 +279,7 @@ I: [1, 2, 3].map(var, var * var) E: ERROR: :1:15: reserved identifier: var | [1, 2, 3].map(var, var * var) | ..............^ -ERROR: :1:15: argument is not an identifier +ERROR: :1:15: The argument must be a simple name | [1, 2, 3].map(var, var * var) | ..............^ ERROR: :1:20: reserved identifier: var @@ -362,4 +422,4 @@ ERROR: :1:6: unsupported syntax '`' | .....^ ERROR: :1:9: missing ')' at '' | has(.`.` - | ........^ + | ........^ \ No newline at end of file From 2bf3353b7662f19aafe7c3ebb93223d850307027 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 22 Jun 2026 10:26:30 -0700 Subject: [PATCH 108/204] Switch constant folding to pre-order traversal PiperOrigin-RevId: 936127570 --- .../dev/cel/optimizer/optimizers/BUILD.bazel | 1 + .../optimizers/ConstantFoldingOptimizer.java | 20 +++++++++++++++---- .../ConstantFoldingOptimizerTest.java | 13 +++++------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel index c887f3d15..155ae262d 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -28,6 +28,7 @@ java_library( "//common/ast", "//common/ast:mutable_expr", "//common/internal:date_time_helpers", + "//common/navigation:common", "//common/navigation:mutable_navigation", "//common/types", "//extensions:optional_library", diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java index 8a8786ce8..b3c0e3046 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java @@ -41,6 +41,7 @@ import dev.cel.common.internal.DateTimeHelpers; import dev.cel.common.navigation.CelNavigableMutableAst; import dev.cel.common.navigation.CelNavigableMutableExpr; +import dev.cel.common.navigation.TraversalOrder; import dev.cel.common.types.SimpleType; import dev.cel.extensions.CelOptionalLibrary.Function; import dev.cel.optimizer.AstMutator; @@ -111,7 +112,7 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) ImmutableList foldableExprs = CelNavigableMutableAst.fromAst(mutableAst) .getRoot() - .allNodes() + .allNodes(TraversalOrder.PRE_ORDER) .filter(this::canFold) .collect(toImmutableList()); for (CelNavigableMutableExpr foldableExpr : foldableExprs) { @@ -122,7 +123,13 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) mutatedResult = maybePruneBranches(mutableAst, foldableExpr.expr()); if (!mutatedResult.isPresent()) { // Evaluate the call then fold - mutatedResult = maybeFold(optimizerEnv, mutableAst, foldableExpr); + try { + mutatedResult = maybeFold(optimizerEnv, mutableAst, foldableExpr); + } catch (CelEvaluationException e) { + throw new CelOptimizationException( + "Constant folding failure. Failed to evaluate subtree due to: " + e.getMessage(), + e); + } } if (!mutatedResult.isPresent()) { @@ -132,12 +139,17 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) continueFolding = true; mutableAst = mutatedResult.get(); + // Break the loop because we mutated the AST. Since we traverse in PRE_ORDER (top-down), + // mutating a parent node means its children are now obsolete or folded. + // We restart the traversal to gather a fresh list of foldable expressions. + break; } } // If the output is a list, map, or struct which contains optional entries, then prune it // to make sure that the optionals, if resolved, do not surface in the output literal. mutableAst = pruneOptionalElements(mutableAst); + return OptimizationResult.create(astMutator.renumberIdsConsecutively(mutableAst).toParsedAst()); } @@ -280,11 +292,11 @@ private static boolean isNestedComprehension(CelNavigableMutableExpr expr) { private Optional maybeFold( Cel cel, CelMutableAst mutableAst, CelNavigableMutableExpr node) - throws CelOptimizationException { + throws CelOptimizationException, CelEvaluationException { Object result; try { result = evaluateExpr(cel, node); - } catch (CelValidationException | CelEvaluationException e) { + } catch (CelValidationException e) { throw new CelOptimizationException( "Constant folding failure. Failed to evaluate subtree due to: " + e.getMessage(), e); } diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java index 33dc2d941..ab5508064 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java @@ -252,6 +252,9 @@ private static Cel setupEnv(CelBuilder celBuilder) { @TestParameters( "{source: 'timestamp(\"2000-01-01T00:02:03.2123Z\") + duration(\"25h2m32s42ms53us29ns\")'," + " expected: 'timestamp(\"2000-01-02T01:04:35.254353029Z\")'}") + @TestParameters( + "{source: 'has({\"req\": \"Avail\"}.opt) ? ({\"req\": \"Avail\"}.req + \" \" +" + + " {\"req\": \"Avail\"}.opt) : {\"req\": \"Avail\"}.req', expected: '\"Avail\"'}") // TODO: Support folding lists with mixed types. This requires mutable lists. // @TestParameters("{source: 'dyn([1]) + [1.0]'}") public void constantFold_success(String source, String expected) throws Exception { @@ -534,26 +537,20 @@ public void constantFold_astProducesConsistentlyNumberedIds() throws Exception { @Test public void iterationLimitReached_throws() throws Exception { - StringBuilder sb = new StringBuilder(); - sb.append("0"); - for (int i = 1; i < 200; i++) { - sb.append(" + ").append(i); - } // 0 + 1 + 2 + 3 + ... 200 Cel cel = runtimeFlavor .builder() .setOptions( CelOptions.current() .enableHeterogeneousNumericComparisons(true) - .maxParseRecursionDepth(200) .build()) .build(); - CelAbstractSyntaxTree ast = cel.compile(sb.toString()).getAst(); + CelAbstractSyntaxTree ast = cel.compile("1 + 1").getAst(); CelOptimizer optimizer = CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers( ConstantFoldingOptimizer.newInstance( - ConstantFoldingOptions.newBuilder().maxIterationLimit(200).build())) + ConstantFoldingOptions.newBuilder().maxIterationLimit(1).build())) .build(); CelOptimizationException e = From 751a0278b4294fa2bd9f89b865c956e10134836b Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 22 Jun 2026 23:19:42 -0700 Subject: [PATCH 109/204] Internal Changes PiperOrigin-RevId: 936453582 --- .../src/main/java/dev/cel/common/CelAbstractSyntaxTree.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/common/src/main/java/dev/cel/common/CelAbstractSyntaxTree.java b/common/src/main/java/dev/cel/common/CelAbstractSyntaxTree.java index 6b3b6a74f..b79c67e79 100644 --- a/common/src/main/java/dev/cel/common/CelAbstractSyntaxTree.java +++ b/common/src/main/java/dev/cel/common/CelAbstractSyntaxTree.java @@ -103,6 +103,11 @@ public Optional getType(long exprId) { return Optional.ofNullable(types().get(exprId)); } + public CelType getTypeOrThrow(long exprId) { + return getType(exprId) + .orElseThrow(() -> new NoSuchElementException("Type not found for expr id: " + exprId)); + } + public ImmutableMap getTypeMap() { return types(); } From 2173c340cc801b14c6dbae72021abf76ba6b8094 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 23 Jun 2026 15:01:08 -0700 Subject: [PATCH 110/204] Internal Changes PiperOrigin-RevId: 936911294 --- .../java/dev/cel/checker/CelStandardDeclarations.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java b/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java index 12ad47c62..53615604f 100644 --- a/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java +++ b/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java @@ -31,6 +31,7 @@ import dev.cel.common.types.SimpleType; import dev.cel.common.types.TypeParamType; import dev.cel.common.types.TypeType; +import java.util.Optional; /** * Standard declarations for CEL. @@ -1474,6 +1475,16 @@ public boolean isHeterogeneousComparison() { public CelOverloadDecl celOverloadDecl() { return this.celOverloadDecl; } + + /** Finds a Comparison by its overload ID. */ + public static Optional fromOverloadId(String overloadId) { + for (Comparison c : values()) { + if (c.celOverloadDecl().overloadId().equals(overloadId)) { + return Optional.of(c); + } + } + return Optional.empty(); + } } private Overload() {} From 534f651354589fdb9c0f42f90d5eb331d1466538 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 24 Jun 2026 11:41:35 -0700 Subject: [PATCH 111/204] Fix optional macro calls to be foldable PiperOrigin-RevId: 937467020 --- .../dev/cel/extensions/CelExtensions.java | 9 ++++ .../dev/cel/extensions/CelExtensionsTest.java | 6 +++ .../optimizers/ConstantFoldingOptimizer.java | 43 +++++++++++-------- .../ConstantFoldingOptimizerTest.java | 5 +++ 4 files changed, 45 insertions(+), 18 deletions(-) diff --git a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java index 8adc39384..446fa26e7 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java @@ -23,6 +23,7 @@ import dev.cel.extensions.CelMathExtensions.Function; import java.util.EnumSet; import java.util.Set; +import java.util.stream.Stream; /** * Collections of CEL Extensions. @@ -381,6 +382,14 @@ public static ImmutableSet getAllFunctionNames() { .map(CelListsExtensions.Function::getFunction), EnumSet.allOf(CelRegexExtensions.Function.class).stream() .map(CelRegexExtensions.Function::getFunction), + Stream.of( + CelOptionalLibrary.Function.VALUE, + CelOptionalLibrary.Function.HAS_VALUE, + CelOptionalLibrary.Function.OPTIONAL_NONE, + CelOptionalLibrary.Function.OPTIONAL_OF, + CelOptionalLibrary.Function.OPTIONAL_UNWRAP, + CelOptionalLibrary.Function.OPTIONAL_OF_NON_ZERO_VALUE) + .map(CelOptionalLibrary.Function::getFunction), EnumSet.allOf(CelComprehensionsExtensions.Function.class).stream() .map(CelComprehensionsExtensions.Function::getFunction)) .collect(toImmutableSet()); diff --git a/extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java index 192630ea3..31c7d65c8 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java @@ -189,6 +189,12 @@ public void getAllFunctionNames() { "regex.replace", "regex.extract", "regex.extractAll", + "value", + "hasValue", + "optional.none", + "optional.of", + "optional.unwrap", + "optional.ofNonZeroValue", "cel.@mapInsert"); } } diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java index b3c0e3046..46f801bb8 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java @@ -369,30 +369,37 @@ private Optional maybeAdaptEvaluatedResult(Object result) { private Optional maybeRewriteOptional( Optional optResult, CelMutableAst mutableAst, CelMutableExpr expr) { - if (!optResult.isPresent()) { - if (!expr.call().function().equals(Function.OPTIONAL_NONE.getFunction())) { - // An empty optional value was encountered. Rewrite the tree with optional.none call. - // This is to account for other optional functions returning an empty optional value - // e.g: optional.ofNonZeroValue(0) - return Optional.of(astMutator.replaceSubtree(mutableAst, newOptionalNoneExpr(), expr.id())); - } - } else if (!expr.call().function().equals(Function.OPTIONAL_OF.getFunction())) { - Object unwrappedResult = optResult.get(); - if (!CelConstant.isConstantValue(unwrappedResult)) { - // Evaluated result is not a constant. Leave the optional as is. + Object unwrappedResult = optResult.orElse(null); + if (unwrappedResult == null) { + if (isCallToFunction(expr, Function.OPTIONAL_NONE.getFunction())) { return Optional.empty(); } + // An empty optional value was encountered. Rewrite the tree with optional.none call. + // This is to account for other optional functions returning an empty optional value + // e.g: optional.ofNonZeroValue(0) + return Optional.of(astMutator.replaceSubtree(mutableAst, newOptionalNoneExpr(), expr.id())); + } - CelMutableExpr newOptionalOfCall = - CelMutableExpr.ofCall( - CelMutableCall.create( - Function.OPTIONAL_OF.getFunction(), - CelMutableExpr.ofConstant(CelConstant.ofObjectValue(unwrappedResult)))); + if (isCallToFunction(expr, Function.OPTIONAL_OF.getFunction())) { + return Optional.empty(); + } - return Optional.of(astMutator.replaceSubtree(mutableAst, newOptionalOfCall, expr.id())); + if (!CelConstant.isConstantValue(unwrappedResult)) { + // Evaluated result is not a constant. Leave the optional as is. + return Optional.empty(); } - return Optional.empty(); + CelMutableExpr newOptionalOfCall = + CelMutableExpr.ofCall( + CelMutableCall.create( + Function.OPTIONAL_OF.getFunction(), + CelMutableExpr.ofConstant(CelConstant.ofObjectValue(unwrappedResult)))); + + return Optional.of(astMutator.replaceSubtree(mutableAst, newOptionalOfCall, expr.id())); + } + + private static boolean isCallToFunction(CelMutableExpr expr, String functionName) { + return expr.getKind().equals(Kind.CALL) && expr.call().function().equals(functionName); } /** Inspects the non-strict calls to determine whether a branch can be removed. */ diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java index ab5508064..66f5a94d7 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java @@ -302,6 +302,9 @@ public void constantFold_success(String source, String expected) throws Exceptio "{source: 'cel.bind(r0, [1, 2, 3], cel.bind(r1, 1 in r0 && 2 in x, r1))', expected:" + " 'cel.bind(r0, [1, 2, 3], cel.bind(r1, 1 in r0 && 2 in x, r1))'}") @TestParameters("{source: 'false ? false : cel.bind(a, x, a)', expected: 'cel.bind(a, x, a)'}") + @TestParameters( + "{source: 'cel.bind(myMap, {\"foo\": \"bar\"}, myMap[?\"foo\"].optMap(x, x + \"baz\"))', " + + "expected: 'optional.of(\"barbaz\")'}") public void constantFold_macros_macroCallMetadataPopulated(String source, String expected) throws Exception { Cel cel = @@ -557,4 +560,6 @@ public void iterationLimitReached_throws() throws Exception { assertThrows(CelOptimizationException.class, () -> optimizer.optimize(ast)); assertThat(e).hasMessageThat().contains("Optimization failure: Max iteration count reached."); } + + } From 3e7dea1648edc08c701de45e778f54e640b77d8a Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 26 Jun 2026 10:45:57 -0700 Subject: [PATCH 112/204] Support pruning aggregate literals in optionals PiperOrigin-RevId: 938667258 --- .../optimizers/ConstantFoldingOptimizer.java | 57 +++++++++++++++---- .../ConstantFoldingOptimizerTest.java | 20 ++++++- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java index 46f801bb8..35d181905 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java @@ -58,7 +58,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Optional; /** @@ -309,8 +308,12 @@ private Optional maybeFold( return maybeRewriteOptional(optResult, mutableAst, node.expr()); } - return maybeAdaptEvaluatedResult(result) - .map(celExpr -> astMutator.replaceSubtree(mutableAst, celExpr, node.id())); + CelMutableExpr adaptedResult = maybeAdaptEvaluatedResult(result).orElse(null); + if (adaptedResult == null) { + return Optional.empty(); + } + + return Optional.of(astMutator.replaceSubtree(mutableAst, adaptedResult, node.id())); } private Optional maybeAdaptEvaluatedResult(Object result) { @@ -331,7 +334,7 @@ private Optional maybeAdaptEvaluatedResult(Object result) { } else if (result instanceof Map) { Map map = (Map) result; List mapEntries = new ArrayList<>(); - for (Entry entry : map.entrySet()) { + for (Map.Entry entry : map.entrySet()) { CelMutableExpr adaptedKey = maybeAdaptEvaluatedResult(entry.getKey()).orElse(null); if (adaptedKey == null) { return Optional.empty(); @@ -384,16 +387,15 @@ private Optional maybeRewriteOptional( return Optional.empty(); } - if (!CelConstant.isConstantValue(unwrappedResult)) { - // Evaluated result is not a constant. Leave the optional as is. + CelMutableExpr adaptedResult = maybeAdaptEvaluatedResult(unwrappedResult).orElse(null); + if (adaptedResult == null) { + // Evaluated result is not an adaptable constant. Leave the optional as is. return Optional.empty(); } CelMutableExpr newOptionalOfCall = CelMutableExpr.ofCall( - CelMutableCall.create( - Function.OPTIONAL_OF.getFunction(), - CelMutableExpr.ofConstant(CelConstant.ofObjectValue(unwrappedResult)))); + CelMutableCall.create(Function.OPTIONAL_OF.getFunction(), adaptedResult)); return Optional.of(astMutator.replaceSubtree(mutableAst, newOptionalOfCall, expr.id())); } @@ -530,6 +532,37 @@ private Optional maybeShortCircuitCall( "Folding variadic logical operator is not supported yet."); } + private boolean isFoldedAggregateLiteral(CelMutableExpr expr) { + if (expr.getKind().equals(Kind.CONSTANT)) { + return true; + } + if (expr.getKind().equals(Kind.LIST)) { + for (CelMutableExpr child : expr.list().elements()) { + if (!isFoldedAggregateLiteral(child)) { + return false; + } + } + return true; + } + if (expr.getKind().equals(Kind.MAP)) { + for (CelMutableExpr.CelMutableMap.Entry entry : expr.map().entries()) { + if (!isFoldedAggregateLiteral(entry.key()) || !isFoldedAggregateLiteral(entry.value())) { + return false; + } + } + return true; + } + if (expr.getKind().equals(Kind.STRUCT)) { + for (CelMutableExpr.CelMutableStruct.Entry entry : expr.struct().entries()) { + if (!isFoldedAggregateLiteral(entry.value())) { + return false; + } + } + return true; + } + return false; + } + private CelMutableAst pruneOptionalElements(CelMutableAst ast) { ImmutableList aggregateLiterals = CelNavigableMutableExpr.fromExpr(ast.expr()) @@ -588,7 +621,7 @@ private CelMutableAst pruneOptionalListElements(CelMutableAst mutableAst, CelMut continue; } else if (call.function().equals(Function.OPTIONAL_OF.getFunction())) { CelMutableExpr arg = call.args().get(0); - if (arg.getKind().equals(Kind.CONSTANT)) { + if (isFoldedAggregateLiteral(arg)) { updatedElemBuilder.add(call.args().get(0)); continue; } @@ -629,7 +662,7 @@ private CelMutableAst pruneOptionalMapElements(CelMutableAst ast, CelMutableExpr continue; } else if (call.function().equals(Function.OPTIONAL_OF.getFunction())) { CelMutableExpr arg = call.args().get(0); - if (arg.getKind().equals(Kind.CONSTANT)) { + if (isFoldedAggregateLiteral(arg)) { modified = true; entry.setOptionalEntry(false); entry.setValue(call.args().get(0)); @@ -670,7 +703,7 @@ private CelMutableAst pruneOptionalStructElements(CelMutableAst ast, CelMutableE continue; } else if (call.function().equals(Function.OPTIONAL_OF.getFunction())) { CelMutableExpr arg = call.args().get(0); - if (arg.getKind().equals(Kind.CONSTANT)) { + if (isFoldedAggregateLiteral(arg)) { modified = true; entry.setOptionalEntry(false); entry.setValue(call.args().get(0)); diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java index 66f5a94d7..ee62c5ed1 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java @@ -255,6 +255,24 @@ private static Cel setupEnv(CelBuilder celBuilder) { @TestParameters( "{source: 'has({\"req\": \"Avail\"}.opt) ? ({\"req\": \"Avail\"}.req + \" \" +" + " {\"req\": \"Avail\"}.opt) : {\"req\": \"Avail\"}.req', expected: '\"Avail\"'}") + @TestParameters("{source: 'true || optional.none().hasValue()', expected: 'true'}") + @TestParameters("{source: 'false && map_var[?\"missing\"].hasValue()', expected: 'false'}") + @TestParameters("{source: '{\"hello\": [1, 2]}.?hello', expected: 'optional.of([1, 2])'}") + @TestParameters( + "{source: '{?\"key\": optional.of({\"a\": 1})}', expected: '{\"key\": {\"a\": 1}}'}") + @TestParameters( + "{source: 'TestAllTypes{?repeated_int32: optional.of([1, 2])}'," + + " expected: 'cel.expr.conformance.proto3.TestAllTypes{repeated_int32: [1, 2]}'}") + @TestParameters("{source: '[?optional.of([1, x])]', expected: '[?optional.of([1, x])]'}") + @TestParameters("{source: '[?optional.of({\"a\": x})]', expected: '[?optional.of({\"a\": x})]'}") + @TestParameters("{source: '[?optional.of({x: 1})]', expected: '[?optional.of({x: 1})]'}") + @TestParameters( + "{source: '[?optional.of(TestAllTypes{single_int32: x})]', expected:" + + " '[?optional.of(cel.expr.conformance.proto3.TestAllTypes{single_int32: x})]'}") + @TestParameters( + "{source: '[?optional.of(TestAllTypes{single_int32: 1})]', expected:" + + " '[cel.expr.conformance.proto3.TestAllTypes{single_int32: 1}]'}") + @TestParameters("{source: '[?optional.of(x)]', expected: '[?optional.of(x)]'}") // TODO: Support folding lists with mixed types. This requires mutable lists. // @TestParameters("{source: 'dyn([1]) + [1.0]'}") public void constantFold_success(String source, String expected) throws Exception { @@ -560,6 +578,4 @@ public void iterationLimitReached_throws() throws Exception { assertThrows(CelOptimizationException.class, () -> optimizer.optimize(ast)); assertThat(e).hasMessageThat().contains("Optimization failure: Max iteration count reached."); } - - } From 0b2bf66c7e0dceeb7de5479b389103baac72b2d4 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 30 Jun 2026 15:04:53 -0700 Subject: [PATCH 113/204] Create CelBlock abstraction to centralize cel.@block logic PiperOrigin-RevId: 940677666 --- common/ast/BUILD.bazel | 6 + .../main/java/dev/cel/common/ast/BUILD.bazel | 14 ++ .../java/dev/cel/common/ast/CelBlock.java | 144 ++++++++++++++++++ .../dev/cel/optimizer/optimizers/BUILD.bazel | 1 + .../optimizers/SubexpressionOptimizer.java | 59 +------ .../SubexpressionOptimizerTest.java | 56 ++++--- .../java/dev/cel/runtime/planner/BUILD.bazel | 1 + .../cel/runtime/planner/ProgramPlanner.java | 35 ++--- 8 files changed, 217 insertions(+), 99 deletions(-) create mode 100644 common/src/main/java/dev/cel/common/ast/CelBlock.java diff --git a/common/ast/BUILD.bazel b/common/ast/BUILD.bazel index 276db0322..302abfc79 100644 --- a/common/ast/BUILD.bazel +++ b/common/ast/BUILD.bazel @@ -11,6 +11,12 @@ java_library( exports = ["//common/src/main/java/dev/cel/common/ast"], ) +java_library( + name = "cel_block", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/ast:cel_block"], +) + cel_android_library( name = "ast_android", exports = ["//common/src/main/java/dev/cel/common/ast:ast_android"], diff --git a/common/src/main/java/dev/cel/common/ast/BUILD.bazel b/common/src/main/java/dev/cel/common/ast/BUILD.bazel index 3fc709a07..46c235d1f 100644 --- a/common/src/main/java/dev/cel/common/ast/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/ast/BUILD.bazel @@ -57,6 +57,20 @@ java_library( ], ) +java_library( + name = "cel_block", + srcs = ["CelBlock.java"], + tags = [ + ], + deps = [ + ":ast", + "//common:cel_ast", + "//common/annotations", + "//common/navigation", + "@maven//:com_google_guava_guava", + ], +) + java_library( name = "expr_converter", srcs = EXPR_CONVERTER_SOURCES, diff --git a/common/src/main/java/dev/cel/common/ast/CelBlock.java b/common/src/main/java/dev/cel/common/ast/CelBlock.java new file mode 100644 index 000000000..12de6d4dd --- /dev/null +++ b/common/src/main/java/dev/cel/common/ast/CelBlock.java @@ -0,0 +1,144 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.ast; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.annotations.Internal; +import dev.cel.common.navigation.CelNavigableExpr; +import java.util.Optional; + +/** + * Represents a {@code cel.@block} expression. + * + *

CEL Block is used by the CSE (Common Subexpression Elimination) optimizer to hoist common + * subexpressions into an evaluated block. + */ +@Internal +public final class CelBlock { + public static final String FUNCTION_NAME = "cel.@block"; + public static final String INDEX_PREFIX = "@index"; + + private final CelExpr blockExpr; + + private CelBlock(CelExpr blockExpr) { + this.blockExpr = blockExpr; + } + + public ImmutableList indices() { + return blockExpr.call().args().get(0).list().elements(); + } + + public CelExpr result() { + return blockExpr.call().args().get(1); + } + + public CelExpr expr() { + return blockExpr; + } + + /** + * Extracts a {@link CelBlock} from the given AST. + * + *

Enforces the contract that {@code cel.@block} must only appear exactly once and at the root + * of the AST. + * + * @throws IllegalArgumentException if the block is malformed or its indices are invalid. + */ + public static Optional extract(CelAbstractSyntaxTree ast) { + CelNavigableExpr celNavigableExpr = CelNavigableExpr.fromExpr(ast.getExpr()); + + ImmutableList allCelBlocks = + celNavigableExpr + .allNodes() + .map(CelNavigableExpr::expr) + .filter(expr -> expr.callOrDefault().function().equals(FUNCTION_NAME)) + .collect(toImmutableList()); + if (allCelBlocks.isEmpty()) { + return Optional.empty(); + } + + Preconditions.checkArgument( + allCelBlocks.size() == 1, + "Expected 1 cel.block function to be present but found %s", + allCelBlocks.size()); + Preconditions.checkArgument( + celNavigableExpr.expr().equals(allCelBlocks.get(0)), + "Expected cel.block to be present at root"); + + return Optional.of(fromExpr(allCelBlocks.get(0))); + } + + /** + * Constructs a {@link CelBlock} from a {@link CelExpr}. + * + * @throws IllegalArgumentException if the expression is not a valid block. + */ + private static CelBlock fromExpr(CelExpr expr) { + Preconditions.checkArgument( + expr.exprKind().getKind() == CelExpr.ExprKind.Kind.CALL, + "Expected cel.@block to be a call expression"); + Preconditions.checkArgument( + expr.call().function().equals(FUNCTION_NAME), "Expected function to be cel.@block"); + Preconditions.checkArgument( + expr.call().args().size() == 2, "Expected exactly 2 arguments for cel.@block"); + Preconditions.checkArgument( + expr.call().args().get(0).exprKind().getKind() == CelExpr.ExprKind.Kind.LIST, + "Expected first argument of cel.@block to be a list"); + + CelBlock block = new CelBlock(expr); + + // Assert correctness on block indices used in subexpressions + ImmutableList subexprs = block.indices(); + for (int i = 0; i < subexprs.size(); i++) { + verifyBlockIndex(subexprs.get(i), i, expr); + } + + // Assert correctness on block indices used in block result + CelExpr blockResult = block.result(); + verifyBlockIndex(blockResult, subexprs.size(), expr); + boolean resultHasAtLeastOneBlockIndex = + CelNavigableExpr.fromExpr(blockResult) + .allNodes() + .map(CelNavigableExpr::expr) + .anyMatch(e -> e.identOrDefault().name().startsWith(INDEX_PREFIX)); + Preconditions.checkArgument( + resultHasAtLeastOneBlockIndex, + "Expected at least one reference of index in cel.block result"); + + return block; + } + + private static void verifyBlockIndex(CelExpr celExpr, int maxIndexValue, CelExpr rootBlock) { + boolean areAllIndicesValid = + CelNavigableExpr.fromExpr(celExpr) + .allNodes() + .map(CelNavigableExpr::expr) + .filter(expr -> expr.identOrDefault().name().startsWith(INDEX_PREFIX)) + .map(CelExpr::ident) + .allMatch( + blockIdent -> + Integer.parseInt(blockIdent.name().substring(INDEX_PREFIX.length())) + < maxIndexValue); + Preconditions.checkArgument( + areAllIndicesValid, + "Illegal block index found. The index value must be less than %s. Expr: %s", + maxIndexValue, + rootBlock); + } +} diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel index 155ae262d..35476a792 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -60,6 +60,7 @@ java_library( "//common:mutable_ast", "//common:mutable_source", "//common/ast", + "//common/ast:cel_block", "//common/ast:mutable_expr", "//common/navigation", "//common/navigation:common", diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java index ce9a5dc77..5eebb1c54 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java @@ -41,6 +41,7 @@ import dev.cel.common.CelSource.Extension.Version; import dev.cel.common.CelValidationException; import dev.cel.common.CelVarDecl; +import dev.cel.common.ast.CelBlock; import dev.cel.common.ast.CelExpr; import dev.cel.common.ast.CelExpr.CelCall; import dev.cel.common.ast.CelExpr.CelComprehension; @@ -238,64 +239,12 @@ private OptimizationResult optimizeUsingCelBlock(CelAbstractSyntaxTree ast, Cel */ @VisibleForTesting static void verifyOptimizedAstCorrectness(CelAbstractSyntaxTree ast) { - CelNavigableExpr celNavigableExpr = CelNavigableExpr.fromExpr(ast.getExpr()); - - ImmutableList allCelBlocks = - celNavigableExpr - .allNodes() - .map(CelNavigableExpr::expr) - .filter(expr -> expr.callOrDefault().function().equals(CEL_BLOCK_FUNCTION)) - .collect(toImmutableList()); - if (allCelBlocks.isEmpty()) { + CelBlock celBlock = CelBlock.extract(ast).orElse(null); + if (celBlock == null) { return; } - CelExpr celBlockExpr = allCelBlocks.get(0); - Verify.verify( - allCelBlocks.size() == 1, - "Expected 1 cel.block function to be present but found %s", - allCelBlocks.size()); - Verify.verify( - celNavigableExpr.expr().equals(celBlockExpr), "Expected cel.block to be present at root"); - - // Assert correctness on block indices used in subexpressions - CelCall celBlockCall = celBlockExpr.call(); - ImmutableList subexprs = celBlockCall.args().get(0).list().elements(); - for (int i = 0; i < subexprs.size(); i++) { - verifyBlockIndex(subexprs.get(i), i); - } - - // Assert correctness on block indices used in block result - CelExpr blockResult = celBlockCall.args().get(1); - verifyBlockIndex(blockResult, subexprs.size()); - boolean resultHasAtLeastOneBlockIndex = - CelNavigableExpr.fromExpr(blockResult) - .allNodes() - .map(CelNavigableExpr::expr) - .anyMatch(expr -> expr.identOrDefault().name().startsWith(BLOCK_INDEX_PREFIX)); - Verify.verify( - resultHasAtLeastOneBlockIndex, - "Expected at least one reference of index in cel.block result"); - - verifyNoInvalidScopedMangledVariables(celBlockExpr); - } - - private static void verifyBlockIndex(CelExpr celExpr, int maxIndexValue) { - boolean areAllIndicesValid = - CelNavigableExpr.fromExpr(celExpr) - .allNodes() - .map(CelNavigableExpr::expr) - .filter(expr -> expr.identOrDefault().name().startsWith(BLOCK_INDEX_PREFIX)) - .map(CelExpr::ident) - .allMatch( - blockIdent -> - Integer.parseInt(blockIdent.name().substring(BLOCK_INDEX_PREFIX.length())) - < maxIndexValue); - Verify.verify( - areAllIndicesValid, - "Illegal block index found. The index value must be less than %s. Expr: %s", - maxIndexValue, - celExpr); + verifyNoInvalidScopedMangledVariables(celBlock.expr()); } private static void verifyNoInvalidScopedMangledVariables(CelExpr celExpr) { diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java index e7387d7d8..1a36bd16b 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java @@ -18,7 +18,6 @@ import static dev.cel.common.CelOverloadDecl.newGlobalOverload; import static org.junit.Assert.assertThrows; -import com.google.common.base.VerifyException; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.testing.junit.testparameterinjector.TestParameter; @@ -65,6 +64,7 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.Before; import org.junit.Test; +import org.junit.function.ThrowingRunnable; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) @@ -377,18 +377,22 @@ public void celBlock_astExtensionTagged() throws Exception { Extension.create("cel_block", Version.of(1L, 1L), Component.COMPONENT_RUNTIME)); } + @SuppressWarnings("Immutable") // Test only private enum BlockTestCase { - BOOL_LITERAL("cel.block([true, false], index0 || index1)"), - STRING_CONCAT("cel.block(['a' + 'b', index0 + 'c'], index1 + 'd') == 'abcd'"), + BOOL_LITERAL("cel.block([true, false], index0 || index1)", true), + STRING_CONCAT("cel.block(['a' + 'b', index0 + 'c'], index1 + 'd')", "abcd"), - BLOCK_WITH_EXISTS_TRUE("cel.block([[1, 2, 3], [3, 4, 5].exists(e, e in index0)], index1)"), - BLOCK_WITH_EXISTS_FALSE("cel.block([[1, 2, 3], ![4, 5].exists(e, e in index0)], index1)"), + BLOCK_WITH_EXISTS_TRUE( + "cel.block([[1, 2, 3], [3, 4, 5].exists(e, e in index0)], index1)", true), + BLOCK_WITH_EXISTS_FALSE("cel.block([[1, 2, 3], ![4, 5].exists(e, e in index0)], index1)", true), ; private final String source; + private final Object expectedResult; - BlockTestCase(String source) { + BlockTestCase(String source, Object expectedResult) { this.source = source; + this.expectedResult = expectedResult; } } @@ -398,7 +402,7 @@ public void block_success(@TestParameter BlockTestCase testCase) throws Exceptio Object evaluatedResult = celForEvaluatingBlock.createProgram(ast).eval(); - assertThat(evaluatedResult).isNotNull(); + assertThat(evaluatedResult).isEqualTo(testCase.expectedResult); } @Test @@ -411,7 +415,7 @@ public void block_success_parsedOnly(@TestParameter BlockTestCase testCase) thro Object evaluatedResult = celForEvaluatingBlock.createProgram(ast).eval(); - assertThat(evaluatedResult).isNotNull(); + assertThat(evaluatedResult).isEqualTo(testCase.expectedResult); } @Test @@ -604,9 +608,10 @@ public void verifyOptimizedAstCorrectness_twoCelBlocks_throws() throws Exception CelAbstractSyntaxTree ast = compileUsingInternalFunctions("cel.block([1, 2], cel.block([2], 3))"); - VerifyException e = + IllegalArgumentException e = assertThrows( - VerifyException.class, () -> SubexpressionOptimizer.verifyOptimizedAstCorrectness(ast)); + IllegalArgumentException.class, + () -> SubexpressionOptimizer.verifyOptimizedAstCorrectness(ast)); assertThat(e) .hasMessageThat() .isEqualTo("Expected 1 cel.block function to be present but found 2"); @@ -616,9 +621,10 @@ public void verifyOptimizedAstCorrectness_twoCelBlocks_throws() throws Exception public void verifyOptimizedAstCorrectness_celBlockNotAtRoot_throws() throws Exception { CelAbstractSyntaxTree ast = compileUsingInternalFunctions("1 + cel.block([1, 2], index0)"); - VerifyException e = + IllegalArgumentException e = assertThrows( - VerifyException.class, () -> SubexpressionOptimizer.verifyOptimizedAstCorrectness(ast)); + IllegalArgumentException.class, + () -> SubexpressionOptimizer.verifyOptimizedAstCorrectness(ast)); assertThat(e).hasMessageThat().isEqualTo("Expected cel.block to be present at root"); } @@ -626,9 +632,10 @@ public void verifyOptimizedAstCorrectness_celBlockNotAtRoot_throws() throws Exce public void verifyOptimizedAstCorrectness_blockContainsNoIndexResult_throws() throws Exception { CelAbstractSyntaxTree ast = compileUsingInternalFunctions("cel.block([1, index0], 2)"); - VerifyException e = + IllegalArgumentException e = assertThrows( - VerifyException.class, () -> SubexpressionOptimizer.verifyOptimizedAstCorrectness(ast)); + IllegalArgumentException.class, + () -> SubexpressionOptimizer.verifyOptimizedAstCorrectness(ast)); assertThat(e) .hasMessageThat() .isEqualTo("Expected at least one reference of index in cel.block result"); @@ -641,9 +648,10 @@ public void verifyOptimizedAstCorrectness_indexOutOfBounds_throws(String source) throws Exception { CelAbstractSyntaxTree ast = compileUsingInternalFunctions(source); - VerifyException e = + IllegalArgumentException e = assertThrows( - VerifyException.class, () -> SubexpressionOptimizer.verifyOptimizedAstCorrectness(ast)); + IllegalArgumentException.class, + () -> SubexpressionOptimizer.verifyOptimizedAstCorrectness(ast)); assertThat(e) .hasMessageThat() .contains("Illegal block index found. The index value must be less than"); @@ -658,9 +666,10 @@ public void verifyOptimizedAstCorrectness_indexIsNotForwardReferencing_throws(St throws Exception { CelAbstractSyntaxTree ast = compileUsingInternalFunctions(source); - VerifyException e = + IllegalArgumentException e = assertThrows( - VerifyException.class, () -> SubexpressionOptimizer.verifyOptimizedAstCorrectness(ast)); + IllegalArgumentException.class, + () -> SubexpressionOptimizer.verifyOptimizedAstCorrectness(ast)); assertThat(e) .hasMessageThat() .contains("Illegal block index found. The index value must be less than"); @@ -670,9 +679,14 @@ public void verifyOptimizedAstCorrectness_indexIsNotForwardReferencing_throws(St public void block_containsCycle_throws() throws Exception { CelAbstractSyntaxTree ast = compileUsingInternalFunctions("cel.block([index1,index0],index0)"); - CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); - assertThat(e).hasMessageThat().contains("Cycle detected: @index0"); + ThrowingRunnable evaluateProgram = () -> cel.createProgram(ast).eval(); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, evaluateProgram); + assertThat(e) + .hasMessageThat() + .containsMatch( + "Cycle detected: @index0|Illegal block index found. The index value must be less than" + + " 0."); } @Test diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index 801e56d73..e82f77c67 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -49,6 +49,7 @@ java_library( "//common:options", "//common/annotations", "//common/ast", + "//common/ast:cel_block", "//common/exceptions:overload_not_found", "//common/types", "//common/types:type_providers", diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index e38d08f8f..6bb3d1e22 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -26,6 +26,7 @@ import dev.cel.common.CelOptions; import dev.cel.common.Operator; import dev.cel.common.annotations.Internal; +import dev.cel.common.ast.CelBlock; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; import dev.cel.common.ast.CelExpr.CelCall; @@ -79,7 +80,11 @@ public Program plan(CelAbstractSyntaxTree ast) throws CelEvaluationException { ErrorMetadata errorMetadata = ErrorMetadata.create(ast.getSource().getPositionsMap(), ast.getSource().getDescription()); try { - plannedInterpretable = plan(ast.getExpr(), PlannerContext.create(ast)); + PlannerContext ctx = PlannerContext.create(ast); + plannedInterpretable = + CelBlock.extract(ast) + .map(celBlock -> planBlock(celBlock, ctx)) + .orElseGet(() -> plan(ast.getExpr(), ctx)); } catch (RuntimeException e) { throw CelEvaluationExceptionBuilder.newBuilder(e.getMessage()) .setMetadata(errorMetadata, ast.getExpr().id()) @@ -231,11 +236,6 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { ResolvedFunction resolvedFunction = resolveFunction(expr, ctx.referenceMap()); String functionName = resolvedFunction.functionName(); - PlannedInterpretable blockCall = maybeInterceptBlockCall(functionName, expr, ctx).orElse(null); - if (blockCall != null) { - return blockCall; - } - CelExpr target = resolvedFunction.target().orElse(null); int argCount = expr.call().args().size(); if (target != null) { @@ -331,26 +331,15 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { } } - private Optional maybeInterceptBlockCall( - String functionName, CelExpr expr, PlannerContext ctx) { - if (!functionName.equals("cel.@block")) { - return Optional.empty(); - } - - CelCall blockCall = expr.call(); - - if (blockCall.args().size() != 2) { - throw new IllegalArgumentException( - "Expected 2 arguments for cel.@block call. Got: " + blockCall.args().size()); - } + private PlannedInterpretable planBlock(CelBlock celBlock, PlannerContext ctx) { + ImmutableList indices = celBlock.indices(); - CelList exprList = blockCall.args().get(0).list(); - PlannedInterpretable[] slotExprs = new PlannedInterpretable[exprList.elements().size()]; + PlannedInterpretable[] slotExprs = new PlannedInterpretable[indices.size()]; for (int i = 0; i < slotExprs.length; i++) { - slotExprs[i] = plan(exprList.elements().get(i), ctx); + slotExprs[i] = plan(indices.get(i), ctx); } - PlannedInterpretable resultExpr = plan(blockCall.args().get(1), ctx); - return Optional.of(EvalBlock.create(expr, slotExprs, resultExpr)); + PlannedInterpretable resultExpr = plan(celBlock.result(), ctx); + return EvalBlock.create(celBlock.expr(), slotExprs, resultExpr); } /** From 8f05b8e965ec38dea62340eb6ae0798b69eacf44 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 30 Jun 2026 17:00:21 -0700 Subject: [PATCH 114/204] Update documentation and CEL-Java codelabs to use planner PiperOrigin-RevId: 940733975 --- README.md | 6 ++-- codelab/README.md | 33 +++++++------------ codelab/src/main/codelab/Exercise3.java | 3 +- codelab/src/main/codelab/Exercise4.java | 2 +- codelab/src/main/codelab/Exercise5.java | 2 +- codelab/src/main/codelab/Exercise6.java | 4 +-- codelab/src/main/codelab/Exercise7.java | 4 +-- codelab/src/main/codelab/Exercise8.java | 2 +- codelab/src/main/codelab/Exercise9.java | 2 +- .../src/main/codelab/solutions/Exercise1.java | 2 +- .../src/main/codelab/solutions/Exercise2.java | 2 +- .../src/main/codelab/solutions/Exercise3.java | 3 +- .../src/main/codelab/solutions/Exercise4.java | 2 +- .../src/main/codelab/solutions/Exercise5.java | 2 +- .../src/main/codelab/solutions/Exercise6.java | 4 +-- .../src/main/codelab/solutions/Exercise7.java | 4 +-- .../src/main/codelab/solutions/Exercise8.java | 7 ++-- .../src/main/codelab/solutions/Exercise9.java | 2 +- 18 files changed, 32 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index e2424a85c..dbcceb7d8 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ public class HelloWorld { private static final CelCompiler CEL_COMPILER = CelCompilerFactory.standardCelCompilerBuilder().addVar("my_var", SimpleType.STRING).build(); private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder().build(); + CelRuntimeFactory.plannerRuntimeBuilder().build(); public void run() throws CelValidationException, CelEvaluationException { // Compile the expression into an Abstract Syntax Tree. @@ -144,7 +144,7 @@ found in the [`CelCompilerBuilder`][9]. Some CEL use cases only require parsing of an expression in order to be useful. For example, one example might want to check whether the expression contains any nested comprehensions, or possibly to pass the parsed expression to a C++ or Go -binary for evaluation. Presently, Java does not support parse-only evaluation. +binary for evaluation. ```java CelValidationResult parseResult = @@ -231,7 +231,7 @@ Expressions can be evaluated using once they are type-checked/compiled by creating a `CelRuntime.Program` from a `CelAbstractSyntaxTree`: ```java -CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build(); +CelRuntime celRuntime = CelRuntimeFactory.plannerRuntimeBuilder().build(); try { CelRuntime.Program program = celRuntime.createProgram(compileResult.getAst()); return program.eval( diff --git a/codelab/README.md b/codelab/README.md index 83257d186..00d7f729e 100644 --- a/codelab/README.md +++ b/codelab/README.md @@ -140,7 +140,7 @@ private static final CelCompiler CEL_COMPILER = // CelRuntime can also be initialized statically and cached just like the // compiler. private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder() + CelRuntimeFactory.plannerRuntimeBuilder() .setOptions(CEL_OPTIONS) .build(); ``` @@ -232,7 +232,7 @@ Copy the following into eval method: Object eval(CelAbstractSyntaxTree ast) { // Construct a CelRuntime instance // CelRuntime is immutable just like the compiler and can be moved to a static final member. - CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build(); + CelRuntime celRuntime = CelRuntimeFactory.plannerRuntimeBuilder().build(); try { // Plan the program @@ -314,7 +314,7 @@ Let's make the evaluation work now. Copy into the eval method: * @throws IllegalArgumentException If the compiled expression in AST fails to evaluate. */ Object eval(CelAbstractSyntaxTree ast, Map parameterValues) { - CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build(); + CelRuntime celRuntime = CelRuntimeFactory.plannerRuntimeBuilder().build(); try { CelRuntime.Program program = celRuntime.createProgram(ast); @@ -510,7 +510,7 @@ CelAbstractSyntaxTree compile(String expression) { */ Object eval(CelAbstractSyntaxTree ast, Map parameterValues) { CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() + CelRuntimeFactory.plannerRuntimeBuilder() // Provide the custom `contains` function implementation here. .build(); @@ -590,7 +590,7 @@ Provide the function implementation to the runtime using the .addFunctionBinding */ Object eval(CelAbstractSyntaxTree ast, Map parameterValues) { CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() + CelRuntimeFactory.plannerRuntimeBuilder() .addFunctionBindings( CelFunctionBinding.from( "map_contains_key_value", @@ -1136,7 +1136,7 @@ private static final CelCompiler CEL_COMPILER = .addMessageTypes(AttributeContext.Request.getDescriptor()) .build(); private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder() + CelRuntimeFactory.plannerRuntimeBuilder() .addMessageTypes(AttributeContext.Request.getDescriptor()) .build(); ``` @@ -1362,11 +1362,11 @@ public void optimize_commonSubexpressionElimination_success() throws Exception { } ``` -CSE will rewrite this expression using a specialized internal function -`cel.@block`. The first argument contain a list duplicate subexpressions -and the second argument is the rewritten result expression that is semantically -the same as the original expression. The subexpressions are lazily evaluated and -memoized when accessed by index (e.g: `@index0`). +CSE optimizes the expression by rewriting it to use a specialized internal +function `cel.@block`. This function takes a list of duplicate subexpressions +as its first argument, and a semantically equivalent rewritten expression as +its second. The subexpressions are lazily evaluated and memoized when accessed +by index (e.g., `@index0`). Make the following changes in `Exercise8.java`: @@ -1375,19 +1375,10 @@ private static final CelOptimizer CEL_OPTIMIZER = CelOptimizerFactory.standardCelOptimizerBuilder(CEL_COMPILER, CEL_RUNTIME) .addAstOptimizers( ConstantFoldingOptimizer.getInstance(), - SubexpressionOptimizer.newInstance( - SubexpressionOptimizerOptions.newBuilder().enableCelBlock(true).build())) + SubexpressionOptimizer.getInstance()) .build(); ``` -As seen here, the usage of `cel.block` must explicitly be enabled as it is -only supported in CEL-Java as of now. Disabling `cel.block` will instead rewrite -the AST using cascaded `cel.bind` macros. Prefer using the block format if -possible as it is a more efficient format for evaluation. - -> [!CAUTION] -> You MUST disable `cel.block` if you are targeting `cel-go` or `cel-cpp` for the runtime until its support has been added in those stacks. - Re-run the tests to confirm that they pass. ## Custom AST Validation diff --git a/codelab/src/main/codelab/Exercise3.java b/codelab/src/main/codelab/Exercise3.java index 77b57f339..1745920f8 100644 --- a/codelab/src/main/codelab/Exercise3.java +++ b/codelab/src/main/codelab/Exercise3.java @@ -27,8 +27,7 @@ final class Exercise3 { private static final CelCompiler CEL_COMPILER = CelCompilerFactory.standardCelCompilerBuilder().setResultType(SimpleType.BOOL).build(); - private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder().build(); + private static final CelRuntime CEL_RUNTIME = CelRuntimeFactory.plannerRuntimeBuilder().build(); /** * Compiles the given expression and evaluates it. diff --git a/codelab/src/main/codelab/Exercise4.java b/codelab/src/main/codelab/Exercise4.java index df4d3ab1c..402255152 100644 --- a/codelab/src/main/codelab/Exercise4.java +++ b/codelab/src/main/codelab/Exercise4.java @@ -63,7 +63,7 @@ CelAbstractSyntaxTree compile(String expression) { */ Object eval(CelAbstractSyntaxTree ast, Map parameterValues) { CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() + CelRuntimeFactory.plannerRuntimeBuilder() // Provide the custom `contains` function implementation here. .build(); diff --git a/codelab/src/main/codelab/Exercise5.java b/codelab/src/main/codelab/Exercise5.java index eca2a5df7..00a64e261 100644 --- a/codelab/src/main/codelab/Exercise5.java +++ b/codelab/src/main/codelab/Exercise5.java @@ -55,7 +55,7 @@ CelAbstractSyntaxTree compile(String expression) { * @throws IllegalArgumentException If the compiled expression in AST fails to evaluate. */ Object eval(CelAbstractSyntaxTree ast, Map parameterValues) { - CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build(); + CelRuntime celRuntime = CelRuntimeFactory.plannerRuntimeBuilder().build(); try { CelRuntime.Program program = celRuntime.createProgram(ast); diff --git a/codelab/src/main/codelab/Exercise6.java b/codelab/src/main/codelab/Exercise6.java index 9991fb566..71d8e09f4 100644 --- a/codelab/src/main/codelab/Exercise6.java +++ b/codelab/src/main/codelab/Exercise6.java @@ -59,9 +59,7 @@ CelAbstractSyntaxTree compile(String expression) { /** Evaluates the compiled AST with the user provided parameter values. */ Object eval(CelAbstractSyntaxTree ast, Map parameterValues) { CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() - .addMessageTypes(Request.getDescriptor()) - .build(); + CelRuntimeFactory.plannerRuntimeBuilder().addMessageTypes(Request.getDescriptor()).build(); try { CelRuntime.Program program = celRuntime.createProgram(ast); diff --git a/codelab/src/main/codelab/Exercise7.java b/codelab/src/main/codelab/Exercise7.java index ce2efd88e..2d2fa2c3a 100644 --- a/codelab/src/main/codelab/Exercise7.java +++ b/codelab/src/main/codelab/Exercise7.java @@ -58,9 +58,7 @@ CelAbstractSyntaxTree compile(String expression) { /** Evaluates the compiled AST with the user provided parameter values. */ Object eval(CelAbstractSyntaxTree ast, Map parameterValues) { CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() - .addMessageTypes(Request.getDescriptor()) - .build(); + CelRuntimeFactory.plannerRuntimeBuilder().addMessageTypes(Request.getDescriptor()).build(); try { CelRuntime.Program program = celRuntime.createProgram(ast); diff --git a/codelab/src/main/codelab/Exercise8.java b/codelab/src/main/codelab/Exercise8.java index d38854687..107e95038 100644 --- a/codelab/src/main/codelab/Exercise8.java +++ b/codelab/src/main/codelab/Exercise8.java @@ -40,7 +40,7 @@ final class Exercise8 { .addMessageTypes(AttributeContext.Request.getDescriptor()) .build(); private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder() + CelRuntimeFactory.plannerRuntimeBuilder() .addMessageTypes(AttributeContext.Request.getDescriptor()) .build(); diff --git a/codelab/src/main/codelab/Exercise9.java b/codelab/src/main/codelab/Exercise9.java index 85705390c..8129800cb 100644 --- a/codelab/src/main/codelab/Exercise9.java +++ b/codelab/src/main/codelab/Exercise9.java @@ -55,7 +55,7 @@ final class Exercise9 { .addMessageTypes(AttributeContext.Request.getDescriptor()) .build(); private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder() + CelRuntimeFactory.plannerRuntimeBuilder() .addMessageTypes(AttributeContext.Request.getDescriptor()) .build(); private static final CelValidator CEL_VALIDATOR = diff --git a/codelab/src/main/codelab/solutions/Exercise1.java b/codelab/src/main/codelab/solutions/Exercise1.java index 0807a1931..e1b3b2269 100644 --- a/codelab/src/main/codelab/solutions/Exercise1.java +++ b/codelab/src/main/codelab/solutions/Exercise1.java @@ -73,7 +73,7 @@ CelAbstractSyntaxTree compile(String expression) { Object eval(CelAbstractSyntaxTree ast) { // Construct a CelRuntime instance // CelRuntime is immutable just like the compiler and can be moved to a static final member. - CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build(); + CelRuntime celRuntime = CelRuntimeFactory.plannerRuntimeBuilder().build(); try { // Plan the program diff --git a/codelab/src/main/codelab/solutions/Exercise2.java b/codelab/src/main/codelab/solutions/Exercise2.java index 5a1c1e8cc..4525a6f60 100644 --- a/codelab/src/main/codelab/solutions/Exercise2.java +++ b/codelab/src/main/codelab/solutions/Exercise2.java @@ -66,7 +66,7 @@ CelAbstractSyntaxTree compile(String expression, String variableName, CelType va * @throws IllegalArgumentException If the compiled expression in AST fails to evaluate. */ Object eval(CelAbstractSyntaxTree ast, Map parameterValues) { - CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build(); + CelRuntime celRuntime = CelRuntimeFactory.plannerRuntimeBuilder().build(); try { CelRuntime.Program program = celRuntime.createProgram(ast); diff --git a/codelab/src/main/codelab/solutions/Exercise3.java b/codelab/src/main/codelab/solutions/Exercise3.java index 590dfd0df..80c9a8beb 100644 --- a/codelab/src/main/codelab/solutions/Exercise3.java +++ b/codelab/src/main/codelab/solutions/Exercise3.java @@ -27,8 +27,7 @@ final class Exercise3 { private static final CelCompiler CEL_COMPILER = CelCompilerFactory.standardCelCompilerBuilder().setResultType(SimpleType.BOOL).build(); - private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder().build(); + private static final CelRuntime CEL_RUNTIME = CelRuntimeFactory.plannerRuntimeBuilder().build(); /** * Compiles the given expression and evaluates it. diff --git a/codelab/src/main/codelab/solutions/Exercise4.java b/codelab/src/main/codelab/solutions/Exercise4.java index b3cc82a24..129a9d7b5 100644 --- a/codelab/src/main/codelab/solutions/Exercise4.java +++ b/codelab/src/main/codelab/solutions/Exercise4.java @@ -81,7 +81,7 @@ CelAbstractSyntaxTree compile(String expression) { */ Object eval(CelAbstractSyntaxTree ast, Map parameterValues) { CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() + CelRuntimeFactory.plannerRuntimeBuilder() .addFunctionBindings( CelFunctionBinding.from( "map_contains_key_value", diff --git a/codelab/src/main/codelab/solutions/Exercise5.java b/codelab/src/main/codelab/solutions/Exercise5.java index e948adfed..8206efa33 100644 --- a/codelab/src/main/codelab/solutions/Exercise5.java +++ b/codelab/src/main/codelab/solutions/Exercise5.java @@ -60,7 +60,7 @@ CelAbstractSyntaxTree compile(String expression) { * @throws IllegalArgumentException If the compiled expression in AST fails to evaluate. */ Object eval(CelAbstractSyntaxTree ast, Map parameterValues) { - CelRuntime celRuntime = CelRuntimeFactory.standardCelRuntimeBuilder().build(); + CelRuntime celRuntime = CelRuntimeFactory.plannerRuntimeBuilder().build(); try { CelRuntime.Program program = celRuntime.createProgram(ast); diff --git a/codelab/src/main/codelab/solutions/Exercise6.java b/codelab/src/main/codelab/solutions/Exercise6.java index 9b6c59949..dba84291b 100644 --- a/codelab/src/main/codelab/solutions/Exercise6.java +++ b/codelab/src/main/codelab/solutions/Exercise6.java @@ -62,9 +62,7 @@ CelAbstractSyntaxTree compile(String expression) { /** Evaluates the compiled AST with the user provided parameter values. */ Object eval(CelAbstractSyntaxTree ast, Map parameterValues) { CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() - .addMessageTypes(Request.getDescriptor()) - .build(); + CelRuntimeFactory.plannerRuntimeBuilder().addMessageTypes(Request.getDescriptor()).build(); try { CelRuntime.Program program = celRuntime.createProgram(ast); diff --git a/codelab/src/main/codelab/solutions/Exercise7.java b/codelab/src/main/codelab/solutions/Exercise7.java index e5be29171..e45002f71 100644 --- a/codelab/src/main/codelab/solutions/Exercise7.java +++ b/codelab/src/main/codelab/solutions/Exercise7.java @@ -60,9 +60,7 @@ CelAbstractSyntaxTree compile(String expression) { /** Evaluates the compiled AST with the user provided parameter values. */ Object eval(CelAbstractSyntaxTree ast, Map parameterValues) { CelRuntime celRuntime = - CelRuntimeFactory.standardCelRuntimeBuilder() - .addMessageTypes(Request.getDescriptor()) - .build(); + CelRuntimeFactory.plannerRuntimeBuilder().addMessageTypes(Request.getDescriptor()).build(); try { CelRuntime.Program program = celRuntime.createProgram(ast); diff --git a/codelab/src/main/codelab/solutions/Exercise8.java b/codelab/src/main/codelab/solutions/Exercise8.java index 161089354..f23bb7aa8 100644 --- a/codelab/src/main/codelab/solutions/Exercise8.java +++ b/codelab/src/main/codelab/solutions/Exercise8.java @@ -27,7 +27,6 @@ import dev.cel.optimizer.CelOptimizerFactory; import dev.cel.optimizer.optimizers.ConstantFoldingOptimizer; import dev.cel.optimizer.optimizers.SubexpressionOptimizer; -import dev.cel.optimizer.optimizers.SubexpressionOptimizer.SubexpressionOptimizerOptions; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelRuntime; import dev.cel.runtime.CelRuntimeFactory; @@ -52,7 +51,7 @@ final class Exercise8 { .addMessageTypes(AttributeContext.Request.getDescriptor()) .build(); private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder() + CelRuntimeFactory.plannerRuntimeBuilder() .addMessageTypes(AttributeContext.Request.getDescriptor()) .build(); @@ -69,9 +68,7 @@ final class Exercise8 { private static final CelOptimizer CEL_OPTIMIZER = CelOptimizerFactory.standardCelOptimizerBuilder(CEL_COMPILER, CEL_RUNTIME) .addAstOptimizers( - ConstantFoldingOptimizer.getInstance(), - SubexpressionOptimizer.newInstance( - SubexpressionOptimizerOptions.newBuilder().enableCelBlock(true).build())) + ConstantFoldingOptimizer.getInstance(), SubexpressionOptimizer.getInstance()) .build(); /** diff --git a/codelab/src/main/codelab/solutions/Exercise9.java b/codelab/src/main/codelab/solutions/Exercise9.java index 2b45c3539..7ea1c1a52 100644 --- a/codelab/src/main/codelab/solutions/Exercise9.java +++ b/codelab/src/main/codelab/solutions/Exercise9.java @@ -62,7 +62,7 @@ final class Exercise9 { .addMessageTypes(AttributeContext.Request.getDescriptor()) .build(); private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder() + CelRuntimeFactory.plannerRuntimeBuilder() .addMessageTypes(AttributeContext.Request.getDescriptor()) .build(); private static final CelValidator CEL_VALIDATOR = From b617a14ccdf869b6b95d72dd923568e666fa2a5b Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 1 Jul 2026 14:40:39 -0700 Subject: [PATCH 115/204] Prepare for planner migration by shifting standard CEL builders to proxy the legacy runtime PiperOrigin-RevId: 941302789 --- bundle/src/main/java/dev/cel/bundle/BUILD.bazel | 1 + .../main/java/dev/cel/bundle/CelFactory.java | 16 ++++++++++++++++ .../src/main/java/dev/cel/runtime/BUILD.bazel | 1 + .../java/dev/cel/runtime/CelRuntimeFactory.java | 17 +++++++++++++++++ 4 files changed, 35 insertions(+) diff --git a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel index 716442849..1c11bb34e 100644 --- a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel +++ b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel @@ -54,6 +54,7 @@ java_library( "//parser", "//runtime", "//runtime:runtime_planner_impl", + "@maven//:com_google_errorprone_error_prone_annotations", ], ) diff --git a/bundle/src/main/java/dev/cel/bundle/CelFactory.java b/bundle/src/main/java/dev/cel/bundle/CelFactory.java index ac589cfe6..79acccc93 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelFactory.java +++ b/bundle/src/main/java/dev/cel/bundle/CelFactory.java @@ -14,6 +14,7 @@ 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; @@ -34,8 +35,23 @@ 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()}). */ + @InlineMe(replacement = "CelFactory.legacyCelBuilder()", imports = "dev.cel.bundle.CelFactory") public static CelBuilder standardCelBuilder() { + return legacyCelBuilder(); + } + + /** + * 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()}). + */ + public static CelBuilder legacyCelBuilder() { return CelImpl.newBuilder( CelCompilerImpl.newBuilder( CelParserImpl.newBuilder(), CelCheckerLegacyImpl.newBuilder()), diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 17160e346..933758c04 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -922,6 +922,7 @@ java_library( ":runtime_legacy_impl", ":runtime_planner_impl", "//common:options", + "@maven//:com_google_errorprone_error_prone_annotations", ], ) diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeFactory.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeFactory.java index 6615b59e0..8fa40d716 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeFactory.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeFactory.java @@ -14,6 +14,7 @@ package dev.cel.runtime; +import com.google.errorprone.annotations.InlineMe; import dev.cel.common.CelOptions; /** Helper class to construct new {@code CelRuntime} instances. */ @@ -24,8 +25,24 @@ 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()}). */ + @InlineMe( + replacement = "CelRuntimeFactory.legacyCelRuntimeBuilder()", + imports = "dev.cel.runtime.CelRuntimeFactory") public static CelRuntimeBuilder standardCelRuntimeBuilder() { + return legacyCelRuntimeBuilder(); + } + + /** + * 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()}). + */ + public static CelRuntimeBuilder legacyCelRuntimeBuilder() { return CelRuntimeLegacyImpl.newBuilder() .setOptions(CelOptions.current().build()) // CEL-Internal-2 From d7f3b3c349a5e9d8dae4c245fde5c75126fda60a Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 1 Jul 2026 19:05:25 -0700 Subject: [PATCH 116/204] Internal Changes PiperOrigin-RevId: 941405721 --- .../CelComprehensionsExtensions.java | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java index 14968099c..70402dd03 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java @@ -52,7 +52,8 @@ public final class CelComprehensionsExtensions private static final TypeParamType TYPE_PARAM_V = TypeParamType.create("V"); private static final MapType MAP_KV_TYPE = MapType.create(TYPE_PARAM_K, TYPE_PARAM_V); - enum Function { + /** Enumeration of functions for Comprehensions extension. */ + public enum Function { MAP_INSERT( CelFunctionDecl.newFunctionDeclaration( MAP_INSERT_FUNCTION, @@ -72,6 +73,10 @@ enum Function { private final CelFunctionDecl functionDecl; + public CelFunctionDecl functionDecl() { + return functionDecl; + } + String getFunction() { return functionDecl.name(); } @@ -81,20 +86,25 @@ String getFunction() { } } - private static final CelExtensionLibrary LIBRARY = - new CelExtensionLibrary() { - private final CelComprehensionsExtensions version0 = new CelComprehensionsExtensions(); + private static final class Library implements CelExtensionLibrary { + private final CelComprehensionsExtensions version0; - @Override - public String name() { - return "comprehensions"; - } + Library() { + version0 = new CelComprehensionsExtensions(); + } - @Override - public ImmutableSet versions() { - return ImmutableSet.of(version0); - } - }; + @Override + public String name() { + return "comprehensions"; + } + + @Override + public ImmutableSet versions() { + return ImmutableSet.of(version0); + } + } + + private static final Library LIBRARY = new Library(); static CelExtensionLibrary library() { return LIBRARY; @@ -103,7 +113,7 @@ static CelExtensionLibrary library() { private final ImmutableSet functions; CelComprehensionsExtensions() { - this.functions = ImmutableSet.copyOf(Function.values()); + this.functions = ImmutableSet.of(Function.MAP_INSERT); } @Override @@ -175,10 +185,10 @@ public void setParserOptions(CelParserBuilder parserBuilder) { private static Map mapInsertMap( Map targetMap, Map mapToMerge, RuntimeEquality equality) { for (Object key : mapToMerge.keySet()) { - if (equality.findInMap(targetMap, key).isPresent()) { - throw new IllegalArgumentException( - String.format("insert failed: key '%s' already exists", key)); - } + checkArgument( + !equality.findInMap(targetMap, key).isPresent(), + "insert failed: key '%s' already exists", + key); } if (targetMap instanceof MutableMapValue) { @@ -198,10 +208,10 @@ private static Map mapInsertKeyValue(Object[] args, RuntimeEqual Object key = args[1]; Object value = args[2]; - if (equality.findInMap(mapArg, key).isPresent()) { - throw new IllegalArgumentException( - String.format("insert failed: key '%s' already exists", key)); - } + checkArgument( + !equality.findInMap(mapArg, key).isPresent(), + "insert failed: key '%s' already exists", + key); if (mapArg instanceof MutableMapValue) { MutableMapValue mutableMap = (MutableMapValue) mapArg; From 028e171b7b7e5c2f652805910efe0ce26d2bd065 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 7 Jul 2026 11:24:59 -0700 Subject: [PATCH 117/204] Internal Changes PiperOrigin-RevId: 944001074 --- .../java/dev/cel/checker/CelStandardDeclarations.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java b/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java index 53615604f..0efcd4c65 100644 --- a/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java +++ b/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java @@ -15,9 +15,11 @@ package dev.cel.checker; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static java.util.Arrays.stream; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.Immutable; @@ -599,6 +601,13 @@ public enum Size implements StandardOverload { CelOverloadDecl.newMemberOverload("map_size", "map size", SimpleType.INT, MAP_OF_AB)), ; + private static final ImmutableMap ID_TO_ENUM = + stream(values()).collect(toImmutableMap(e -> e.celOverloadDecl().overloadId(), e -> e)); + + public static Optional fromOverloadId(String overloadId) { + return Optional.ofNullable(ID_TO_ENUM.get(overloadId)); + } + private final CelOverloadDecl celOverloadDecl; Size(CelOverloadDecl overloadDecl) { From 313a3cdb703daf080215a0fd700815691c9c6d44 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Thu, 9 Jul 2026 18:58:35 +0000 Subject: [PATCH 118/204] Update workflow.yml --- .bazelrc | 3 +++ .github/workflows/workflow.yml | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.bazelrc b/.bazelrc index 8724ec1da..f6e2f39c0 100644 --- a/.bazelrc +++ b/.bazelrc @@ -18,3 +18,6 @@ common --javacopt=-Xlint:-options # Remove flag once https://github.com/google/cel-spec/issues/508 and rules_jvm_external is fixed. common --incompatible_autoload_externally=proto_library,cc_proto_library,java_proto_library,java_test +# Limit repository cache size by not caching extracted repository contents +build --repo_contents_cache= + diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 94effd33b..8a34af31c 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -30,7 +30,7 @@ jobs: # Avoid downloading Bazel every time. bazelisk-cache: true # Store build cache per workflow. - disk-cache: ${{ github.workflow }} + disk-cache: ${{ github.workflow }}-${{ github.job }} # Share repository cache between workflows. repository-cache: true # Prevent PRs from polluting cache @@ -60,7 +60,7 @@ jobs: # Avoid downloading Bazel every time. bazelisk-cache: true # Store build cache per workflow. - disk-cache: ${{ github.workflow }} + disk-cache: ${{ github.workflow }}-${{ github.job }} # Share repository cache between workflows. repository-cache: true # Prevent PRs from polluting cache @@ -95,7 +95,7 @@ jobs: # Avoid downloading Bazel every time. bazelisk-cache: true # Store build cache per workflow. - disk-cache: ${{ github.workflow }} + disk-cache: ${{ github.workflow }}-${{ github.job }} # Share repository cache between workflows. repository-cache: true # Never write to the cache, strictly read-only From fe24e6e3b7aa6d6e193069ba7be43e8788e2ba20 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 13 Jul 2026 13:42:21 -0700 Subject: [PATCH 119/204] Migrate Lite Runtime to Program Planner PiperOrigin-RevId: 947219284 --- .../test/java/dev/cel/bundle/CelImplTest.java | 54 +- common/BUILD.bazel | 5 + common/ast/BUILD.bazel | 6 + common/navigation/BUILD.bazel | 6 + .../src/main/java/dev/cel/common/BUILD.bazel | 12 + .../main/java/dev/cel/common/ast/BUILD.bazel | 14 + .../dev/cel/common/navigation/BUILD.bazel | 39 + .../java/dev/cel/common/values/BUILD.bazel | 2 + .../values/BaseProtoMessageValueProvider.java | 5 + runtime/BUILD.bazel | 23 + runtime/planner/BUILD.bazel | 6 + .../src/main/java/dev/cel/runtime/BUILD.bazel | 119 +-- .../cel/runtime/CelLiteRuntimeBuilder.java | 24 + .../dev/cel/runtime/CelRuntimeLegacyImpl.java | 26 +- .../runtime/CelValueRuntimeTypeProvider.java | 137 --- .../java/dev/cel/runtime/LiteProgramImpl.java | 64 -- .../java/dev/cel/runtime/LiteRuntimeImpl.java | 112 ++- .../java/dev/cel/runtime/planner/BUILD.bazel | 791 ++++++++++++++---- .../cel/runtime/planner/MissingAttribute.java | 2 + .../planner/PresenceTestQualifier.java | 2 + .../cel/runtime/planner/StringQualifier.java | 2 + .../AbstractPlannerInterpreterTest.java | 280 +++++++ .../src/test/java/dev/cel/runtime/BUILD.bazel | 32 +- .../cel/runtime/CelLiteInterpreterTest.java | 6 +- .../runtime/CelLiteRuntimeAndroidTest.java | 29 +- .../dev/cel/runtime/CelLiteRuntimeTest.java | 34 +- .../cel/runtime/CelRuntimeLegacyImplTest.java | 7 +- .../cel/runtime/PlannerInterpreterTest.java | 260 +----- testing.bzl | 5 +- 29 files changed, 1259 insertions(+), 845 deletions(-) delete mode 100644 runtime/src/main/java/dev/cel/runtime/CelValueRuntimeTypeProvider.java delete mode 100644 runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java create mode 100644 runtime/src/test/java/dev/cel/runtime/AbstractPlannerInterpreterTest.java diff --git a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java index a3ad60d40..fbacb242a 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java @@ -59,7 +59,6 @@ import com.google.rpc.context.AttributeContext; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; -import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.checker.CelCheckerLegacyImpl; import dev.cel.checker.DescriptorTypeProvider; import dev.cel.checker.ProtoTypeMask; @@ -232,9 +231,7 @@ public void check() throws Exception { } @Test - @TestParameters("{useProtoResultType: false}") - @TestParameters("{useProtoResultType: true}") - public void compile(boolean useProtoResultType) throws Exception { + public void compile(@TestParameter boolean useProtoResultType) throws Exception { CelBuilder celBuilder = standardCelBuilderWithMacros(); if (useProtoResultType) { celBuilder.setProtoResultType(CelProtoTypes.BOOL); @@ -246,9 +243,7 @@ public void compile(boolean useProtoResultType) throws Exception { } @Test - @TestParameters("{useProtoResultType: false}") - @TestParameters("{useProtoResultType: true}") - public void compile_resultTypeCheckFailure(boolean useProtoResultType) { + public void compile_resultTypeCheckFailure(@TestParameter boolean useProtoResultType) { CelBuilder celBuilder = standardCelBuilderWithMacros(); if (useProtoResultType) { celBuilder.setProtoResultType(CelProtoTypes.STRING); @@ -563,23 +558,6 @@ public void program_withVars() throws Exception { assertThat(program.eval(ImmutableMap.of("variable", "hello"))).isEqualTo(true); } - @Test - public void program_withCelValue() throws Exception { - Cel cel = - standardCelBuilderWithMacros() - .setOptions(CelOptions.current().enableCelValue(true).build()) - .addDeclarations( - Decl.newBuilder() - .setName("variable") - .setIdent(IdentDecl.newBuilder().setType(CelProtoTypes.STRING)) - .build()) - .setResultType(SimpleType.BOOL) - .build(); - - CelRuntime.Program program = cel.createProgram(cel.compile("variable == 'hello'").getAst()); - - assertThat(program.eval(ImmutableMap.of("variable", "hello"))).isEqualTo(true); - } @Test public void program_withProtoVars() throws Exception { @@ -1006,9 +984,8 @@ public void program_protoActivation() throws Exception { } @Test - @TestParameters("{resolveTypeDependencies: false}") - @TestParameters("{resolveTypeDependencies: true}") - public void program_enumTypeDirectResolution(boolean resolveTypeDependencies) throws Exception { + public void program_enumTypeDirectResolution(@TestParameter boolean resolveTypeDependencies) + throws Exception { Cel cel = standardCelBuilderWithMacros() .addFileTypes(StandaloneGlobalEnum.getDescriptor().getFile()) @@ -1029,9 +1006,7 @@ public void program_enumTypeDirectResolution(boolean resolveTypeDependencies) th } @Test - @TestParameters("{resolveTypeDependencies: false}") - @TestParameters("{resolveTypeDependencies: true}") - public void program_enumTypeReferenceResolution(boolean resolveTypeDependencies) + public void program_enumTypeReferenceResolution(@TestParameter boolean resolveTypeDependencies) throws Exception { Cel cel = standardCelBuilderWithMacros() @@ -1429,25 +1404,6 @@ public void programAdvanceEvaluation_nestedSelect() throws Exception { .isEqualTo(CelUnknownSet.create(CelAttribute.fromQualifiedIdentifier("com.google.a"))); } - @Test - public void programAdvanceEvaluation_nestedSelect_withCelValue() throws Exception { - Cel cel = - standardCelBuilderWithMacros() - .setOptions( - CelOptions.current().enableUnknownTracking(true).enableCelValue(true).build()) - .addVar("com", MapType.create(SimpleType.STRING, SimpleType.DYN)) - .addFunctionBindings() - .setResultType(SimpleType.BOOL) - .build(); - CelRuntime.Program program = cel.createProgram(cel.compile("com.google.a || false").getAst()); - - assertThat( - program.advanceEvaluation( - UnknownContext.create( - fromMap(ImmutableMap.of()), - ImmutableList.of(CelAttributePattern.fromQualifiedIdentifier("com.google.a"))))) - .isEqualTo(CelUnknownSet.create(CelAttribute.fromQualifiedIdentifier("com.google.a"))); - } @Test public void programAdvanceEvaluation_argumentMergeErrorPriority() throws Exception { diff --git a/common/BUILD.bazel b/common/BUILD.bazel index 4e0d7485c..b67069de7 100644 --- a/common/BUILD.bazel +++ b/common/BUILD.bazel @@ -22,6 +22,11 @@ java_library( exports = ["//common/src/main/java/dev/cel/common:container"], ) +cel_android_library( + name = "container_android", + exports = ["//common/src/main/java/dev/cel/common:container_android"], +) + java_library( name = "proto_ast", exports = ["//common/src/main/java/dev/cel/common:proto_ast"], diff --git a/common/ast/BUILD.bazel b/common/ast/BUILD.bazel index 302abfc79..1361ad76b 100644 --- a/common/ast/BUILD.bazel +++ b/common/ast/BUILD.bazel @@ -17,6 +17,12 @@ java_library( exports = ["//common/src/main/java/dev/cel/common/ast:cel_block"], ) +cel_android_library( + name = "cel_block_android", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/ast:cel_block_android"], +) + cel_android_library( name = "ast_android", exports = ["//common/src/main/java/dev/cel/common/ast:ast_android"], diff --git a/common/navigation/BUILD.bazel b/common/navigation/BUILD.bazel index 1dba25b8e..0c03596f9 100644 --- a/common/navigation/BUILD.bazel +++ b/common/navigation/BUILD.bazel @@ -1,4 +1,5 @@ load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") package( default_applicable_licenses = ["//:license"], @@ -15,6 +16,11 @@ java_library( exports = ["//common/src/main/java/dev/cel/common/navigation"], ) +cel_android_library( + name = "navigation_android", + exports = ["//common/src/main/java/dev/cel/common/navigation:navigation_android"], +) + java_library( name = "mutable_navigation", exports = ["//common/src/main/java/dev/cel/common/navigation:mutable_navigation"], diff --git a/common/src/main/java/dev/cel/common/BUILD.bazel b/common/src/main/java/dev/cel/common/BUILD.bazel index 38548744c..11b762220 100644 --- a/common/src/main/java/dev/cel/common/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/BUILD.bazel @@ -365,6 +365,18 @@ java_library( ], ) +cel_android_library( + name = "container_android", + srcs = ["CelContainer.java"], + tags = [ + ], + deps = [ + "//:auto_value", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "operator", srcs = ["Operator.java"], diff --git a/common/src/main/java/dev/cel/common/ast/BUILD.bazel b/common/src/main/java/dev/cel/common/ast/BUILD.bazel index 46c235d1f..c72857080 100644 --- a/common/src/main/java/dev/cel/common/ast/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/ast/BUILD.bazel @@ -71,6 +71,20 @@ java_library( ], ) +cel_android_library( + name = "cel_block_android", + srcs = ["CelBlock.java"], + tags = [ + ], + deps = [ + ":ast_android", + "//common:cel_ast_android", + "//common/annotations", + "//common/navigation:navigation_android", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "expr_converter", srcs = EXPR_CONVERTER_SOURCES, diff --git a/common/src/main/java/dev/cel/common/navigation/BUILD.bazel b/common/src/main/java/dev/cel/common/navigation/BUILD.bazel index b43f3c289..3c2eaad62 100644 --- a/common/src/main/java/dev/cel/common/navigation/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/navigation/BUILD.bazel @@ -1,4 +1,5 @@ load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") package( default_applicable_licenses = [ @@ -28,6 +29,25 @@ java_library( ], ) +cel_android_library( + name = "common_android", + srcs = [ + "BaseNavigableExpr.java", + "CelNavigableExprVisitor.java", + "ExprPropertyCalculator.java", + "TraversalOrder.java", + ], + tags = [ + ], + deps = [ + "//:auto_value", + "//common/ast:ast_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "navigation", srcs = [ @@ -47,6 +67,25 @@ java_library( ], ) +cel_android_library( + name = "navigation_android", + srcs = [ + "CelNavigableAst.java", + "CelNavigableExpr.java", + ], + tags = [ + ], + deps = [ + ":common_android", + "//:auto_value", + "//common:cel_ast_android", + "//common/ast:ast_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "mutable_navigation", srcs = [ diff --git a/common/src/main/java/dev/cel/common/values/BUILD.bazel b/common/src/main/java/dev/cel/common/values/BUILD.bazel index 5ccc498fd..433dcd477 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -408,6 +408,7 @@ java_library( ], deps = [ "//common/annotations", + "//common/values", "//common/values:base_proto_cel_value_converter", "//common/values:cel_value_provider", "@maven//:com_google_errorprone_error_prone_annotations", @@ -423,6 +424,7 @@ cel_android_library( "//common/annotations", "//common/values:base_proto_cel_value_converter_android", "//common/values:cel_value_provider_android", + "//common/values:values_android", "@maven//:com_google_errorprone_error_prone_annotations", ], ) diff --git a/common/src/main/java/dev/cel/common/values/BaseProtoMessageValueProvider.java b/common/src/main/java/dev/cel/common/values/BaseProtoMessageValueProvider.java index f42a16179..51bb0a497 100644 --- a/common/src/main/java/dev/cel/common/values/BaseProtoMessageValueProvider.java +++ b/common/src/main/java/dev/cel/common/values/BaseProtoMessageValueProvider.java @@ -28,4 +28,9 @@ public abstract class BaseProtoMessageValueProvider implements CelValueProvider { public abstract BaseProtoCelValueConverter protoCelValueConverter(); + + @Override + public CelValueConverter celValueConverter() { + return protoCelValueConverter(); + } } diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index d1cb99b64..c87fadca9 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -226,11 +226,21 @@ java_library( exports = ["//runtime/src/main/java/dev/cel/runtime:interpreter_util"], ) +cel_android_library( + name = "interpreter_util_android", + exports = ["//runtime/src/main/java/dev/cel/runtime:interpreter_util_android"], +) + java_library( name = "evaluation_listener", exports = ["//runtime/src/main/java/dev/cel/runtime:evaluation_listener"], ) +cel_android_library( + name = "evaluation_listener_android", + exports = ["//runtime/src/main/java/dev/cel/runtime:evaluation_listener_android"], +) + cel_android_library( name = "standard_functions_android", exports = [ @@ -352,7 +362,20 @@ java_library( ], ) +cel_android_library( + name = "accumulated_unknowns_android", + visibility = ["//:internal"], + exports = [ + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", + ], +) + java_library( name = "partial_vars", exports = ["//runtime/src/main/java/dev/cel/runtime:partial_vars"], ) + +cel_android_library( + name = "partial_vars_android", + exports = ["//runtime/src/main/java/dev/cel/runtime:partial_vars_android"], +) diff --git a/runtime/planner/BUILD.bazel b/runtime/planner/BUILD.bazel index 9b5dbee6a..860d413a0 100644 --- a/runtime/planner/BUILD.bazel +++ b/runtime/planner/BUILD.bazel @@ -1,4 +1,5 @@ load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") package( default_applicable_licenses = ["//:license"], @@ -10,6 +11,11 @@ java_library( exports = ["//runtime/src/main/java/dev/cel/runtime/planner:program_planner"], ) +cel_android_library( + name = "program_planner_android", + exports = ["//runtime/src/main/java/dev/cel/runtime/planner:program_planner_android"], +) + java_library( name = "planned_program", visibility = ["//:internal"], diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 933758c04..489bb64d8 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -44,11 +44,6 @@ LITE_RUNTIME_IMPL_SOURCES = [ "LiteRuntimeImpl.java", ] -# keep sorted -LITE_PROGRAM_IMPL_SOURCES = [ - "LiteProgramImpl.java", -] - # keep sorted FUNCTION_BINDING_SOURCES = [ "CelFunctionBinding.java", @@ -416,7 +411,6 @@ java_library( "//common/internal:dynamic_proto", "//common/internal:proto_equality", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", ], ) @@ -508,7 +502,6 @@ java_library( ":function_binding", ":function_resolver", ":resolved_overload", - "//:auto_value", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], @@ -525,7 +518,6 @@ cel_android_library( ":function_binding_android", ":function_resolver_android", ":resolved_overload_android", - "//:auto_value", "@maven//:com_google_errorprone_error_prone_annotations", "@maven_android//:com_google_guava_guava", ], @@ -732,7 +724,6 @@ cel_android_library( "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven_android//:com_google_guava_guava", - "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) @@ -876,7 +867,6 @@ java_library( tags = [ ], deps = [ - ":cel_value_runtime_type_provider", ":descriptor_message_provider", ":descriptor_type_resolver", ":dispatcher", @@ -901,7 +891,6 @@ java_library( "//common/types:type_providers", "//common/values", "//common/values:cel_value_provider", - "//common/values:proto_message_value_provider", "//runtime/standard:int", "//runtime/standard:timestamp", "@maven//:com_google_code_findbugs_annotations", @@ -969,15 +958,15 @@ java_library( ":evaluation_exception", ":function_binding", ":program", - "//:auto_value", "//common:cel_ast", + "//common:container", "//common:options", "//common/annotations", + "//common/types:type_providers", "//common/values:cel_value_provider", "//runtime/standard:standard_function", "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", ], ) @@ -987,55 +976,25 @@ java_library( tags = [ ], deps = [ - ":cel_value_runtime_type_provider", ":dispatcher", ":function_binding", - ":interpreter", - ":lite_program_impl", ":lite_runtime", ":program", ":runtime_equality", ":runtime_helpers", - ":type_resolver", - "//:auto_value", "//common:cel_ast", + "//common:container", "//common:options", + "//common/types:default_type_provider", + "//common/types:type_providers", + "//common/values", "//common/values:cel_value_provider", + "//runtime:evaluation_exception", + "//runtime/planner:program_planner", "//runtime/standard:standard_function", "@maven//:com_google_code_findbugs_annotations", - "@maven//:com_google_guava_guava", - ], -) - -java_library( - name = "lite_program_impl", - srcs = LITE_PROGRAM_IMPL_SOURCES, - deps = [ - ":activation", - ":evaluation_exception", - ":function_resolver", - ":interpretable", - ":partial_vars", - ":program", - ":variable_resolver", - "//:auto_value", - "@maven//:com_google_errorprone_error_prone_annotations", - ], -) - -cel_android_library( - name = "lite_program_impl_android", - srcs = LITE_PROGRAM_IMPL_SOURCES, - deps = [ - ":activation_android", - ":evaluation_exception", - ":function_resolver_android", - ":interpretable_android", - ":partial_vars_android", - ":program_android", - ":variable_resolver", - "//:auto_value", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", ], ) @@ -1045,20 +1004,21 @@ cel_android_library( tags = [ ], deps = [ - ":cel_value_runtime_type_provider_android", ":dispatcher_android", ":function_binding_android", - ":interpreter_android", - ":lite_program_impl_android", ":lite_runtime_android", ":program_android", ":runtime_equality_android", ":runtime_helpers_android", - ":type_resolver_android", - "//:auto_value", "//common:cel_ast_android", + "//common:container_android", "//common:options", + "//common/types:default_type_provider_android", + "//common/types:type_providers_android", "//common/values:cel_value_provider_android", + "//common/values:values_android", + "//runtime:evaluation_exception", + "//runtime/planner:program_planner_android", "//runtime/standard:standard_function_android", "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", @@ -1151,46 +1111,6 @@ cel_android_library( ], ) -java_library( - name = "cel_value_runtime_type_provider", - srcs = ["CelValueRuntimeTypeProvider.java"], - deps = [ - ":runtime_type_provider", - ":unknown_attributes", - "//common/annotations", - "//common/exceptions:attribute_not_found", - "//common/values", - "//common/values:base_proto_cel_value_converter", - "//common/values:base_proto_message_value_provider", - "//common/values:cel_value", - "//common/values:cel_value_provider", - "//common/values:combined_cel_value_provider", - "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", - "@maven//:com_google_protobuf_protobuf_java", - ], -) - -cel_android_library( - name = "cel_value_runtime_type_provider_android", - srcs = ["CelValueRuntimeTypeProvider.java"], - deps = [ - ":runtime_type_provider_android", - ":unknown_attributes_android", - "//common/annotations", - "//common/exceptions:attribute_not_found", - "//common/values:base_proto_cel_value_converter_android", - "//common/values:base_proto_message_value_provider_android", - "//common/values:cel_value_android", - "//common/values:cel_value_provider_android", - "//common/values:combined_cel_value_provider_android", - "//common/values:values_android", - "@maven//:com_google_errorprone_error_prone_annotations", - "@maven_android//:com_google_guava_guava", - "@maven_android//:com_google_protobuf_protobuf_javalite", - ], -) - java_library( name = "interpreter_util", srcs = ["InterpreterUtil.java"], @@ -1210,7 +1130,6 @@ java_library( cel_android_library( name = "interpreter_util_android", srcs = ["InterpreterUtil.java"], - visibility = ["//visibility:private"], deps = [ ":accumulated_unknowns_android", ":evaluation_exception", @@ -1237,7 +1156,6 @@ java_library( cel_android_library( name = "evaluation_listener_android", srcs = ["CelEvaluationListener.java"], - visibility = ["//visibility:private"], deps = [ "//common/ast:ast_android", "@maven//:com_google_code_findbugs_annotations", @@ -1254,15 +1172,15 @@ cel_android_library( ":evaluation_exception", ":function_binding_android", ":program_android", - "//:auto_value", "//common:cel_ast_android", + "//common:container_android", "//common:options", "//common/annotations", + "//common/types:type_providers_android", "//common/values:cel_value_provider_android", "//runtime/standard:standard_function_android", "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven_android//:com_google_guava_guava", ], ) @@ -1291,7 +1209,6 @@ java_library( cel_android_library( name = "accumulated_unknowns_android", srcs = ["AccumulatedUnknowns.java"], - visibility = ["//visibility:private"], deps = [ ":unknown_attributes_android", "//common/annotations", @@ -1343,7 +1260,6 @@ java_library( ":variable_resolver", "//:auto_value", "//runtime:unknown_attributes", - "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], ) @@ -1357,7 +1273,6 @@ cel_android_library( ":variable_resolver", "//:auto_value", "//runtime:unknown_attributes_android", - "@maven//:com_google_errorprone_error_prone_annotations", "@maven_android//:com_google_guava_guava", ], ) diff --git a/runtime/src/main/java/dev/cel/runtime/CelLiteRuntimeBuilder.java b/runtime/src/main/java/dev/cel/runtime/CelLiteRuntimeBuilder.java index 48b51274d..f2f27da27 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelLiteRuntimeBuilder.java +++ b/runtime/src/main/java/dev/cel/runtime/CelLiteRuntimeBuilder.java @@ -16,7 +16,9 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CheckReturnValue; +import dev.cel.common.CelContainer; import dev.cel.common.CelOptions; +import dev.cel.common.types.CelTypeProvider; import dev.cel.common.values.CelValueProvider; import dev.cel.runtime.standard.CelStandardFunction; @@ -62,6 +64,28 @@ CelLiteRuntimeBuilder setStandardFunctions( @CanIgnoreReturnValue CelLiteRuntimeBuilder addLibraries(Iterable libraries); + /** + * Sets the {@link CelTypeProvider} for resolving CEL types during evaluation, such as a fully + * qualified type name to a struct or an enum value. + */ + @CanIgnoreReturnValue + CelLiteRuntimeBuilder setTypeProvider(CelTypeProvider celTypeProvider); + + /** + * Set the {@link CelContainer} to use as the namespace for resolving CEL expression variables and + * functions. + */ + @CanIgnoreReturnValue + CelLiteRuntimeBuilder setContainer(CelContainer container); + + /** Adds bindings for functions that are allowed to be late-bound (resolved at execution time). */ + @CanIgnoreReturnValue + CelLiteRuntimeBuilder addLateBoundFunctions(String... lateBoundFunctionNames); + + /** Adds bindings for functions that are allowed to be late-bound (resolved at execution time). */ + @CanIgnoreReturnValue + CelLiteRuntimeBuilder addLateBoundFunctions(Iterable lateBoundFunctionNames); + @CheckReturnValue CelLiteRuntime build(); } diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java index b9ce022cf..c5e06d013 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java @@ -44,7 +44,6 @@ import dev.cel.common.types.CelTypes; import dev.cel.common.values.CelValueConverter; import dev.cel.common.values.CelValueProvider; -import dev.cel.common.values.ProtoMessageValueProvider; import dev.cel.runtime.standard.IntFunction.IntOverload; import dev.cel.runtime.standard.TimestampFunction.TimestampOverload; import java.util.Arrays; @@ -78,7 +77,6 @@ public final class CelRuntimeLegacyImpl implements CelRuntime { private final Function customTypeFactory; private final CelStandardFunctions overriddenStandardFunctions; - private final CelValueProvider celValueProvider; private final ImmutableSet fileDescriptors; // This does not affect the evaluation behavior in any manner. @@ -113,9 +111,6 @@ public CelRuntimeBuilder toRuntimeBuilder() { builder.setStandardFunctions(overriddenStandardFunctions); } - if (celValueProvider != null) { - builder.setValueProvider(celValueProvider); - } return builder; } @@ -136,7 +131,6 @@ public static final class Builder implements CelRuntimeBuilder { @VisibleForTesting final ImmutableSet.Builder celRuntimeLibraries; @VisibleForTesting Function customTypeFactory; - @VisibleForTesting CelValueProvider celValueProvider; @VisibleForTesting CelStandardFunctions overriddenStandardFunctions; private CelOptions options; @@ -209,8 +203,8 @@ public CelRuntimeBuilder setTypeProvider(CelTypeProvider celTypeProvider) { @Override public CelRuntimeBuilder setValueProvider(CelValueProvider celValueProvider) { - this.celValueProvider = celValueProvider; - return this; + throw new UnsupportedOperationException( + "setValueProvider is not supported for legacy runtime"); } @Override @@ -335,19 +329,10 @@ public CelRuntimeLegacyImpl build() { RuntimeTypeProvider runtimeTypeProvider; if (options.enableCelValue()) { - CelValueProvider messageValueProvider = celValueProvider; - - if (messageValueProvider == null) { - messageValueProvider = ProtoMessageValueProvider.newInstance(options, dynamicProto); - } - - runtimeTypeProvider = CelValueRuntimeTypeProvider.newInstance(messageValueProvider); - celValueConverter = messageValueProvider.celValueConverter(); + throw new UnsupportedOperationException( + "enableCelValue is not supported for legacy runtime"); } else { runtimeTypeProvider = new DescriptorMessageProvider(runtimeTypeFactory, options); - if (celValueProvider != null) { - celValueConverter = celValueProvider.celValueConverter(); - } } DefaultInterpreter interpreter = @@ -364,7 +349,6 @@ public CelRuntimeLegacyImpl build() { extensionRegistry, customTypeFactory, overriddenStandardFunctions, - celValueProvider, fileDescriptors, runtimeLibraries, ImmutableList.copyOf(customFunctionBindings.values())); @@ -452,7 +436,6 @@ private CelRuntimeLegacyImpl( ExtensionRegistry extensionRegistry, @Nullable Function customTypeFactory, @Nullable CelStandardFunctions overriddenStandardFunctions, - @Nullable CelValueProvider celValueProvider, ImmutableSet fileDescriptors, ImmutableSet celRuntimeLibraries, ImmutableList celFunctionBindings) { @@ -462,7 +445,6 @@ private CelRuntimeLegacyImpl( this.extensionRegistry = extensionRegistry; this.customTypeFactory = customTypeFactory; this.overriddenStandardFunctions = overriddenStandardFunctions; - this.celValueProvider = celValueProvider; this.fileDescriptors = fileDescriptors; this.celRuntimeLibraries = celRuntimeLibraries; this.celFunctionBindings = celFunctionBindings; diff --git a/runtime/src/main/java/dev/cel/runtime/CelValueRuntimeTypeProvider.java b/runtime/src/main/java/dev/cel/runtime/CelValueRuntimeTypeProvider.java deleted file mode 100644 index 38365127c..000000000 --- a/runtime/src/main/java/dev/cel/runtime/CelValueRuntimeTypeProvider.java +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright 2023 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dev.cel.runtime; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.errorprone.annotations.Immutable; -import com.google.protobuf.MessageLite; -import dev.cel.common.annotations.Internal; -import dev.cel.common.exceptions.CelAttributeNotFoundException; -import dev.cel.common.values.BaseProtoCelValueConverter; -import dev.cel.common.values.BaseProtoMessageValueProvider; -import dev.cel.common.values.CelValueProvider; -import dev.cel.common.values.CombinedCelValueProvider; -import dev.cel.common.values.SelectableValue; -import java.util.Map; -import java.util.NoSuchElementException; - -/** Bridge between the old RuntimeTypeProvider and CelValueProvider APIs. */ -@Internal -@Immutable -final class CelValueRuntimeTypeProvider implements RuntimeTypeProvider { - - private final CelValueProvider valueProvider; - private final BaseProtoCelValueConverter protoCelValueConverter; - private static final BaseProtoCelValueConverter DEFAULT_CEL_VALUE_CONVERTER = - new BaseProtoCelValueConverter() {}; - - static CelValueRuntimeTypeProvider newInstance(CelValueProvider valueProvider) { - BaseProtoCelValueConverter converter = DEFAULT_CEL_VALUE_CONVERTER; - - // Find the underlying ProtoCelValueConverter. - // This is required because DefaultInterpreter works with a resolved protobuf messages directly - // in evaluation flow. - // A new runtime should not directly depend on protobuf, thus this will not be needed in the - // future. - if (valueProvider instanceof BaseProtoMessageValueProvider) { - converter = ((BaseProtoMessageValueProvider) valueProvider).protoCelValueConverter(); - } else if (valueProvider instanceof CombinedCelValueProvider) { - converter = - ((CombinedCelValueProvider) valueProvider) - .valueProviders().stream() - .filter(p -> p instanceof BaseProtoMessageValueProvider) - .map(p -> ((BaseProtoMessageValueProvider) p).protoCelValueConverter()) - .findFirst() - .orElse(DEFAULT_CEL_VALUE_CONVERTER); - } - - return new CelValueRuntimeTypeProvider(valueProvider, converter); - } - - @Override - public Object createMessage(String messageName, Map values) { - return protoCelValueConverter.maybeUnwrap( - valueProvider - .newValue(messageName, values) - .orElseThrow( - () -> - new NoSuchElementException( - String.format("cannot resolve '%s' as a message", messageName)))); - } - - @Override - public Object selectField(Object message, String fieldName) { - if (message instanceof Map) { - Map map = (Map) message; - if (map.containsKey(fieldName)) { - return map.get(fieldName); - } - - throw CelAttributeNotFoundException.forMissingMapKey(fieldName); - } - - SelectableValue selectableValue = getSelectableValueOrThrow(message, fieldName); - Object value = selectableValue.select(fieldName); - - return protoCelValueConverter.maybeUnwrap(value); - } - - @Override - public Object hasField(Object message, String fieldName) { - SelectableValue selectableValue = getSelectableValueOrThrow(message, fieldName); - - return selectableValue.find(fieldName).isPresent(); - } - - @SuppressWarnings("unchecked") - private SelectableValue getSelectableValueOrThrow(Object obj, String fieldName) { - Object convertedCelValue = protoCelValueConverter.toRuntimeValue(obj); - - if (!(convertedCelValue instanceof SelectableValue)) { - throwInvalidFieldSelection(fieldName); - } - - return (SelectableValue) convertedCelValue; - } - - @Override - public Object adapt(String messageName, Object message) { - if (message instanceof CelUnknownSet) { - return message; // CelUnknownSet is handled specially for iterative evaluation. No need to - // adapt to CelValue. - } - - if (message instanceof MessageLite.Builder) { - message = ((MessageLite.Builder) message).build(); - } - - if (message instanceof MessageLite) { - return protoCelValueConverter.maybeUnwrap(protoCelValueConverter.toRuntimeValue(message)); - } - - return message; - } - - private static void throwInvalidFieldSelection(String fieldName) { - throw CelAttributeNotFoundException.forFieldResolution(fieldName); - } - - private CelValueRuntimeTypeProvider( - CelValueProvider valueProvider, BaseProtoCelValueConverter protoCelValueConverter) { - this.valueProvider = checkNotNull(valueProvider); - this.protoCelValueConverter = checkNotNull(protoCelValueConverter); - } -} diff --git a/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java b/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java deleted file mode 100644 index af8c1a6d0..000000000 --- a/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2025 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dev.cel.runtime; - -import com.google.auto.value.AutoValue; -import com.google.errorprone.annotations.Immutable; -import java.util.Map; - -@Immutable -@AutoValue -abstract class LiteProgramImpl implements Program { - - abstract Interpretable interpretable(); - - @Override - public Object eval() throws CelEvaluationException { - return interpretable().eval(GlobalResolver.EMPTY); - } - - @Override - public Object eval(Map mapValue) throws CelEvaluationException { - return interpretable().eval(Activation.copyOf(mapValue)); - } - - @Override - public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctionResolver) - throws CelEvaluationException { - return interpretable().eval(Activation.copyOf(mapValue), lateBoundFunctionResolver); - } - - @Override - public Object eval(CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { - // TODO: Wire in program planner - throw new UnsupportedOperationException("To be implemented"); - } - - @Override - public Object eval(CelVariableResolver resolver) throws CelEvaluationException { - // TODO: Wire in program planner - throw new UnsupportedOperationException("To be implemented"); - } - - @Override - public Object eval(PartialVars partialVars) throws CelEvaluationException { - // TODO: Wire in program planner - throw new UnsupportedOperationException("To be implemented"); - } - - static Program plan(Interpretable interpretable) { - return new AutoValue_LiteProgramImpl(interpretable); - } -} diff --git a/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java index 8ce2d7733..6572621a6 100644 --- a/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java @@ -15,37 +15,43 @@ package dev.cel.runtime; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import javax.annotation.concurrent.ThreadSafe; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; import dev.cel.common.CelOptions; +import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.DefaultTypeProvider; +import dev.cel.common.values.CelValueConverter; import dev.cel.common.values.CelValueProvider; +import dev.cel.runtime.planner.ProgramPlanner; import dev.cel.runtime.standard.CelStandardFunction; import java.util.Arrays; import java.util.HashMap; +import java.util.Map; import java.util.Optional; @ThreadSafe final class LiteRuntimeImpl implements CelLiteRuntime { - private final Interpreter interpreter; + private final ProgramPlanner planner; private final CelOptions celOptions; private final ImmutableList customFunctionBindings; private final ImmutableSet celStandardFunctions; private final CelValueProvider celValueProvider; + private final CelTypeProvider celTypeProvider; + private final CelContainer celContainer; - // This does not affect the evaluation behavior in any manner. // CEL-Internal-4 private final ImmutableSet runtimeLibraries; @Override - public Program createProgram(CelAbstractSyntaxTree ast) { - checkState(ast.isChecked(), "programs must be created from checked expressions"); - return LiteProgramImpl.plan(interpreter.createInterpretable(ast)); + public Program createProgram(CelAbstractSyntaxTree ast) throws CelEvaluationException { + return planner.plan(ast); } @Override @@ -55,12 +61,17 @@ public CelLiteRuntimeBuilder toRuntimeBuilder() { .setOptions(celOptions) .setStandardFunctions(celStandardFunctions) .addFunctionBindings(customFunctionBindings) - .addLibraries(runtimeLibraries); + .addLibraries(runtimeLibraries) + .setContainer(celContainer); if (celValueProvider != null) { builder.setValueProvider(celValueProvider); } + if (celTypeProvider != null) { + builder.setTypeProvider(celTypeProvider); + } + return builder; } @@ -72,9 +83,18 @@ static final class Builder implements CelLiteRuntimeBuilder { @VisibleForTesting final ImmutableSet.Builder runtimeLibrariesBuilder; @VisibleForTesting final ImmutableSet.Builder standardFunctionBuilder; @VisibleForTesting CelValueProvider celValueProvider; + private CelContainer container; + @VisibleForTesting final ImmutableSet.Builder lateBoundFunctionNamesBuilder; + @VisibleForTesting CelTypeProvider celTypeProvider; @Override public CelLiteRuntimeBuilder setOptions(CelOptions celOptions) { + Preconditions.checkArgument( + celOptions.enableUnsignedLongs(), + "CelLiteRuntime requires CelOptions.enableUnsignedLongs(true)."); + Preconditions.checkArgument( + celOptions.unwrapWellKnownTypesOnFunctionDispatch(), + "CelLiteRuntime requires CelOptions.unwrapWellKnownTypesOnFunctionDispatch(true)."); this.celOptions = celOptions; return this; } @@ -119,12 +139,32 @@ public CelLiteRuntimeBuilder addLibraries(Iterable lateBoundFunctionNames) { + lateBoundFunctionNamesBuilder.addAll(lateBoundFunctionNames); + return this; + } + + @Override + public CelLiteRuntimeBuilder setTypeProvider(CelTypeProvider celTypeProvider) { + this.celTypeProvider = celTypeProvider; + return this; + } + + @Override + public CelLiteRuntimeBuilder setContainer(CelContainer container) { + this.container = checkNotNull(container); + return this; + } + /** Throws if an unsupported flag in CelOptions is toggled. */ private static void assertAllowedCelOptions(CelOptions celOptions) { String prefix = "Misconfigured CelOptions: "; - if (!celOptions.enableCelValue()) { - throw new IllegalArgumentException(prefix + "enableCelValue must be enabled."); - } if (!celOptions.enableUnsignedLongs()) { throw new IllegalArgumentException(prefix + "enableUnsignedLongs cannot be disabled."); } @@ -175,28 +215,52 @@ public CelLiteRuntime build() { func.getDefinition()); }); - Interpreter interpreter = - new DefaultInterpreter( - TypeResolver.create(celValueProvider.celValueConverter()), - CelValueRuntimeTypeProvider.newInstance(celValueProvider), + CelTypeProvider celTypeProvider = DefaultTypeProvider.getInstance(); + if (this.celTypeProvider != null) { + celTypeProvider = + new CelTypeProvider.CombinedCelTypeProvider(celTypeProvider, this.celTypeProvider); + } + + ProgramPlanner planner = + ProgramPlanner.newPlanner( + celTypeProvider, + celValueProvider, dispatcherBuilder.build(), - celOptions); + celValueProvider.celValueConverter(), + container, + celOptions, + lateBoundFunctionNamesBuilder.build()); return new LiteRuntimeImpl( - interpreter, + planner, celOptions, customFunctionBindings.values(), standardFunctions, runtimeLibs, - celValueProvider); + celValueProvider, + celTypeProvider, + container); } private Builder() { - this.celOptions = CelOptions.current().enableCelValue(true).build(); - this.celValueProvider = (structType, fields) -> Optional.empty(); + this.celOptions = CelOptions.DEFAULT; + this.celValueProvider = + new CelValueProvider() { + @Override + public Optional newValue(String structType, Map fields) { + return Optional.empty(); + } + + @Override + public CelValueConverter celValueConverter() { + return CelValueConverter.getDefaultInstance(); + } + }; this.customFunctionBindings = new HashMap<>(); this.standardFunctionBuilder = ImmutableSet.builder(); this.runtimeLibrariesBuilder = ImmutableSet.builder(); + this.lateBoundFunctionNamesBuilder = ImmutableSet.builder(); + this.container = CelContainer.newBuilder().build(); } } @@ -205,17 +269,21 @@ static CelLiteRuntimeBuilder newBuilder() { } private LiteRuntimeImpl( - Interpreter interpreter, + ProgramPlanner planner, CelOptions celOptions, Iterable customFunctionBindings, ImmutableSet celStandardFunctions, ImmutableSet runtimeLibraries, - CelValueProvider celValueProvider) { - this.interpreter = interpreter; + CelValueProvider celValueProvider, + CelTypeProvider celTypeProvider, + CelContainer celContainer) { + this.planner = planner; this.celOptions = celOptions; this.customFunctionBindings = ImmutableList.copyOf(customFunctionBindings); this.celStandardFunctions = celStandardFunctions; this.runtimeLibraries = runtimeLibraries; this.celValueProvider = celValueProvider; + this.celTypeProvider = celTypeProvider; + this.celContainer = celContainer; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index e82f77c67..67a06ffb5 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -1,4 +1,5 @@ load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") package( default_applicable_licenses = ["//:license"], @@ -37,11 +38,8 @@ java_library( ":eval_unary", ":eval_var_args_call", ":eval_zero_arity", - ":interpretable_attribute", ":planned_interpretable", ":planned_program", - ":qualifier", - ":string_qualifier", "//:auto_value", "//common:cel_ast", "//common:container", @@ -108,33 +106,26 @@ java_library( ], ) -java_library( - name = "interpretable_attribute", - srcs = ["InterpretableAttribute.java"], - deps = [ - ":planned_interpretable", - ":qualifier", - "//common/ast", - "@maven//:com_google_errorprone_error_prone_annotations", - ], -) - java_library( name = "attribute", srcs = [ "Attribute.java", "AttributeFactory.java", + "InterpretableAttribute.java", "MaybeAttribute.java", "MissingAttribute.java", "NamespacedAttribute.java", + "PresenceTestQualifier.java", + "Qualifier.java", "RelativeAttribute.java", + "StringQualifier.java", ], deps = [ ":activation_wrapper", ":eval_helpers", ":planned_interpretable", - ":qualifier", "//common:container", + "//common/ast", "//common/exceptions:attribute_not_found", "//common/types", "//common/types:type_providers", @@ -151,125 +142,210 @@ java_library( ) java_library( - name = "activation_wrapper", - srcs = ["ActivationWrapper.java"], + name = "eval_attribute", + srcs = ["EvalAttribute.java"], deps = [ + ":attribute", + ":planned_interpretable", + "//common/ast", "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", ], ) java_library( - name = "qualifier", - srcs = ["Qualifier.java"], + name = "eval_create_list", + srcs = ["EvalCreateList.java"], deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//runtime:accumulated_unknowns", + "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", ], ) java_library( - name = "presence_test_qualifier", - srcs = ["PresenceTestQualifier.java"], + name = "eval_create_map", + srcs = ["EvalCreateMap.java"], deps = [ - ":attribute", - ":qualifier", + ":eval_helpers", + ":localized_evaluation_exception", + ":planned_interpretable", + "//common/ast", + "//common/exceptions:duplicate_key", + "//common/exceptions:invalid_argument", + "//runtime:accumulated_unknowns", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "activation_wrapper", + srcs = ["ActivationWrapper.java"], + deps = ["//runtime:interpretable"], +) + +java_library( + name = "error_metadata", + srcs = ["ErrorMetadata.java"], + deps = [ + "//runtime:metadata", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_and", + srcs = ["EvalAnd.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:interpretable", + "@maven//:com_google_guava_guava", ], ) java_library( - name = "string_qualifier", - srcs = ["StringQualifier.java"], + name = "eval_binary", + srcs = ["EvalBinary.java"], deps = [ - ":qualifier", - "//common/exceptions:attribute_not_found", + ":eval_helpers", + ":planned_interpretable", + "//common/ast", "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "//runtime:resolved_overload", ], ) java_library( - name = "eval_attribute", - srcs = ["EvalAttribute.java"], + name = "eval_block", + srcs = ["EvalBlock.java"], deps = [ - ":attribute", - ":interpretable_attribute", ":planned_interpretable", - ":qualifier", "//common/ast", + "//runtime:evaluation_exception", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", ], ) java_library( - name = "eval_test_only", - srcs = ["EvalTestOnly.java"], + name = "eval_conditional", + srcs = ["EvalConditional.java"], deps = [ - ":interpretable_attribute", ":planned_interpretable", - ":presence_test_qualifier", - ":qualifier", "//common/ast", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", - "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", ], ) java_library( - name = "eval_zero_arity", - srcs = ["EvalZeroArity.java"], + name = "eval_create_struct", + srcs = ["EvalCreateStruct.java"], deps = [ ":eval_helpers", ":planned_interpretable", "//common/ast", + "//common/types:type_providers", "//common/values", - "//runtime:evaluation_exception", + "//common/values:cel_value_provider", + "//runtime:accumulated_unknowns", "//runtime:interpretable", - "//runtime:resolved_overload", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", ], ) java_library( - name = "eval_unary", - srcs = ["EvalUnary.java"], + name = "eval_exhaustive_and", + srcs = ["EvalExhaustiveAnd.java"], deps = [ ":eval_helpers", ":planned_interpretable", "//common/ast", "//common/values", - "//runtime:evaluation_exception", + "//runtime:accumulated_unknowns", "//runtime:interpretable", - "//runtime:resolved_overload", + "@maven//:com_google_errorprone_error_prone_annotations", ], ) java_library( - name = "eval_binary", - srcs = ["EvalBinary.java"], + name = "eval_exhaustive_conditional", + srcs = ["EvalExhaustiveConditional.java"], deps = [ ":eval_helpers", ":planned_interpretable", "//common/ast", - "//common/values", "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", - "//runtime:resolved_overload", + "@maven//:com_google_errorprone_error_prone_annotations", ], ) java_library( - name = "eval_var_args_call", - srcs = ["EvalVarArgsCall.java"], + name = "eval_exhaustive_or", + srcs = ["EvalExhaustiveOr.java"], deps = [ ":eval_helpers", ":planned_interpretable", "//common/ast", "//common/values", "//runtime:accumulated_unknowns", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "eval_fold", + srcs = ["EvalFold.java"], + deps = [ + ":activation_wrapper", + ":planned_interpretable", + "//common/ast", + "//common/exceptions:runtime_exception", + "//common/values:mutable_map_value", + "//runtime:accumulated_unknowns", + "//runtime:concatenated_list_view", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + +java_library( + name = "eval_helpers", + srcs = ["EvalHelpers.java"], + deps = [ + ":localized_evaluation_exception", + ":planned_interpretable", + "//common:error_codes", + "//common/exceptions:runtime_exception", + "//common/values", "//runtime:evaluation_exception", "//runtime:interpretable", "//runtime:resolved_overload", + "@maven//:com_google_guava_guava", ], ) @@ -291,130 +367,125 @@ java_library( ) java_library( - name = "eval_or", - srcs = ["EvalOr.java"], + name = "eval_optional_or", + srcs = ["EvalOptionalOr.java"], deps = [ ":eval_helpers", ":planned_interpretable", "//common/ast", - "//common/values", + "//common/exceptions:overload_not_found", "//runtime:accumulated_unknowns", "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], ) java_library( - name = "eval_and", - srcs = ["EvalAnd.java"], + name = "eval_optional_or_value", + srcs = ["EvalOptionalOrValue.java"], deps = [ ":eval_helpers", ":planned_interpretable", "//common/ast", - "//common/values", + "//common/exceptions:overload_not_found", "//runtime:accumulated_unknowns", "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], ) java_library( - name = "eval_conditional", - srcs = ["EvalConditional.java"], + name = "eval_optional_select_field", + srcs = ["EvalOptionalSelectField.java"], deps = [ + ":eval_helpers", ":planned_interpretable", "//common/ast", + "//common/values", "//runtime:accumulated_unknowns", - "//runtime:evaluation_exception", "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], ) java_library( - name = "eval_create_struct", - srcs = ["EvalCreateStruct.java"], + name = "eval_or", + srcs = ["EvalOr.java"], deps = [ ":eval_helpers", ":planned_interpretable", "//common/ast", - "//common/types:type_providers", "//common/values", - "//common/values:cel_value_provider", "//runtime:accumulated_unknowns", "//runtime:interpretable", - "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], ) java_library( - name = "eval_create_list", - srcs = ["EvalCreateList.java"], + name = "eval_test_only", + srcs = ["EvalTestOnly.java"], deps = [ - ":eval_helpers", + ":attribute", ":planned_interpretable", "//common/ast", - "//runtime:accumulated_unknowns", + "//runtime:evaluation_exception", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", ], ) java_library( - name = "eval_create_map", - srcs = ["EvalCreateMap.java"], + name = "eval_unary", + srcs = ["EvalUnary.java"], deps = [ ":eval_helpers", - ":localized_evaluation_exception", ":planned_interpretable", "//common/ast", - "//common/exceptions:duplicate_key", - "//common/exceptions:invalid_argument", - "//runtime:accumulated_unknowns", + "//common/values", "//runtime:evaluation_exception", "//runtime:interpretable", - "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", + "//runtime:resolved_overload", ], ) java_library( - name = "eval_fold", - srcs = ["EvalFold.java"], + name = "eval_var_args_call", + srcs = ["EvalVarArgsCall.java"], deps = [ - ":activation_wrapper", + ":eval_helpers", ":planned_interpretable", "//common/ast", - "//common/exceptions:runtime_exception", - "//common/values:mutable_map_value", + "//common/values", "//runtime:accumulated_unknowns", - "//runtime:concatenated_list_view", "//runtime:evaluation_exception", "//runtime:interpretable", - "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", - "@maven//:org_jspecify_jspecify", + "//runtime:resolved_overload", ], ) java_library( - name = "eval_helpers", - srcs = ["EvalHelpers.java"], + name = "eval_zero_arity", + srcs = ["EvalZeroArity.java"], deps = [ - ":localized_evaluation_exception", + ":eval_helpers", ":planned_interpretable", - "//common:error_codes", - "//common/exceptions:runtime_exception", + "//common/ast", "//common/values", "//runtime:evaluation_exception", "//runtime:interpretable", "//runtime:resolved_overload", - "@maven//:com_google_guava_guava", ], ) +alias( + name = "interpretable_attribute", + actual = ":attribute", +) + java_library( name = "localized_evaluation_exception", srcs = ["LocalizedEvaluationException.java"], @@ -424,16 +495,6 @@ java_library( ], ) -java_library( - name = "error_metadata", - srcs = ["ErrorMetadata.java"], - deps = [ - "//runtime:metadata", - "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", - ], -) - java_library( name = "planned_interpretable", srcs = [ @@ -458,101 +519,505 @@ java_library( ], ) -java_library( - name = "eval_optional_or", - srcs = ["EvalOptionalOr.java"], +cel_android_library( + name = "program_planner_android", + srcs = ["ProgramPlanner.java"], + tags = [ + ], deps = [ - ":eval_helpers", - ":planned_interpretable", - "//common/ast", + ":attribute_android", + ":error_metadata_android", + ":eval_and_android", + ":eval_attribute_android", + ":eval_binary_android", + ":eval_block_android", + ":eval_conditional_android", + ":eval_const_android", + ":eval_create_list_android", + ":eval_create_map_android", + ":eval_create_struct_android", + ":eval_exhaustive_and_android", + ":eval_exhaustive_conditional_android", + ":eval_exhaustive_or_android", + ":eval_fold_android", + ":eval_late_bound_call_android", + ":eval_optional_or_android", + ":eval_optional_or_value_android", + ":eval_optional_select_field_android", + ":eval_or_android", + ":eval_test_only_android", + ":eval_unary_android", + ":eval_var_args_call_android", + ":eval_zero_arity_android", + ":planned_interpretable_android", + ":planned_program_android", + "//:auto_value", + "//common:cel_ast_android", + "//common:container_android", + "//common:operator_android", + "//common:options", + "//common/annotations", + "//common/ast:ast_android", + "//common/ast:cel_block_android", "//common/exceptions:overload_not_found", - "//runtime:accumulated_unknowns", - "//runtime:interpretable", + "//common/types:type_providers_android", + "//common/types:types_android", + "//common/values:cel_value_provider_android", + "//common/values:values_android", + "//runtime:dispatcher_android", + "//runtime:evaluation_exception", + "//runtime:evaluation_exception_builder", + "//runtime:resolved_overload_android", + "//runtime/src/main/java/dev/cel/runtime:program_android", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", ], ) -java_library( - name = "eval_optional_or_value", - srcs = ["EvalOptionalOrValue.java"], +cel_android_library( + name = "planned_program_android", + srcs = ["PlannedProgram.java"], deps = [ - ":eval_helpers", - ":planned_interpretable", - "//common/ast", - "//common/exceptions:overload_not_found", - "//runtime:accumulated_unknowns", - "//runtime:interpretable", + ":error_metadata_android", + ":localized_evaluation_exception_android", + ":planned_interpretable_android", + "//:auto_value", + "//common:options", + "//common/annotations", + "//common/exceptions:runtime_exception", + "//common/values:values_android", + "//runtime:activation_android", + "//runtime:evaluation_exception", + "//runtime:evaluation_exception_builder", + "//runtime:interpretable_android", + "//runtime:resolved_overload_android", + "//runtime:variable_resolver", + "//runtime/src/main/java/dev/cel/runtime:evaluation_listener_android", + "//runtime/src/main/java/dev/cel/runtime:function_resolver_android", + "//runtime/src/main/java/dev/cel/runtime:interpreter_util_android", + "//runtime/src/main/java/dev/cel/runtime:partial_vars_android", + "//runtime/src/main/java/dev/cel/runtime:program_android", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", ], ) -java_library( - name = "eval_optional_select_field", - srcs = ["EvalOptionalSelectField.java"], +cel_android_library( + name = "eval_const_android", + srcs = ["EvalConstant.java"], deps = [ - ":eval_helpers", - ":planned_interpretable", - "//common/ast", - "//common/values", - "//runtime:accumulated_unknowns", - "//runtime:interpretable", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//runtime:interpretable_android", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", ], ) -java_library( - name = "eval_exhaustive_and", +cel_android_library( + name = "attribute_android", + srcs = [ + "Attribute.java", + "AttributeFactory.java", + "InterpretableAttribute.java", + "MaybeAttribute.java", + "MissingAttribute.java", + "NamespacedAttribute.java", + "PresenceTestQualifier.java", + "Qualifier.java", + "RelativeAttribute.java", + "StringQualifier.java", + ], + deps = [ + ":activation_wrapper_android", + ":eval_helpers_android", + ":planned_interpretable_android", + "//common:container_android", + "//common/ast:ast_android", + "//common/exceptions:attribute_not_found", + "//common/types:type_providers_android", + "//common/types:types_android", + "//common/values:values_android", + "//runtime:interpretable_android", + "//runtime:unknown_attributes_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", + "//runtime/src/main/java/dev/cel/runtime:interpreter_util_android", + "//runtime/src/main/java/dev/cel/runtime:partial_vars_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "eval_attribute_android", + srcs = ["EvalAttribute.java"], + deps = [ + ":attribute_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//runtime:interpretable_android", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "eval_create_list_android", + srcs = ["EvalCreateList.java"], + deps = [ + ":eval_helpers_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//runtime:interpretable_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "eval_create_map_android", + srcs = ["EvalCreateMap.java"], + deps = [ + ":eval_helpers_android", + ":localized_evaluation_exception_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/exceptions:duplicate_key", + "//common/exceptions:invalid_argument", + "//runtime:evaluation_exception", + "//runtime:interpretable_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "activation_wrapper_android", + srcs = ["ActivationWrapper.java"], + deps = ["//runtime:interpretable_android"], +) + +cel_android_library( + name = "error_metadata_android", + srcs = ["ErrorMetadata.java"], + deps = [ + "//runtime:metadata", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "eval_and_android", + srcs = ["EvalAnd.java"], + deps = [ + ":eval_helpers_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/values:values_android", + "//runtime:interpretable_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "eval_binary_android", + srcs = ["EvalBinary.java"], + deps = [ + ":eval_helpers_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/values:values_android", + "//runtime:evaluation_exception", + "//runtime:interpretable_android", + "//runtime:resolved_overload_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", + ], +) + +cel_android_library( + name = "eval_block_android", + srcs = ["EvalBlock.java"], + deps = [ + ":planned_interpretable_android", + "//common/ast:ast_android", + "//runtime:evaluation_exception", + "//runtime:interpretable_android", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "eval_conditional_android", + srcs = ["EvalConditional.java"], + deps = [ + ":planned_interpretable_android", + "//common/ast:ast_android", + "//runtime:evaluation_exception", + "//runtime:interpretable_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "eval_create_struct_android", + srcs = ["EvalCreateStruct.java"], + deps = [ + ":eval_helpers_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/types:type_providers_android", + "//common/values:cel_value_provider_android", + "//common/values:values_android", + "//runtime:interpretable_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "eval_exhaustive_and_android", srcs = ["EvalExhaustiveAnd.java"], deps = [ - ":eval_helpers", - ":planned_interpretable", - "//common/ast", - "//common/values", - "//runtime:accumulated_unknowns", - "//runtime:interpretable", + ":eval_helpers_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/values:values_android", + "//runtime:interpretable_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", "@maven//:com_google_errorprone_error_prone_annotations", ], ) -java_library( - name = "eval_exhaustive_or", +cel_android_library( + name = "eval_exhaustive_conditional_android", + srcs = ["EvalExhaustiveConditional.java"], + deps = [ + ":eval_helpers_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//runtime:evaluation_exception", + "//runtime:interpretable_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "eval_exhaustive_or_android", srcs = ["EvalExhaustiveOr.java"], deps = [ - ":eval_helpers", - ":planned_interpretable", - "//common/ast", - "//common/values", - "//runtime:accumulated_unknowns", - "//runtime:interpretable", + ":eval_helpers_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/values:values_android", + "//runtime:interpretable_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", "@maven//:com_google_errorprone_error_prone_annotations", ], ) -java_library( - name = "eval_exhaustive_conditional", - srcs = ["EvalExhaustiveConditional.java"], +cel_android_library( + name = "eval_fold_android", + srcs = ["EvalFold.java"], deps = [ - ":eval_helpers", - ":planned_interpretable", - "//common/ast", - "//runtime:accumulated_unknowns", + ":activation_wrapper_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/exceptions:runtime_exception", + "//common/values:mutable_map_value_android", + "//runtime:concatenated_list_view", "//runtime:evaluation_exception", - "//runtime:interpretable", + "//runtime:interpretable_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", ], ) -java_library( - name = "eval_block", - srcs = ["EvalBlock.java"], +cel_android_library( + name = "eval_helpers_android", + srcs = ["EvalHelpers.java"], deps = [ - ":planned_interpretable", - "//common/ast", + ":localized_evaluation_exception_android", + ":planned_interpretable_android", + "//common:error_codes", + "//common/exceptions:runtime_exception", + "//common/values:values_android", "//runtime:evaluation_exception", - "//runtime:interpretable", + "//runtime:interpretable_android", + "//runtime:resolved_overload_android", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "eval_late_bound_call_android", + srcs = ["EvalLateBoundCall.java"], + deps = [ + ":eval_helpers_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/exceptions:overload_not_found", + "//common/values:values_android", + "//runtime:evaluation_exception", + "//runtime:interpretable_android", + "//runtime:resolved_overload_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "eval_optional_or_android", + srcs = ["EvalOptionalOr.java"], + deps = [ + ":eval_helpers_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/exceptions:overload_not_found", + "//runtime:interpretable_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "eval_optional_or_value_android", + srcs = ["EvalOptionalOrValue.java"], + deps = [ + ":eval_helpers_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/exceptions:overload_not_found", + "//runtime:interpretable_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "eval_optional_select_field_android", + srcs = ["EvalOptionalSelectField.java"], + deps = [ + ":eval_helpers_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/values:values_android", + "//runtime:interpretable_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "eval_or_android", + srcs = ["EvalOr.java"], + deps = [ + ":eval_helpers_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/values:values_android", + "//runtime:interpretable_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "eval_test_only_android", + srcs = ["EvalTestOnly.java"], + deps = [ + ":attribute_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//runtime:evaluation_exception", + "//runtime:interpretable_android", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "eval_unary_android", + srcs = ["EvalUnary.java"], + deps = [ + ":eval_helpers_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/values:values_android", + "//runtime:evaluation_exception", + "//runtime:interpretable_android", + "//runtime:resolved_overload_android", + ], +) + +cel_android_library( + name = "eval_var_args_call_android", + srcs = ["EvalVarArgsCall.java"], + deps = [ + ":eval_helpers_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/values:values_android", + "//runtime:evaluation_exception", + "//runtime:interpretable_android", + "//runtime:resolved_overload_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", + ], +) + +cel_android_library( + name = "eval_zero_arity_android", + srcs = ["EvalZeroArity.java"], + deps = [ + ":eval_helpers_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/values:values_android", + "//runtime:evaluation_exception", + "//runtime:interpretable_android", + "//runtime:resolved_overload_android", + ], +) + +cel_android_library( + name = "localized_evaluation_exception_android", + srcs = ["LocalizedEvaluationException.java"], + deps = [ + "//common:error_codes", + "//common/exceptions:runtime_exception", + ], +) + +cel_android_library( + name = "planned_interpretable_android", + srcs = [ + "BlockMemoizer.java", + "ExecutionFrame.java", + "PlannedInterpretable.java", + ], + deps = [ + ":localized_evaluation_exception_android", + "//common:options", + "//common/ast:ast_android", + "//common/exceptions:iteration_budget_exceeded", + "//runtime:evaluation_exception", + "//runtime:evaluation_listener_android", + "//runtime:interpretable_android", + "//runtime:interpreter_util_android", + "//runtime:resolved_overload_android", + "//runtime/src/main/java/dev/cel/runtime:function_resolver_android", + "//runtime/src/main/java/dev/cel/runtime:partial_vars_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", ], ) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java index b7fb8ad72..46af3c701 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/MissingAttribute.java @@ -15,10 +15,12 @@ package dev.cel.runtime.planner; import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.Immutable; import dev.cel.common.exceptions.CelAttributeNotFoundException; import dev.cel.runtime.GlobalResolver; /** Represents a missing attribute that is surfaced while resolving a struct field or a map key. */ +@Immutable final class MissingAttribute implements Attribute { private final ImmutableSet missingAttributes; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PresenceTestQualifier.java b/runtime/src/main/java/dev/cel/runtime/planner/PresenceTestQualifier.java index 5c2cba1ed..a93ec74b7 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/PresenceTestQualifier.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/PresenceTestQualifier.java @@ -16,10 +16,12 @@ import static dev.cel.runtime.planner.MissingAttribute.newMissingField; +import com.google.errorprone.annotations.Immutable; import dev.cel.common.values.SelectableValue; import java.util.Map; /** A qualifier for presence testing a field or a map key. */ +@Immutable final class PresenceTestQualifier implements Qualifier { @SuppressWarnings("Immutable") diff --git a/runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java b/runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java index 293ca5c7d..21a4b6721 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java @@ -14,12 +14,14 @@ package dev.cel.runtime.planner; +import com.google.errorprone.annotations.Immutable; import dev.cel.common.exceptions.CelAttributeNotFoundException; import dev.cel.common.values.OptionalValue; import dev.cel.common.values.SelectableValue; import java.util.Map; /** A qualifier that accesses fields or map keys using a string identifier. */ +@Immutable final class StringQualifier implements Qualifier { private final String value; diff --git a/runtime/src/test/java/dev/cel/runtime/AbstractPlannerInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/AbstractPlannerInterpreterTest.java new file mode 100644 index 000000000..5d8474442 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/AbstractPlannerInterpreterTest.java @@ -0,0 +1,280 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Timestamp; +import dev.cel.common.CelContainer; +import dev.cel.common.CelOptions; +import dev.cel.common.types.ListType; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.testing.BaseInterpreterTest; +import java.util.Arrays; +import java.util.Objects; +import org.junit.Test; + +public abstract class AbstractPlannerInterpreterTest extends BaseInterpreterTest { + + @Override + public void optional_errors() { + // Exercised in planner_optional_errors instead + skipBaselineVerification(); + } + + @Test + public void planner_optional_errors() { + source = "optional.unwrap([dyn(1)])"; + runTest(ImmutableMap.of()); + } + + @Override + public void unknownField() { + // Exercised in planner_unknownFieldSelection instead + skipBaselineVerification(); + } + + @Override + public void unknownResultSet() { + // Exercised in planner_unknownResultSet_success instead + skipBaselineVerification(); + } + + @Test + public void planner_unknownFieldSelection() { + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + + CelAttributePattern patternX = CelAttributePattern.fromQualifiedIdentifier("x"); + + source = "x"; + // We have the full message, but we're claiming that the attribute is unknown. + runTest(ImmutableMap.of("x", TestAllTypes.getDefaultInstance()), patternX); + // A "partially known message". The result is still an unknown. + runTest( + ImmutableMap.of("x", TestAllTypes.getDefaultInstance()), + CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); + + source = "x.single_int32"; + runTest(ImmutableMap.of(), patternX); + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); + + source = "x.map_int32_int64[22]"; + runTest(ImmutableMap.of(), patternX); + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.map_int32_int64")); + + source = "x.repeated_nested_message[1]"; + runTest(ImmutableMap.of(), patternX); + runTest( + ImmutableMap.of(), + CelAttributePattern.fromQualifiedIdentifier("x.repeated_nested_message")); + + source = "x.single_nested_message.bb"; + runTest(ImmutableMap.of(), patternX); + runTest( + ImmutableMap.of(), + CelAttributePattern.fromQualifiedIdentifier("x.single_nested_message.bb")); + + source = "{1: x.single_int32}"; + runTest(ImmutableMap.of(), patternX); + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); + + source = "[1, x.single_int32]"; + runTest(ImmutableMap.of(), patternX); + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); + } + + @Test + public void planner_unknownResultSet_success() { + setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + TestAllTypes message = + TestAllTypes.newBuilder() + .setSingleString("test") + .setSingleTimestamp(Timestamp.newBuilder().setSeconds(15)) + .build(); + ImmutableMap variables = ImmutableMap.of("x", message); + CelAttributePattern unknownInt32 = + CelAttributePattern.fromQualifiedIdentifier("x.single_int32"); + CelAttributePattern unknownInt64 = + CelAttributePattern.fromQualifiedIdentifier("x.single_int64"); + + source = "x.single_int32 == 1 && true"; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 && false"; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 && x.single_int64 == 1"; + runTest(variables, unknownInt32, unknownInt64); + + source = "true && x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = "false && x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_string == \"test\""; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_string != \"test\""; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_int64 == 1"; + runTest(variables, unknownInt32, unknownInt64); + + source = "true || x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = "false || x.single_int32 == 1"; + runTest(variables, unknownInt32); + + // dispatch test + declareFunction( + "f", memberOverload("f", Arrays.asList(SimpleType.INT, SimpleType.INT), SimpleType.BOOL)); + celRuntime = + newBaseRuntimeBuilder( + CelOptions.current() + .enableHeterogeneousNumericComparisons(true) + .enableOptionalSyntax(true) + .comprehensionMaxIterations(1_000) + .build()) + .addFunctionBindings( + CelFunctionBinding.from("f", Integer.class, Integer.class, Objects::equals)) + .setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())) + .build(); + + source = "x.single_int32.f(1)"; + runTest(variables, unknownInt32); + + source = "1.f(x.single_int32)"; + runTest(variables, unknownInt32); + + source = "x.single_int64.f(x.single_int32)"; + runTest(variables, unknownInt32, unknownInt64); + + source = "[0, 2, 4].exists(z, z == 2 || z == x.single_int32)"; + runTest(variables, unknownInt32); + + source = "[0, 2, 4].exists(z, z == x.single_int32)"; + runTest(variables, unknownInt32); + + source = + "[0, 2, 4].exists_one(z, z == 0 || (z == 2 && z == x.single_int32) " + + "|| (z == 4 && z == x.single_int64))"; + runTest(variables, unknownInt32, unknownInt64); + + source = "[0, 2].all(z, z == 2 || z == x.single_int32)"; + runTest(variables, unknownInt32); + + source = + "[0, 2, 4].filter(z, z == 0 || (z == 2 && z == x.single_int32) " + + "|| (z == 4 && z == x.single_int64))"; + runTest(variables, unknownInt32, unknownInt64); + + source = + "[0, 2, 4].map(z, z == 0 || (z == 2 && z == x.single_int32) " + + "|| (z == 4 && z == x.single_int64))"; + runTest(variables, unknownInt32, unknownInt64); + + source = "x.single_int32 == 1 ? 1 : 2"; + runTest(variables, unknownInt32); + + source = "true ? x.single_int32 : 2"; + runTest(variables, unknownInt32); + + source = "true ? 1 : x.single_int32"; + runTest(variables, unknownInt32); + + source = "false ? x.single_int32 : 2"; + runTest(variables, unknownInt32); + + source = "false ? 1 : x.single_int32"; + runTest(variables, unknownInt32); + + source = "x.single_int64 == 1 ? x.single_int32 : x.single_int32"; + runTest(variables, unknownInt32, unknownInt64); + + source = "{x.single_int32: 2, 3: 4}"; + runTest(variables, unknownInt32); + + source = "{1: x.single_int32, 3: 4}"; + runTest(variables, unknownInt32); + + source = "{1: x.single_int32, x.single_int64: 4}"; + runTest(variables, unknownInt32, unknownInt64); + + source = "[1, x.single_int32, 3, 4]"; + runTest(variables, unknownInt32); + + source = "[1, x.single_int32, x.single_int64, 4]"; + runTest(variables, unknownInt32, unknownInt64); + + source = "TestAllTypes{single_int32: x.single_int32}.single_int32 == 2"; + runTest(variables, unknownInt32); + + source = "TestAllTypes{single_int32: x.single_int32, single_int64: x.single_int64}"; + runTest(variables, unknownInt32, unknownInt64); + + clearAllDeclarations(); + declareVariable("unknown_list", ListType.create(SimpleType.INT)); + source = "unknown_list.map(x, x)"; + runTest(variables, CelAttributePattern.fromQualifiedIdentifier("unknown_list")); + + clearAllDeclarations(); + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + source = "cel.bind(x, [1, 2, 3], 1 in x)"; + runTest(variables, CelAttributePattern.fromQualifiedIdentifier("x")); + } + + @Test + public void planner_unknownResultSet_errors() { + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + TestAllTypes message = + TestAllTypes.newBuilder() + .setSingleString("test") + .setSingleTimestamp(Timestamp.newBuilder().setSeconds(15)) + .build(); + ImmutableMap variables = ImmutableMap.of("x", message); + CelAttributePattern unknownInt32 = + CelAttributePattern.fromQualifiedIdentifier("x.single_int32"); + + source = "x.single_int32 == 1 && x.single_timestamp <= timestamp(\"bad timestamp string\")"; + runTest(variables, unknownInt32); + + source = "x.single_timestamp <= timestamp(\"bad timestamp string\") && x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = + "x.single_timestamp <= timestamp(\"bad timestamp string\") " + + "&& x.single_timestamp > timestamp(\"another bad timestamp string\")"; + runTest(variables, unknownInt32); + + source = "x.single_int32 == 1 || x.single_timestamp <= timestamp(\"bad timestamp string\")"; + runTest(variables, unknownInt32); + + source = "x.single_timestamp <= timestamp(\"bad timestamp string\") || x.single_int32 == 1"; + runTest(variables, unknownInt32); + + source = + "x.single_timestamp <= timestamp(\"bad timestamp string\") " + + "|| x.single_timestamp > timestamp(\"another bad timestamp string\")"; + runTest(variables, unknownInt32); + + source = "x"; + runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x")); + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel index e886c3d8a..a2e44223a 100644 --- a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel @@ -19,6 +19,7 @@ java_library( ["*.java"], # keep sorted exclude = [ + "AbstractPlannerInterpreterTest.java", "CelLiteInterpreterTest.java", "InterpreterTest.java", "PlannerInterpreterTest.java", @@ -55,7 +56,6 @@ java_library( "//common/types:message_type_provider", "//common/values", "//common/values:cel_byte_string", - "//common/values:cel_value_provider", "//common/values:proto_message_lite_value_provider", "//compiler", "//compiler:compiler_builder", @@ -127,6 +127,24 @@ java_library( ], ) +java_library( + name = "abstract_planner_interpreter_test", + testonly = 1, + srcs = ["AbstractPlannerInterpreterTest.java"], + deps = [ + "//common:container", + "//common:options", + "//common/types", + "//runtime:function_binding", + "//runtime:unknown_attributes", + "//testing:base_interpreter_test", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:junit_junit", + ], +) + java_library( name = "planner_interpreter_test", testonly = 1, @@ -137,20 +155,15 @@ java_library( "//runtime/testdata", ], deps = [ + ":abstract_planner_interpreter_test", "//common:cel_ast", "//common:compiler_common", "//common:container", "//common:options", - "//common/types", "//common/types:type_providers", "//extensions", "//runtime", - "//runtime:function_binding", - "//runtime:unknown_attributes", - "//testing:base_interpreter_test", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", - "@maven//:com_google_guava_guava", - "@maven//:com_google_protobuf_protobuf_java", "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", ], @@ -176,6 +189,7 @@ cel_android_local_test( "//runtime:lite_runtime_android", "//runtime:lite_runtime_factory_android", "//runtime:lite_runtime_impl_android", + "//runtime:partial_vars_android", "//runtime:standard_functions_android", "//runtime:unknown_attributes_android", "//runtime/src/main/java/dev/cel/runtime:program_android", @@ -199,7 +213,11 @@ java_library( srcs = [ "CelLiteInterpreterTest.java", ], + resources = [ + "//runtime/testdata", + ], deps = [ + ":abstract_planner_interpreter_test", "//common:options", "//common/values:proto_message_lite_value_provider", "//extensions:optional_library", diff --git a/runtime/src/test/java/dev/cel/runtime/CelLiteInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/CelLiteInterpreterTest.java index 1d1a316c0..bc0fde200 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelLiteInterpreterTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelLiteInterpreterTest.java @@ -27,16 +27,18 @@ * ProtoMessageLiteValueProvider} and full version of protobuf messages. */ @RunWith(TestParameterInjector.class) -public class CelLiteInterpreterTest extends BaseInterpreterTest { +public class CelLiteInterpreterTest extends AbstractPlannerInterpreterTest { @Override protected CelRuntimeBuilder newBaseRuntimeBuilder(CelOptions celOptions) { - return CelRuntimeFactory.standardCelRuntimeBuilder() + return CelRuntimeFactory.plannerRuntimeBuilder() .setValueProvider( ProtoMessageLiteValueProvider.newInstance( dev.cel.expr.conformance.proto2.TestAllTypesCelDescriptor.getDescriptor(), TestAllTypesCelDescriptor.getDescriptor())) .addLibraries(CelOptionalLibrary.INSTANCE) + .addFileTypes(TEST_FILE_DESCRIPTORS) + .addLateBoundFunctions("record") .setOptions(celOptions.toBuilder().enableCelValue(true).build()); } diff --git a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeAndroidTest.java b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeAndroidTest.java index 73492d126..6c54ce486 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeAndroidTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeAndroidTest.java @@ -192,7 +192,10 @@ public void eval_add() throws Exception { @Test public void eval_stringLiteral() throws Exception { - CelLiteRuntime runtime = CelLiteRuntimeFactory.newLiteRuntimeBuilder().build(); + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .build(); // Expr: 'hello world' CelAbstractSyntaxTree ast = readCheckedExpr("compiled_hello_world"); Program program = runtime.createProgram(ast); @@ -205,7 +208,10 @@ public void eval_stringLiteral() throws Exception { @Test @SuppressWarnings("unchecked") public void eval_listLiteral() throws Exception { - CelLiteRuntime runtime = CelLiteRuntimeFactory.newLiteRuntimeBuilder().build(); + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .build(); // Expr: ['a', 1, 2u, 3.5] CelAbstractSyntaxTree ast = readCheckedExpr("compiled_list_literal"); Program program = runtime.createProgram(ast); @@ -286,6 +292,7 @@ public void eval_customFunctions_asLateBoundFunctions() throws Exception { CelLiteRuntime runtime = CelLiteRuntimeFactory.newLiteRuntimeBuilder() .addFunctionBindings(CelFunctionBinding.from("list_isEmpty", List.class, List::isEmpty)) + .addLateBoundFunctions("isEmpty") .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) .build(); // Expr: ''.isEmpty() && [].isEmpty() @@ -307,11 +314,18 @@ public void eval_customFunctions_asLateBoundFunctions() throws Exception { @TestParameters("{checkedExpr: 'compiled_proto2_select_primitives'}") @TestParameters("{checkedExpr: 'compiled_proto3_select_primitives'}") public void eval_protoMessage_unknowns(String checkedExpr) throws Exception { - CelLiteRuntime runtime = CelLiteRuntimeFactory.newLiteRuntimeBuilder().build(); + CelLiteRuntime runtime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .build(); CelAbstractSyntaxTree ast = readCheckedExpr(checkedExpr); Program program = runtime.createProgram(ast); - CelUnknownSet result = (CelUnknownSet) program.eval(); + CelUnknownSet result = + (CelUnknownSet) + program.eval( + PartialVars.of( + CelAttributePattern.create("proto2"), CelAttributePattern.create("proto3"))); assertThat(result.unknownExprIds()).hasSize(15); } @@ -516,7 +530,8 @@ public void eval_protoMessage_deepTraversalReturnsRepeatedStrings(String checked .setPayload( dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() .addAllRepeatedString(data) - .build())), + .build())) + .build(), "proto3", TestAllTypes.newBuilder() .setOneofType( @@ -524,7 +539,8 @@ public void eval_protoMessage_deepTraversalReturnsRepeatedStrings(String checked .setPayload( TestAllTypes.newBuilder() .addAllRepeatedString(data) - .build())))); + .build())) + .build())); assertThat(result).isEqualTo(data); } @@ -713,7 +729,6 @@ public void eval_protoMessage_mapFields(String checkedExpr) throws Exception { } private enum CelOptionsTestCase { - CEL_VALUE_DISABLED(newBaseTestOptions().enableCelValue(false).build()), UNSIGNED_LONG_DISABLED(newBaseTestOptions().enableUnsignedLongs(false).build()), UNWRAP_WKT_DISABLED(newBaseTestOptions().unwrapWellKnownTypesOnFunctionDispatch(false).build()), ; diff --git a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeTest.java b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeTest.java index 0ce7bd184..56a944d8b 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeTest.java @@ -20,6 +20,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.primitives.UnsignedLong; import com.google.protobuf.BoolValue; import com.google.protobuf.ByteString; @@ -42,6 +43,7 @@ import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOverloadDecl; import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.common.types.ProtoMessageTypeProvider; import dev.cel.common.types.SimpleType; import dev.cel.common.types.StructTypeReference; import dev.cel.common.values.CelByteString; @@ -74,22 +76,33 @@ /** Exercises tests for CelLiteRuntime using full version of protobuf messages. */ @RunWith(TestParameterInjector.class) public class CelLiteRuntimeTest { + private static final CelContainer CEL_CONTAINER = + CelContainer.ofName("cel.expr.conformance.proto3"); + private static final CelCompiler CEL_COMPILER = CelCompilerFactory.standardCelCompilerBuilder() .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) .addVar("content", SimpleType.DYN) .addMessageTypes(TestAllTypes.getDescriptor()) - .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .setContainer(CEL_CONTAINER) .build(); private static final CelLiteRuntime CEL_RUNTIME = CelLiteRuntimeFactory.newLiteRuntimeBuilder() .setStandardFunctions(CelStandardFunctions.ALL_STANDARD_FUNCTIONS) + .setTypeProvider( + ProtoMessageTypeProvider.newBuilder() + .addDescriptors( + ImmutableSet.of( + dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor(), + TestAllTypes.getDescriptor())) + .build()) .setValueProvider( ProtoMessageLiteValueProvider.newInstance( dev.cel.expr.conformance.proto2.TestAllTypesCelDescriptor.getDescriptor(), TestAllTypesCelDescriptor.getDescriptor())) + .setContainer(CEL_CONTAINER) .build(); @Test @@ -596,6 +609,11 @@ public void nestedMessage_fromImportedProto() throws Exception { ProtoMessageLiteValueProvider.newInstance( SingleFileCelDescriptor.getDescriptor(), MultiFileCelDescriptor.getDescriptor())) + .setTypeProvider( + ProtoMessageTypeProvider.newBuilder() + .addDescriptors( + ImmutableList.of(SingleFile.getDescriptor(), MultiFile.getDescriptor())) + .build()) .build(); CelAbstractSyntaxTree ast = celCompiler.compile("multiFile.nested_single_file.name").getAst(); @@ -608,7 +626,8 @@ public void nestedMessage_fromImportedProto() throws Exception { ImmutableMap.of( "multiFile", MultiFile.newBuilder() - .setNestedSingleFile(SingleFile.newBuilder().setName("foo").build()))); + .setNestedSingleFile(SingleFile.newBuilder().setName("foo").build()) + .build())); assertThat(result).isEqualTo("foo"); } @@ -623,7 +642,10 @@ public void eval_withLateBoundFunction() throws Exception { CelOverloadDecl.newGlobalOverload( "lateBoundFunc_string", SimpleType.STRING, SimpleType.STRING))) .build(); - CelLiteRuntime celRuntime = CelLiteRuntimeFactory.newLiteRuntimeBuilder().build(); + CelLiteRuntime celRuntime = + CelLiteRuntimeFactory.newLiteRuntimeBuilder() + .addLateBoundFunctions("lateBoundFunc") + .build(); CelAbstractSyntaxTree ast = celCompiler.compile("lateBoundFunc('hello')").getAst(); String result = @@ -680,6 +702,10 @@ public void eval_withEnumField() throws Exception { .setValueProvider( ProtoMessageLiteValueProvider.newInstance( MessageWithEnumCelDescriptor.getDescriptor())) + .setTypeProvider( + ProtoMessageTypeProvider.newBuilder() + .addFileDescriptors(ImmutableList.of(MessageWithEnum.getDescriptor().getFile())) + .build()) .build(); CelAbstractSyntaxTree ast = celCompiler.compile("msg.simple_enum").getAst(); @@ -689,7 +715,7 @@ public void eval_withEnumField() throws Exception { .createProgram(ast) .eval( ImmutableMap.of( - "msg", MessageWithEnum.newBuilder().setSimpleEnum(SimpleEnum.BAR))); + "msg", MessageWithEnum.newBuilder().setSimpleEnum(SimpleEnum.BAR).build())); assertThat(result).isEqualTo(SimpleEnum.BAR.getNumber()); } diff --git a/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java b/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java index fa3b5f4ae..fec5fab41 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java @@ -19,12 +19,10 @@ import com.google.protobuf.Message; import dev.cel.common.CelException; import dev.cel.common.exceptions.CelDivideByZeroException; -import dev.cel.common.values.CelValueProvider; import dev.cel.compiler.CelCompiler; import dev.cel.compiler.CelCompilerFactory; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.runtime.CelStandardFunctions.StandardFunction; -import java.util.Optional; import java.util.function.Function; import org.junit.Assert; import org.junit.Test; @@ -108,13 +106,11 @@ public void toRuntimeBuilder_optionalProperties() { Function customTypeFactory = (typeName) -> TestAllTypes.newBuilder(); CelStandardFunctions overriddenStandardFunctions = CelStandardFunctions.newBuilder().includeFunctions(StandardFunction.ADD).build(); - CelValueProvider noOpValueProvider = (structType, fields) -> Optional.empty(); CelRuntimeBuilder celRuntimeBuilder = CelRuntimeFactory.standardCelRuntimeBuilder() .setStandardEnvironmentEnabled(false) .setTypeFactory(customTypeFactory) - .setStandardFunctions(overriddenStandardFunctions) - .setValueProvider(noOpValueProvider); + .setStandardFunctions(overriddenStandardFunctions); CelRuntime celRuntime = celRuntimeBuilder.build(); CelRuntimeLegacyImpl.Builder newRuntimeBuilder = @@ -123,6 +119,5 @@ public void toRuntimeBuilder_optionalProperties() { assertThat(newRuntimeBuilder.customTypeFactory).isEqualTo(customTypeFactory); assertThat(newRuntimeBuilder.overriddenStandardFunctions) .isEqualTo(overriddenStandardFunctions); - assertThat(newRuntimeBuilder.celValueProvider).isEqualTo(noOpValueProvider); } } diff --git a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java index 9ae8590d5..4d93c6e07 100644 --- a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java +++ b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java @@ -14,8 +14,6 @@ package dev.cel.runtime; -import com.google.common.collect.ImmutableMap; -import com.google.protobuf.Timestamp; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.common.CelAbstractSyntaxTree; @@ -23,20 +21,13 @@ import dev.cel.common.CelOptions; import dev.cel.common.CelValidationException; import dev.cel.common.types.CelTypeProvider; -import dev.cel.common.types.ListType; -import dev.cel.common.types.SimpleType; -import dev.cel.common.types.StructTypeReference; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.extensions.CelExtensions; -import dev.cel.testing.BaseInterpreterTest; -import java.util.Arrays; -import java.util.Objects; -import org.junit.Test; import org.junit.runner.RunWith; /** Interpreter tests using ProgramPlanner */ @RunWith(TestParameterInjector.class) -public class PlannerInterpreterTest extends BaseInterpreterTest { +public class PlannerInterpreterTest extends AbstractPlannerInterpreterTest { @TestParameter boolean isParseOnly; @@ -79,253 +70,4 @@ protected CelAbstractSyntaxTree prepareTest(CelTypeProvider typeProvider) { return null; } } - - @Override - public void optional_errors() { - // Exercised in planner_optional_errors instead - skipBaselineVerification(); - } - - @Test - public void planner_optional_errors() { - source = "optional.unwrap([dyn(1)])"; - runTest(ImmutableMap.of()); - } - - @Override - public void unknownField() { - // Exercised in planner_unknownFieldAccess instead - skipBaselineVerification(); - } - - @Override - public void unknownResultSet() { - // Exercised in planner_unknownResultSet_success instead - skipBaselineVerification(); - } - - @Test - public void planner_unknownFieldSelection() { - setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); - declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); - - CelAttributePattern patternX = CelAttributePattern.fromQualifiedIdentifier("x"); - - source = "x"; - // We have the full message, but we're claiming that the attribute is unknown. - runTest(ImmutableMap.of("x", TestAllTypes.getDefaultInstance()), patternX); - // A "partially known message". The result is still an unknown. - runTest( - ImmutableMap.of("x", TestAllTypes.getDefaultInstance()), - CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); - - source = "x.single_int32"; - runTest(ImmutableMap.of(), patternX); - runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); - - source = "x.map_int32_int64[22]"; - runTest(ImmutableMap.of(), patternX); - runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.map_int32_int64")); - - source = "x.repeated_nested_message[1]"; - runTest(ImmutableMap.of(), patternX); - runTest( - ImmutableMap.of(), - CelAttributePattern.fromQualifiedIdentifier("x.repeated_nested_message")); - - source = "x.single_nested_message.bb"; - runTest(ImmutableMap.of(), patternX); - runTest( - ImmutableMap.of(), - CelAttributePattern.fromQualifiedIdentifier("x.single_nested_message.bb")); - - source = "{1: x.single_int32}"; - runTest(ImmutableMap.of(), patternX); - runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); - - source = "[1, x.single_int32]"; - runTest(ImmutableMap.of(), patternX); - runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x.single_int32")); - } - - @Test - public void planner_unknownResultSet_success() { - setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())); - declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); - TestAllTypes message = - TestAllTypes.newBuilder() - .setSingleString("test") - .setSingleTimestamp(Timestamp.newBuilder().setSeconds(15)) - .build(); - ImmutableMap variables = ImmutableMap.of("x", message); - CelAttributePattern unknownInt32 = - CelAttributePattern.fromQualifiedIdentifier("x.single_int32"); - CelAttributePattern unknownInt64 = - CelAttributePattern.fromQualifiedIdentifier("x.single_int64"); - - source = "x.single_int32 == 1 && true"; - runTest(variables, unknownInt32); - - source = "x.single_int32 == 1 && false"; - runTest(variables, unknownInt32); - - source = "x.single_int32 == 1 && x.single_int64 == 1"; - runTest(variables, unknownInt32, unknownInt64); - - source = "true && x.single_int32 == 1"; - runTest(variables, unknownInt32); - - source = "false && x.single_int32 == 1"; - runTest(variables, unknownInt32); - - source = "x.single_int32 == 1 || x.single_string == \"test\""; - runTest(variables, unknownInt32); - - source = "x.single_int32 == 1 || x.single_string != \"test\""; - runTest(variables, unknownInt32); - - source = "x.single_int32 == 1 || x.single_int64 == 1"; - runTest(variables, unknownInt32, unknownInt64); - - source = "true || x.single_int32 == 1"; - runTest(variables, unknownInt32); - - source = "false || x.single_int32 == 1"; - runTest(variables, unknownInt32); - - // dispatch test - declareFunction( - "f", memberOverload("f", Arrays.asList(SimpleType.INT, SimpleType.INT), SimpleType.BOOL)); - celRuntime = - newBaseRuntimeBuilder( - CelOptions.current() - .enableHeterogeneousNumericComparisons(true) - .enableOptionalSyntax(true) - .comprehensionMaxIterations(1_000) - .build()) - .addFunctionBindings( - CelFunctionBinding.from("f", Integer.class, Integer.class, Objects::equals)) - .setContainer(CelContainer.ofName(TestAllTypes.getDescriptor().getFile().getPackage())) - .build(); - - source = "x.single_int32.f(1)"; - runTest(variables, unknownInt32); - - source = "1.f(x.single_int32)"; - runTest(variables, unknownInt32); - - source = "x.single_int64.f(x.single_int32)"; - runTest(variables, unknownInt32, unknownInt64); - - source = "[0, 2, 4].exists(z, z == 2 || z == x.single_int32)"; - runTest(variables, unknownInt32); - - source = "[0, 2, 4].exists(z, z == x.single_int32)"; - runTest(variables, unknownInt32); - - source = - "[0, 2, 4].exists_one(z, z == 0 || (z == 2 && z == x.single_int32) " - + "|| (z == 4 && z == x.single_int64))"; - runTest(variables, unknownInt32, unknownInt64); - - source = "[0, 2].all(z, z == 2 || z == x.single_int32)"; - runTest(variables, unknownInt32); - - source = - "[0, 2, 4].filter(z, z == 0 || (z == 2 && z == x.single_int32) " - + "|| (z == 4 && z == x.single_int64))"; - runTest(variables, unknownInt32, unknownInt64); - - source = - "[0, 2, 4].map(z, z == 0 || (z == 2 && z == x.single_int32) " - + "|| (z == 4 && z == x.single_int64))"; - runTest(variables, unknownInt32, unknownInt64); - - source = "x.single_int32 == 1 ? 1 : 2"; - runTest(variables, unknownInt32); - - source = "true ? x.single_int32 : 2"; - runTest(variables, unknownInt32); - - source = "true ? 1 : x.single_int32"; - runTest(variables, unknownInt32); - - source = "false ? x.single_int32 : 2"; - runTest(variables, unknownInt32); - - source = "false ? 1 : x.single_int32"; - runTest(variables, unknownInt32); - - source = "x.single_int64 == 1 ? x.single_int32 : x.single_int32"; - runTest(variables, unknownInt32, unknownInt64); - - source = "{x.single_int32: 2, 3: 4}"; - runTest(variables, unknownInt32); - - source = "{1: x.single_int32, 3: 4}"; - runTest(variables, unknownInt32); - - source = "{1: x.single_int32, x.single_int64: 4}"; - runTest(variables, unknownInt32, unknownInt64); - - source = "[1, x.single_int32, 3, 4]"; - runTest(variables, unknownInt32); - - source = "[1, x.single_int32, x.single_int64, 4]"; - runTest(variables, unknownInt32, unknownInt64); - - source = "TestAllTypes{single_int32: x.single_int32}.single_int32 == 2"; - runTest(variables, unknownInt32); - - source = "TestAllTypes{single_int32: x.single_int32, single_int64: x.single_int64}"; - runTest(variables, unknownInt32, unknownInt64); - - clearAllDeclarations(); - declareVariable("unknown_list", ListType.create(SimpleType.INT)); - source = "unknown_list.map(x, x)"; - runTest(variables, CelAttributePattern.fromQualifiedIdentifier("unknown_list")); - - clearAllDeclarations(); - declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); - source = "cel.bind(x, [1, 2, 3], 1 in x)"; - runTest(variables, CelAttributePattern.fromQualifiedIdentifier("x")); - } - - @Test - public void planner_unknownResultSet_errors() { - declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); - TestAllTypes message = - TestAllTypes.newBuilder() - .setSingleString("test") - .setSingleTimestamp(Timestamp.newBuilder().setSeconds(15)) - .build(); - ImmutableMap variables = ImmutableMap.of("x", message); - CelAttributePattern unknownInt32 = - CelAttributePattern.fromQualifiedIdentifier("x.single_int32"); - - source = "x.single_int32 == 1 && x.single_timestamp <= timestamp(\"bad timestamp string\")"; - runTest(variables, unknownInt32); - - source = "x.single_timestamp <= timestamp(\"bad timestamp string\") && x.single_int32 == 1"; - runTest(variables, unknownInt32); - - source = - "x.single_timestamp <= timestamp(\"bad timestamp string\") " - + "&& x.single_timestamp > timestamp(\"another bad timestamp string\")"; - runTest(variables, unknownInt32); - - source = "x.single_int32 == 1 || x.single_timestamp <= timestamp(\"bad timestamp string\")"; - runTest(variables, unknownInt32); - - source = "x.single_timestamp <= timestamp(\"bad timestamp string\") || x.single_int32 == 1"; - runTest(variables, unknownInt32); - - source = - "x.single_timestamp <= timestamp(\"bad timestamp string\") " - + "|| x.single_timestamp > timestamp(\"another bad timestamp string\")"; - runTest(variables, unknownInt32); - - source = "x"; - runTest(ImmutableMap.of(), CelAttributePattern.fromQualifiedIdentifier("x")); - } } diff --git a/testing.bzl b/testing.bzl index 5425f7719..636ea83c5 100644 --- a/testing.bzl +++ b/testing.bzl @@ -75,7 +75,10 @@ def junit4_test_suites( test_files = srcs or native.glob( ["**/*Test.java"], # TODO: Inspect built JAR and derive the included test files from classpath instead (provided from java_library deps). - exclude = ["**/*AndroidTest.java"], + exclude = [ + "**/*AndroidTest.java", + "**/AbstractPlannerInterpreterTest.java", + ], ) test_classes = [] for src in test_files: From eaf61f7bf22260fe357e05ac5677038545538f5c Mon Sep 17 00:00:00 2001 From: CEL Dev Team Date: Tue, 14 Jul 2026 09:01:41 -0700 Subject: [PATCH 120/204] Add more unit tests for ConstantFoldingOptimizer PiperOrigin-RevId: 947715876 --- .../ConstantFoldingOptimizerTest.java | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java index ee62c5ed1..ec4ffd6bc 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java @@ -94,6 +94,7 @@ private static Cel setupEnv(CelBuilder celBuilder) { .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .setOptions(CEL_OPTIONS) .addCompilerLibraries( + CelExtensions.comprehensions(), CelExtensions.bindings(), CelOptionalLibrary.INSTANCE, CelExtensions.math(CEL_OPTIONS), @@ -228,6 +229,23 @@ private static Cel setupEnv(CelBuilder celBuilder) { @TestParameters("{source: 'true == false', expected: 'false'}") @TestParameters("{source: 'true == true', expected: 'true'}") @TestParameters("{source: 'false == true', expected: 'false'}") + @TestParameters("{source: '[1, 2, 3].map(item, item * 2)', expected: '[2, 4, 6]'}") + @TestParameters("{source: '[1, 2, 3].filter(item, item > 1)', expected: '[2, 3]'}") + @TestParameters("{source: '[1, 2, 3].exists(item, item > 1)', expected: 'true'}") + @TestParameters("{source: '[1, 2, 3].all(item, item > 1)', expected: 'false'}") + @TestParameters("{source: '{\"a\": 1}.all(k, v, k == \"a\" || v > 0)', expected: 'true'}") + @TestParameters( + "{source: '{\"a\": 1, \"b\": x}.all(k, v, k == \"a\" || v > 0)', expected: '{\"a\": 1, \"b\":" + + " x}.all(k, v, k == \"a\" || v > 0)'}") + @TestParameters("{source: '[1, 2].map(x, [3, 4].map(y, x + y))', expected: '[[4, 5], [5, 6]]'}") + @TestParameters( + "{source: '[1, 2, x].map(item, item * 2)', expected: '[1, 2, x].map(item, item * 2)'}") + @TestParameters("{source: '[[1, 2], [3, 4]].map(x, x[0] == 1)', expected: '[true, false]'}") + @TestParameters("{source: '[{\"a\": 1}].map(item, item.a) == [1]', expected: 'true'}") + @TestParameters("{source: '[1].map(item, [1].exists(x, item == x))', expected: '[true]'}") + @TestParameters("{source: '[{\"a\": 1}].map(x, x.a == 1)', expected: '[true]'}") + @TestParameters( + "{source: '[1, 2, x].map(item, item * 2)', expected: '[1, 2, x].map(item, item * 2)'}") @TestParameters("{source: 'false == false', expected: 'true'}") @TestParameters("{source: '10 == 42', expected: 'false'}") @TestParameters("{source: '42 == 42', expected: 'true'}") @@ -561,10 +579,7 @@ public void iterationLimitReached_throws() throws Exception { Cel cel = runtimeFlavor .builder() - .setOptions( - CelOptions.current() - .enableHeterogeneousNumericComparisons(true) - .build()) + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) .build(); CelAbstractSyntaxTree ast = cel.compile("1 + 1").getAst(); CelOptimizer optimizer = From 3ca90e1cc41a89839c7ba2a0b7d88608abda9f90 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 15 Jul 2026 14:42:41 -0700 Subject: [PATCH 121/204] Internal Changes PiperOrigin-RevId: 948554471 --- .../CelComprehensionsExtensions.java | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java index 70402dd03..7391eb16d 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java @@ -48,9 +48,12 @@ public final class CelComprehensionsExtensions private static final String MAP_INSERT_FUNCTION = "cel.@mapInsert"; private static final String MAP_INSERT_OVERLOAD_MAP_MAP = "cel_@mapInsert_map_map"; private static final String MAP_INSERT_OVERLOAD_KEY_VALUE = "cel_@mapInsert_map_key_value"; - private static final TypeParamType TYPE_PARAM_K = TypeParamType.create("K"); - private static final TypeParamType TYPE_PARAM_V = TypeParamType.create("V"); - private static final MapType MAP_KV_TYPE = MapType.create(TYPE_PARAM_K, TYPE_PARAM_V); + + private static final class Types { + private static final TypeParamType TYPE_PARAM_K = TypeParamType.create("K"); + private static final TypeParamType TYPE_PARAM_V = TypeParamType.create("V"); + private static final MapType MAP_KV_TYPE = MapType.create(TYPE_PARAM_K, TYPE_PARAM_V); + } /** Enumeration of functions for Comprehensions extension. */ public enum Function { @@ -60,16 +63,16 @@ public enum Function { CelOverloadDecl.newGlobalOverload( MAP_INSERT_OVERLOAD_MAP_MAP, "Returns a map that's the result of merging given two maps.", - MAP_KV_TYPE, - MAP_KV_TYPE, - MAP_KV_TYPE), + Types.MAP_KV_TYPE, + Types.MAP_KV_TYPE, + Types.MAP_KV_TYPE), CelOverloadDecl.newGlobalOverload( MAP_INSERT_OVERLOAD_KEY_VALUE, "Adds the given key-value pair to the map.", - MAP_KV_TYPE, - MAP_KV_TYPE, - TYPE_PARAM_K, - TYPE_PARAM_V))); + Types.MAP_KV_TYPE, + Types.MAP_KV_TYPE, + Types.TYPE_PARAM_K, + Types.TYPE_PARAM_V))); private final CelFunctionDecl functionDecl; @@ -118,7 +121,7 @@ static CelExtensionLibrary library() { @Override public void setCheckerOptions(CelCheckerBuilder checkerBuilder) { - functions.forEach(function -> checkerBuilder.addFunctionDeclarations(function.functionDecl)); + functions.forEach(function -> checkerBuilder.addFunctionDeclarations(function.functionDecl())); } @Override From e11f8d27847adee7d50e735dd3c31dafab054c50 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 15 Jul 2026 14:52:44 -0700 Subject: [PATCH 122/204] Internal Changes PiperOrigin-RevId: 948559671 --- verifier/BUILD.bazel | 41 + verifier/axioms/BUILD.bazel | 8 + .../main/java/dev/cel/verifier/BUILD.bazel | 123 + .../cel/verifier/CelAstToZ3Translator.java | 1237 ++++++++++ .../dev/cel/verifier/CelPolicyVerifier.java | 30 + .../verifier/CelPolicyVerifierBuilder.java | 22 + .../verifier/CelPolicyVerifierFactory.java | 29 + .../cel/verifier/CelPolicyVerifierImpl.java | 59 + .../verifier/CelVerificationException.java | 26 + .../cel/verifier/CelVerificationResult.java | 53 + .../java/dev/cel/verifier/CelVerifier.java | 46 + .../dev/cel/verifier/CelVerifierBuilder.java | 88 + .../dev/cel/verifier/CelVerifierFactory.java | 27 + .../dev/cel/verifier/CelVerifierZ3Impl.java | 393 +++ .../CelZ3CounterexampleGenerator.java | 252 ++ .../verifier/CelZ3ExtensionalityAxioms.java | 161 ++ .../cel/verifier/CelZ3FunctionRegistry.java | 63 + .../cel/verifier/CelZ3OperatorTranslator.java | 726 ++++++ .../dev/cel/verifier/CelZ3TypeSystem.java | 916 +++++++ .../dev/cel/verifier/TranslatedValue.java | 217 ++ .../dev/cel/verifier/axioms/AddAxiom.java | 100 + .../dev/cel/verifier/axioms/AxiomHelpers.java | 51 + .../java/dev/cel/verifier/axioms/BUILD.bazel | 27 + .../verifier/axioms/CelZ3FunctionAxiom.java | 190 ++ .../verifier/axioms/CelZ3OverloadResult.java | 44 + .../axioms/CelZ3OverloadTranslator.java | 56 + .../verifier/axioms/CelZ3StandardAxioms.java | 48 + .../dev/cel/verifier/axioms/DivideAxiom.java | 61 + .../dev/cel/verifier/axioms/GreaterAxiom.java | 137 ++ .../verifier/axioms/GreaterEqualsAxiom.java | 137 ++ .../java/dev/cel/verifier/axioms/InAxiom.java | 107 + .../dev/cel/verifier/axioms/LessAxiom.java | 137 ++ .../cel/verifier/axioms/LessEqualsAxiom.java | 137 ++ .../cel/verifier/axioms/MapInsertAxiom.java | 82 + .../dev/cel/verifier/axioms/ModuloAxiom.java | 50 + .../cel/verifier/axioms/MultiplyAxiom.java | 62 + .../dev/cel/verifier/axioms/NegateAxiom.java | 46 + .../cel/verifier/axioms/OptionalAxioms.java | 101 + .../dev/cel/verifier/axioms/SizeAxiom.java | 74 + .../dev/cel/verifier/axioms/StringAxioms.java | 66 + .../cel/verifier/axioms/SubtractAxiom.java | 59 + .../dev/cel/verifier/axioms/TypeAxiom.java | 87 + .../verifier/axioms/TypeConversionAxioms.java | 246 ++ .../test/java/dev/cel/verifier/BUILD.bazel | 65 + .../verifier/CelPolicyVerifierImplTest.java | 311 +++ .../cel/verifier/CelVerifierZ3ImplTest.java | 2188 +++++++++++++++++ 46 files changed, 9186 insertions(+) create mode 100644 verifier/BUILD.bazel create mode 100644 verifier/axioms/BUILD.bazel create mode 100644 verifier/src/main/java/dev/cel/verifier/BUILD.bazel create mode 100644 verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java create mode 100644 verifier/src/main/java/dev/cel/verifier/CelPolicyVerifier.java create mode 100644 verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierBuilder.java create mode 100644 verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierFactory.java create mode 100644 verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierImpl.java create mode 100644 verifier/src/main/java/dev/cel/verifier/CelVerificationException.java create mode 100644 verifier/src/main/java/dev/cel/verifier/CelVerificationResult.java create mode 100644 verifier/src/main/java/dev/cel/verifier/CelVerifier.java create mode 100644 verifier/src/main/java/dev/cel/verifier/CelVerifierBuilder.java create mode 100644 verifier/src/main/java/dev/cel/verifier/CelVerifierFactory.java create mode 100644 verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java create mode 100644 verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java create mode 100644 verifier/src/main/java/dev/cel/verifier/CelZ3ExtensionalityAxioms.java create mode 100644 verifier/src/main/java/dev/cel/verifier/CelZ3FunctionRegistry.java create mode 100644 verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java create mode 100644 verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java create mode 100644 verifier/src/main/java/dev/cel/verifier/TranslatedValue.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/AddAxiom.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/AxiomHelpers.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/BUILD.bazel create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/CelZ3FunctionAxiom.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/CelZ3OverloadResult.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/CelZ3OverloadTranslator.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/CelZ3StandardAxioms.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/DivideAxiom.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/GreaterAxiom.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/GreaterEqualsAxiom.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/InAxiom.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/LessAxiom.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/LessEqualsAxiom.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/MapInsertAxiom.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/ModuloAxiom.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/MultiplyAxiom.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/NegateAxiom.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/OptionalAxioms.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/SizeAxiom.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/StringAxioms.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/SubtractAxiom.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/TypeAxiom.java create mode 100644 verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java create mode 100644 verifier/src/test/java/dev/cel/verifier/BUILD.bazel create mode 100644 verifier/src/test/java/dev/cel/verifier/CelPolicyVerifierImplTest.java create mode 100644 verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java diff --git a/verifier/BUILD.bazel b/verifier/BUILD.bazel new file mode 100644 index 000000000..41837d1bc --- /dev/null +++ b/verifier/BUILD.bazel @@ -0,0 +1,41 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//:internal"], +) + +java_library( + name = "verifier", + exports = ["//verifier/src/main/java/dev/cel/verifier"], +) + +java_library( + name = "policy_verifier", + exports = ["//verifier/src/main/java/dev/cel/verifier:policy_verifier"], +) + +java_library( + name = "policy_verifier_factory", + exports = ["//verifier/src/main/java/dev/cel/verifier:policy_verifier_factory"], +) + +java_library( + name = "verifier_factory", + compatible_with = [], + exports = ["//verifier/src/main/java/dev/cel/verifier:verifier_factory"], +) + +java_library( + name = "type_system", + compatible_with = [], + visibility = ["//:internal"], + exports = ["//verifier/src/main/java/dev/cel/verifier:type_system"], +) + +java_library( + name = "z3_impl", + compatible_with = [], + visibility = ["//:internal"], + exports = ["//verifier/src/main/java/dev/cel/verifier:z3_impl"], +) diff --git a/verifier/axioms/BUILD.bazel b/verifier/axioms/BUILD.bazel new file mode 100644 index 000000000..537cfd2ce --- /dev/null +++ b/verifier/axioms/BUILD.bazel @@ -0,0 +1,8 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package(default_visibility = ["//verifier:__subpackages__"]) + +java_library( + name = "axioms", + exports = ["//verifier/src/main/java/dev/cel/verifier/axioms"], +) diff --git a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel new file mode 100644 index 000000000..3f2fb509f --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel @@ -0,0 +1,123 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//verifier:__pkg__"], +) + +java_library( + name = "verifier", + srcs = [ + "CelVerificationException.java", + "CelVerificationResult.java", + "CelVerifier.java", + "CelVerifierBuilder.java", + ], + tags = [ + ], + deps = [ + "//:auto_value", + "//common:cel_ast", + "//common/types:type_providers", + ], +) + +java_library( + name = "verifier_factory", + srcs = ["CelVerifierFactory.java"], + compatible_with = [], + tags = [ + ], + deps = [ + ":verifier", + ":z3_impl", + ], +) + +java_library( + name = "policy_verifier", + srcs = [ + "CelPolicyVerifier.java", + "CelPolicyVerifierBuilder.java", + ], + tags = [ + ], + deps = [ + ":verifier", + "//policy", + "//policy:validation_exception", + ], +) + +java_library( + name = "policy_verifier_factory", + srcs = ["CelPolicyVerifierFactory.java"], + tags = [ + ], + deps = [ + ":policy_verifier", + ":policy_verifier_impl", + ":verifier", + "//policy:compiler", + ], +) + +java_library( + name = "policy_verifier_impl", + srcs = ["CelPolicyVerifierImpl.java"], + tags = [ + ], + deps = [ + ":policy_verifier", + ":verifier", + "//common:cel_ast", + "//policy", + "//policy:compiler", + "//policy:validation_exception", + ], +) + +java_library( + name = "type_system", + srcs = ["CelZ3TypeSystem.java"], + compatible_with = [], + tags = [ + ], + deps = [ + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:tools_aqua_z3_turnkey", + ], +) + +java_library( + name = "z3_impl", + srcs = [ + "CelAstToZ3Translator.java", + "CelVerifierZ3Impl.java", + "CelZ3CounterexampleGenerator.java", + "CelZ3ExtensionalityAxioms.java", + "CelZ3FunctionRegistry.java", + "CelZ3OperatorTranslator.java", + "TranslatedValue.java", + ], + compatible_with = [], + tags = [ + ], + deps = [ + ":type_system", + ":verifier", + "//:auto_value", + "//common:cel_ast", + "//common:compiler_common", + "//common:operator", + "//common/ast", + "//common/ast:cel_block", + "//common/types", + "//common/types:type_providers", + "//verifier/axioms", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:tools_aqua_z3_turnkey", + ], +) diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java new file mode 100644 index 000000000..dc2f377b4 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java @@ -0,0 +1,1237 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.microsoft.z3.ArithExpr; +import com.microsoft.z3.ArrayExpr; +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Context; +import com.microsoft.z3.Expr; +import com.microsoft.z3.FuncDecl; +import com.microsoft.z3.IntExpr; +import com.microsoft.z3.Pattern; +import com.microsoft.z3.Quantifier; +import com.microsoft.z3.SeqExpr; +import com.microsoft.z3.Sort; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.Operator; +import dev.cel.common.ast.CelBlock; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.ast.CelExpr.CelComprehension; +import dev.cel.common.ast.CelExpr.CelList; +import dev.cel.common.ast.CelExpr.ExprKind; +import dev.cel.common.types.CelKind; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.ListType; +import dev.cel.common.types.MapType; +import dev.cel.common.types.NullableType; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.common.types.TypeType; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +/** + * Internal translator from CEL AST to Z3 SMT expressions using a 4-valued logic tagged union. + * + *

Lifetime & Mutability: This translator is stateful and highly mutable. It accumulates + * type constraints, loop variables (via its symbol table), and uninterpreted functions (via field + * caches) during AST traversal. + * + *

    + *
  • It is not thread-safe. + *
  • It is strictly bound to the lifecycle of a single Z3 {@link Context}. + *
  • It should be instantiated once per verification task. + *
  • For equivalence checks involving multiple ASTs, the same translator instance must be + * used to translate all ASTs. This ensures that uninterpreted functions (like field accesses) + * and type constraints are correctly shared and unified across the expressions. + *
+ */ +@SuppressWarnings({"unchecked", "rawtypes"}) // Z3 Java API uses raw types. +final class CelAstToZ3Translator { + private static final String MAP_REF_PREFIX = "!mapRef_"; + private static final String LIST_REF_PREFIX = "!listRef_"; + private static final String MSG_REF_PREFIX = "!msg_ref"; + private static final String EMPTY_MSG_REF_PREFIX = "!empty_msg_ref_"; + private static final String EMPTY_LIST_PREFIX = "!empty_list"; + private static final String EMPTY_MAP_PREFIX = "!empty_map"; + private static final String MAP_BIJECTION_PREFIX = "k_map_bijection"; + private final Context ctx; + private final CelZ3TypeSystem typeSystem; + private final CelZ3OperatorTranslator operatorTranslator; + private final Map symbolTable; + private final Set typeConstraints; + private final ImmutableSet unknownIdentifiers; + private final int comprehensionUnrollLimit; + private final CelTypeProvider typeProvider; + private final List truncationConditions; + private final Map> emptyMessageCache; + private final Map> listLiteralCache; + private Expr emptyListCache; + private Expr emptyMapCache; + + /** + * Returns a set of Z3 boolean expressions representing type constraints for the translated AST. + */ + Set getTypeConstraints() { + return typeConstraints; + } + + BoolExpr hasTruncation() { + return CelZ3TypeSystem.mkOrFlattened(ctx, truncationConditions); + } + + CelZ3TypeSystem getTypeSystem() { + return typeSystem; + } + + /** + * Returns a Z3 boolean expression asserting that the provided CEL value is a boolean and is true. + */ + BoolExpr isTrue(Expr celValue) { + return ctx.mkAnd(typeSystem.isBool(celValue), (BoolExpr) typeSystem.unwrapBool(celValue)); + } + + TranslatedValue translate(CelAbstractSyntaxTree ast) { + TranslatedValue result; + CelBlock celBlock = CelBlock.extract(ast).orElse(null); + if (celBlock != null) { + result = translateBlock(celBlock.indices(), 0, celBlock.result(), ast); + } else { + result = translateExpr(ast.getExpr(), ast); + } + + Set> visited = new LinkedHashSet<>(); + Set> listRefs = new LinkedHashSet<>(); + Set> mapRefs = new LinkedHashSet<>(); + Set> msgRefs = new LinkedHashSet<>(); + + for (Expr constraint : typeConstraints) { + collectReferences(constraint, visited, listRefs, mapRefs, msgRefs); + } + collectReferences(result.z3Expr(), visited, listRefs, mapRefs, msgRefs); + + this.typeConstraints.addAll( + CelZ3ExtensionalityAxioms.generateAxioms(ctx, typeSystem, listRefs, mapRefs, msgRefs)); + + return result; + } + + private void collectReferences( + Expr expr, + Set> visited, + Set> listRefs, + Set> mapRefs, + Set> msgRefs) { + if (!visited.add(expr)) { + return; + } + Sort sort = expr.getSort(); + if (sort.equals(typeSystem.listRefSort())) { + listRefs.add(expr); + } else if (sort.equals(typeSystem.mapRefSort())) { + mapRefs.add(expr); + } else if (sort.equals(typeSystem.messageRefSort())) { + msgRefs.add(expr); + } + + if (expr.isApp()) { + for (Expr arg : expr.getArgs()) { + collectReferences(arg, visited, listRefs, mapRefs, msgRefs); + } + } else if (expr.isQuantifier()) { + collectReferences(((Quantifier) expr).getBody(), visited, listRefs, mapRefs, msgRefs); + } + } + + private TranslatedValue translateExpr(CelExpr celExpr, CelAbstractSyntaxTree ast) { + ExprKind.Kind kind = celExpr.exprKind().getKind(); + + switch (kind) { + case CONSTANT: + return translateConstant(celExpr); + case IDENT: + return translateIdent(celExpr, ast); + case SELECT: + return translateSelect(celExpr, ast); + case CALL: + return translateCall(celExpr, ast); + case LIST: + return translateList(celExpr, ast); + case MAP: + return translateMap(celExpr, ast); + case STRUCT: + return translateStruct(celExpr, ast); + case COMPREHENSION: + return translateComprehension(celExpr, ast); + default: + throw new IllegalArgumentException("Unsupported expression kind: " + kind); + } + } + + private TranslatedValue translateConstant(CelExpr celExpr) { + CelConstant constant = celExpr.constant(); + Expr val; + switch (constant.getKind()) { + case BOOLEAN_VALUE: + val = typeSystem.mkBool(constant.booleanValue()); + break; + case INT64_VALUE: + val = typeSystem.mkInt(constant.int64Value()); + break; + case UINT64_VALUE: + val = typeSystem.mkUint(constant.uint64Value().longValue()); + break; + case DOUBLE_VALUE: + val = typeSystem.mkDouble(constant.doubleValue()); + break; + case STRING_VALUE: + val = typeSystem.mkString(constant.stringValue()); + break; + case BYTES_VALUE: + val = typeSystem.mkBytes(constant.bytesValue().toStringUtf8()); + break; + case NULL_VALUE: + val = typeSystem.mkNull(); + break; + default: + throw new UnsupportedOperationException("Unsupported constant: " + constant.getKind()); + } + return TranslatedValue.create(val, celExpr, typeSystem, ctx.mkFalse()); + } + + private TranslatedValue translateIdent(CelExpr celExpr, CelAbstractSyntaxTree ast) { + CelExpr.CelIdent ident = celExpr.ident(); + long exprId = celExpr.id(); + String name = ident.name(); + CelType type = ast.getTypeOrThrow(exprId); + if (type instanceof TypeType) { + TypeType typeType = (TypeType) type; + if (typeType.type().kind() != CelKind.DYN) { + return TranslatedValue.create( + typeSystem.mkString(typeType.containingTypeName()), celExpr, typeSystem, ctx.mkFalse()); + } + } + TranslatedValue tv = + symbolTable.computeIfAbsent( + name, + n -> { + Expr v = ctx.mkConst(n, typeSystem.celValueSort()); + + // Variables at rest can never be pre-cooked Errors + // Basically prevents error being a counterexample of var == var + typeConstraints.add(ctx.mkNot(typeSystem.isError(v))); + + BoolExpr constraint = createTypeConstraint(v, exprId, ast); + if (unknownIdentifiers.contains(name)) { + typeConstraints.add(ctx.mkOr(typeSystem.isUnknown(v), constraint)); + } else { + typeConstraints.add(ctx.mkNot(typeSystem.isUnknown(v))); + typeConstraints.add(constraint); + } + + return TranslatedValue.create( + v, + CelExpr.newBuilder().setIdent(ident).setId(exprId).build(), + typeSystem, + /* isApproximate= */ ctx.mkFalse()); + }); + + return TranslatedValue.create(tv.z3Expr(), celExpr, typeSystem, ctx.mkFalse()); + } + + private TranslatedValue translateList(CelExpr celExpr, CelAbstractSyntaxTree ast) { + CelList createList = celExpr.list(); + Optional cacheKey = toCacheKey(celExpr); + List elementsTv = new ArrayList<>(); + Expr listRef = cacheKey.map(listLiteralCache::get).orElse(null); + + // Cache list literals containing only constants. If the same list literal appears multiple + // times (e.g., in equality checks like `[1, 2] == [1, 2]`), caching allows us to reuse the + // same Z3 constant. This avoids expensive sequence equality reasoning in Z3 by reducing the + // check to a trivial identity check (e.g., `list_ref_0 == list_ref_0`). + if (listRef == null) { + SeqExpr seq = ctx.mkEmptySeq(ctx.mkSeqSort(typeSystem.celValueSort())); + for (CelExpr element : createList.elements()) { + TranslatedValue elem = translateExpr(element, ast); + elementsTv.add(elem); + + seq = typeSystem.mkConcatSafe(seq, ctx.mkUnit(elem.z3Expr())); + } + listRef = typeSystem.mkListRefConst(LIST_REF_PREFIX); + typeConstraints.add(ctx.mkEq(typeSystem.getSeq(listRef), seq)); + Expr finalListRef = listRef; + cacheKey.ifPresent(key -> listLiteralCache.put(key, finalListRef)); + } + + Expr result = typeSystem.wrapList(listRef); + return TranslatedValue.propagateStrict(ctx, typeSystem, result, celExpr, elementsTv); + } + + private TranslatedValue translateMap(CelExpr celExpr, CelAbstractSyntaxTree ast) { + CelExpr.CelMap createMap = celExpr.map(); + Expr mapRef = typeSystem.mkMapRefConst(MAP_REF_PREFIX); + + ArrayExpr mapValues = ctx.mkConstArray(typeSystem.celValueSort(), typeSystem.mkUnknown()); + ArrayExpr mapPresence = ctx.mkConstArray(typeSystem.celValueSort(), ctx.mkFalse()); + Expr keysSeq = ctx.mkEmptySeq(ctx.mkSeqSort(typeSystem.celValueSort())); + + List elementsTv = new ArrayList<>(); + + for (CelExpr.CelMap.Entry entryAst : createMap.entries()) { + TranslatedValue keyTv = translateExpr(entryAst.key(), ast); + Expr key = keyTv.z3Expr(); + elementsTv.add(keyTv); + TranslatedValue valueTv = translateExpr(entryAst.value(), ast); + Expr value = valueTv.z3Expr(); + elementsTv.add(valueTv); + + BoolExpr keyAlreadyPresent = (BoolExpr) ctx.mkSelect(mapPresence, key); + keysSeq = + ctx.mkITE(keyAlreadyPresent, keysSeq, typeSystem.mkConcatSafe(keysSeq, ctx.mkUnit(key))); + + mapValues = ctx.mkStore(mapValues, key, value); + mapPresence = ctx.mkStore(mapPresence, key, ctx.mkTrue()); + } + + typeConstraints.add(ctx.mkEq(typeSystem.getMapValues(mapRef), mapValues)); + typeConstraints.add(ctx.mkEq(typeSystem.getMapPresence(mapRef), mapPresence)); + typeConstraints.add(ctx.mkEq(typeSystem.getMapKeys(mapRef), keysSeq)); + + Expr result = typeSystem.wrapMap(mapRef); + return TranslatedValue.propagateStrict(ctx, typeSystem, result, celExpr, elementsTv); + } + + private TranslatedValue translateStruct(CelExpr celExpr, CelAbstractSyntaxTree ast) { + CelExpr.CelStruct createStruct = celExpr.struct(); + // Bypass SMT when the struct is empty (return the cached SMT default pointer) + if (createStruct.entries().isEmpty()) { + return TranslatedValue.create( + getDefaultValueForType(StructTypeReference.create(createStruct.messageName())), + celExpr, + typeSystem, + ctx.mkFalse()); + } + + Expr msgRef = typeSystem.mkMessageRefConst(MSG_REF_PREFIX); + + // Initialize the values array with Unknown. Missing fields and explicit primitive defaults + // will both bypass the store and fall back to this Unknown base. However, their msgPresence + // will evaluate to false, allowing translateSelect to properly return the default value. + ArrayExpr msgValues = ctx.mkConstArray(ctx.getStringSort(), typeSystem.mkUnknown()); + ArrayExpr msgPresence = ctx.mkConstArray(ctx.getStringSort(), ctx.mkFalse()); + + List elementsTv = new ArrayList<>(); + + for (CelExpr.CelStruct.Entry entryAst : createStruct.entries()) { + Expr key = ctx.mkString(entryAst.fieldKey()); + TranslatedValue valueTv = translateExpr(entryAst.value(), ast); + Expr value = valueTv.z3Expr(); + elementsTv.add(valueTv); + + CelType fieldType = + typeProvider + .findType(createStruct.messageName()) + .filter(t -> t instanceof StructType) + .map(t -> (StructType) t) + .flatMap(t -> t.findField(entryAst.fieldKey())) + .map(StructType.Field::type) + .orElseGet(() -> extractAstTypeOrDefault(ast, entryAst.value().id())); + Expr defaultVal = getDefaultValueForType(fieldType); + + // Canonicalization Trick: + // + // We avoid storing explicit default values (e.g. `single_int32: 0`) + // in `msgValues`, leaving them as `Unknown` (identical to missing fields). + // This structurally aligns defaults with missing fields, allowing Z3's native array equality + // (`msg1 == msg2`) to work without using quantifiers (which avoids MBQI loops). + // Because proto3 singular primitives do not have field presence, we also skip setting + // `msgPresence`. + BoolExpr shouldBypass = + fieldType.kind().isPrimitive() ? ctx.mkEq(value, defaultVal) : ctx.mkFalse(); + + msgValues = + (ArrayExpr) ctx.mkITE(shouldBypass, msgValues, ctx.mkStore(msgValues, key, value)); + + msgPresence = + (ArrayExpr) + ctx.mkITE(shouldBypass, msgPresence, ctx.mkStore(msgPresence, key, ctx.mkTrue())); + } + + typeConstraints.add( + ctx.mkEq(typeSystem.getMsgTypeName(msgRef), ctx.mkString(createStruct.messageName()))); + typeConstraints.add(ctx.mkEq(typeSystem.getMsgValues(msgRef), msgValues)); + typeConstraints.add(ctx.mkEq(typeSystem.getMsgPresence(msgRef), msgPresence)); + + Expr result = typeSystem.wrapMessage(msgRef); + return TranslatedValue.propagateStrict(ctx, typeSystem, result, celExpr, elementsTv); + } + + private Expr getDefaultValueForType(CelType type) { + if (type instanceof NullableType) { + return typeSystem.mkNull(); + } + if (type.equals(SimpleType.INT)) { + return typeSystem.mkInt(0); + } + if (type.equals(SimpleType.BOOL)) { + return typeSystem.wrapBool(ctx.mkFalse()); + } + if (type.equals(SimpleType.STRING)) { + return typeSystem.mkString(""); + } + if (type.equals(SimpleType.BYTES)) { + return typeSystem.mkBytes(""); + } + if (type.equals(SimpleType.DOUBLE)) { + return typeSystem.mkDouble(0.0); + } + if (type.equals(SimpleType.UINT)) { + return typeSystem.mkUint(0); + } + if (type instanceof ListType) { + if (emptyListCache == null) { + emptyListCache = typeSystem.mkListRefConst(EMPTY_LIST_PREFIX); + typeConstraints.add( + ctx.mkEq( + typeSystem.getSeq(emptyListCache), + ctx.mkEmptySeq(ctx.mkSeqSort(typeSystem.celValueSort())))); + } + return typeSystem.wrapList(emptyListCache); + } + if (type instanceof MapType) { + if (emptyMapCache == null) { + emptyMapCache = typeSystem.mkMapRefConst(EMPTY_MAP_PREFIX); + typeConstraints.add( + ctx.mkEq( + typeSystem.getMapPresence(emptyMapCache), + ctx.mkConstArray(typeSystem.celValueSort(), ctx.mkFalse()))); + typeConstraints.add( + ctx.mkEq( + typeSystem.getMapValues(emptyMapCache), + ctx.mkConstArray(typeSystem.celValueSort(), typeSystem.mkUnknown()))); + typeConstraints.add( + ctx.mkEq( + typeSystem.getMapKeys(emptyMapCache), + ctx.mkEmptySeq(ctx.mkSeqSort(typeSystem.celValueSort())))); + } + return typeSystem.wrapMap(emptyMapCache); + } + if (type.kind() == CelKind.STRUCT) { + String messageName = type.name(); + Expr msgRef = + emptyMessageCache.computeIfAbsent( + messageName, + name -> { + Expr ref = typeSystem.mkMessageRefConst(EMPTY_MSG_REF_PREFIX + name); + typeConstraints.add( + ctx.mkEq( + typeSystem.getMsgPresence(ref), + ctx.mkConstArray(ctx.getStringSort(), ctx.mkFalse()))); + typeConstraints.add( + ctx.mkEq( + typeSystem.getMsgValues(ref), + ctx.mkConstArray(ctx.getStringSort(), typeSystem.mkUnknown()))); + typeConstraints.add(ctx.mkEq(typeSystem.getMsgTypeName(ref), ctx.mkString(name))); + return ref; + }); + return typeSystem.wrapMessage(msgRef); + } + return typeSystem.mkUnknown(); + } + + private TranslatedValue translateSelect(CelExpr celExpr, CelAbstractSyntaxTree ast) { + CelExpr.CelSelect select = celExpr.select(); + long exprId = celExpr.id(); + TranslatedValue operandTv = translateExpr(select.operand(), ast); + Expr operand = operandTv.z3Expr(); + String field = select.field(); + CelType operandType = extractAstTypeOrDefault(ast, select.operand().id()); + + Expr presenceResult; + Expr valueResult; + + if (operandType instanceof MapType) { + Expr mapFieldZ3Str = typeSystem.mkString(field); + Expr mapRef = typeSystem.getMapRef(operand); + ArrayExpr mapPresence = (ArrayExpr) typeSystem.getMapPresence(mapRef); + ArrayExpr mapValues = (ArrayExpr) typeSystem.getMapValues(mapRef); + Expr inMap = ctx.mkSelect(mapPresence, mapFieldZ3Str); + Expr mapVal = ctx.mkSelect(mapValues, mapFieldZ3Str); + + BoolExpr valNotError = ctx.mkNot(ctx.mkEq(mapVal, typeSystem.mkError())); + typeConstraints.add(ctx.mkImplies((BoolExpr) inMap, valNotError)); + if (unknownIdentifiers.isEmpty()) { + BoolExpr valNotUnknown = ctx.mkNot(ctx.mkEq(mapVal, typeSystem.mkUnknown())); + typeConstraints.add(ctx.mkImplies((BoolExpr) inMap, valNotUnknown)); + } + + presenceResult = inMap; + valueResult = ctx.mkITE((BoolExpr) inMap, mapVal, typeSystem.mkError()); + } else if (operandType.kind() == CelKind.STRUCT) { + Expr msgFieldZ3Str = ctx.mkString(field); + Expr msgRef = typeSystem.getMessageRef(operand); + ArrayExpr msgPresence = (ArrayExpr) typeSystem.getMsgPresence(msgRef); + ArrayExpr msgValues = (ArrayExpr) typeSystem.getMsgValues(msgRef); + Expr inMsg = ctx.mkSelect(msgPresence, msgFieldZ3Str); + Expr msgVal = ctx.mkSelect(msgValues, msgFieldZ3Str); + + presenceResult = inMsg; + Expr defaultVal = getDefaultValueForType(extractAstTypeOrDefault(ast, exprId)); + valueResult = ctx.mkITE((BoolExpr) inMsg, msgVal, defaultVal); + } else { + // Dynamic type: generate the full SMT decision tree + Expr mapFieldZ3Str = typeSystem.mkString(field); + Expr mapRef = typeSystem.getMapRef(operand); + ArrayExpr mapPresence = (ArrayExpr) typeSystem.getMapPresence(mapRef); + ArrayExpr mapValues = (ArrayExpr) typeSystem.getMapValues(mapRef); + Expr inMap = ctx.mkSelect(mapPresence, mapFieldZ3Str); + Expr mapVal = ctx.mkSelect(mapValues, mapFieldZ3Str); + + Expr msgFieldZ3Str = ctx.mkString(field); + Expr msgRef = typeSystem.getMessageRef(operand); + ArrayExpr msgPresence = (ArrayExpr) typeSystem.getMsgPresence(msgRef); + ArrayExpr msgValues = (ArrayExpr) typeSystem.getMsgValues(msgRef); + Expr inMsg = ctx.mkSelect(msgPresence, msgFieldZ3Str); + Expr msgVal = ctx.mkSelect(msgValues, msgFieldZ3Str); + + BoolExpr isMap = typeSystem.isMap(operand); + BoolExpr isMessage = typeSystem.isMessage(operand); + + BoolExpr mapValNotError = ctx.mkNot(ctx.mkEq(mapVal, typeSystem.mkError())); + typeConstraints.add(ctx.mkImplies(ctx.mkAnd(isMap, (BoolExpr) inMap), mapValNotError)); + if (unknownIdentifiers.isEmpty()) { + BoolExpr mapValNotUnknown = ctx.mkNot(ctx.mkEq(mapVal, typeSystem.mkUnknown())); + typeConstraints.add(ctx.mkImplies(ctx.mkAnd(isMap, (BoolExpr) inMap), mapValNotUnknown)); + } + + BoolExpr msgValNotError = ctx.mkNot(ctx.mkEq(msgVal, typeSystem.mkError())); + typeConstraints.add(ctx.mkImplies(ctx.mkAnd(isMessage, (BoolExpr) inMsg), msgValNotError)); + if (unknownIdentifiers.isEmpty()) { + BoolExpr msgValNotUnknown = ctx.mkNot(ctx.mkEq(msgVal, typeSystem.mkUnknown())); + typeConstraints.add( + ctx.mkImplies(ctx.mkAnd(isMessage, (BoolExpr) inMsg), msgValNotUnknown)); + } + + presenceResult = + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(isMessage, inMsg) + .addCase(isMap, inMap) + .build(ctx.mkFalse()); + + Expr defaultVal = getDefaultValueForType(extractAstTypeOrDefault(ast, exprId)); + Expr msgRead = ctx.mkITE((BoolExpr) inMsg, msgVal, defaultVal); + Expr mapRead = ctx.mkITE((BoolExpr) inMap, mapVal, typeSystem.mkError()); + + valueResult = + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(isMessage, msgRead) + .addCase(isMap, mapRead) + .build(typeSystem.mkError()); + } + + Expr fieldAccess = select.testOnly() ? typeSystem.wrapBool(presenceResult) : valueResult; + + typeConstraints.add(createTypeConstraint(fieldAccess, exprId, ast)); + + return TranslatedValue.propagateStrict( + ctx, typeSystem, fieldAccess, celExpr, Arrays.asList(operandTv)); + } + + private TranslatedValue translateBlock( + List boundExprs, int currentIndex, CelExpr resultExpr, CelAbstractSyntaxTree ast) { + if (currentIndex >= boundExprs.size()) { + return translateExpr(resultExpr, ast); + } + + CelExpr subExprAst = boundExprs.get(currentIndex); + TranslatedValue subExprVal = translateExpr(subExprAst, ast); + String varName = CelBlock.INDEX_PREFIX + currentIndex; + + return withScope( + varName, subExprVal, () -> translateBlock(boundExprs, currentIndex + 1, resultExpr, ast)); + } + + private TranslatedValue translateCall(CelExpr expr, CelAbstractSyntaxTree ast) { + CelExpr.CelCall call = expr.call(); + long exprId = expr.id(); + String functionName = call.function(); + ImmutableList argsAst = call.args(); + + List args = new ArrayList<>(); + + if (call.target().isPresent()) { + CelExpr targetAst = call.target().get(); + args.add(translateExpr(targetAst, ast)); + } + + for (CelExpr argAst : argsAst) { + args.add(translateExpr(argAst, ast)); + } + + return operatorTranslator + .translateFunctionCall(functionName, args, exprId, ast) + .map(tv -> TranslatedValue.create(tv.z3Expr(), expr, typeSystem, tv.isApproximate())) + .orElseGet( + () -> { + // Uninterpreted function + Sort[] argSorts = new Sort[args.size()]; + Arrays.fill(argSorts, 0, args.size(), typeSystem.celValueSort()); + FuncDecl funcDecl = + typeSystem.internFuncDecl(functionName, argSorts, typeSystem.celValueSort()); + + Expr[] exprArgs = args.stream().map(TranslatedValue::z3Expr).toArray(Expr[]::new); + Expr callRes = ctx.mkApp(funcDecl, exprArgs); + typeConstraints.add(createTypeConstraint(callRes, exprId, ast)); + typeConstraints.add(ctx.mkNot(typeSystem.isUnknown(callRes))); + typeConstraints.add(ctx.mkNot(typeSystem.isError(callRes))); + + return TranslatedValue.propagateStrict( + ctx, typeSystem, callRes, Optional.of(expr), ctx.mkTrue(), args); + }); + } + + private T withScope(String varName, TranslatedValue value, Supplier action) { + TranslatedValue prev = symbolTable.put(varName, value); + try { + return action.get(); + } finally { + if (prev != null) { + symbolTable.put(varName, prev); + } else { + symbolTable.remove(varName); + } + } + } + + private TranslatedValue translateComprehension(CelExpr celExpr, CelAbstractSyntaxTree ast) { + CelComprehension comp = celExpr.comprehension(); + CelExpr iterRangeExpr = comp.iterRange(); + List iterationElements = new ArrayList<>(); + List taints = new ArrayList<>(); + List> allRangeElems = new ArrayList<>(); + + // For statically known list/map literals, unroll them exactly. + if (iterRangeExpr.exprKind().getKind() == ExprKind.Kind.LIST) { + ImmutableList elements = iterRangeExpr.list().elements(); + for (int i = 0; i < elements.size(); i++) { + TranslatedValue valueTv = translateExpr(elements.get(i), ast); + Expr value = valueTv.z3Expr(); + taints.add(valueTv.isApproximate()); + iterationElements.add(new IterationElement(typeSystem.mkInt(i), value)); + allRangeElems.add(value); + } + } else if (iterRangeExpr.exprKind().getKind() == ExprKind.Kind.MAP) { + for (CelExpr.CelMap.Entry entry : iterRangeExpr.map().entries()) { + TranslatedValue keyTv = translateExpr(entry.key(), ast); + Expr key = keyTv.z3Expr(); + taints.add(keyTv.isApproximate()); + TranslatedValue valueTv = translateExpr(entry.value(), ast); + Expr value = valueTv.z3Expr(); + taints.add(valueTv.isApproximate()); + iterationElements.add(new IterationElement(key, value)); + allRangeElems.add(key); + allRangeElems.add(value); + } + } else { + return translateDynamicComprehension(celExpr, ast); + } + + TranslatedValue accuTv = translateExpr(comp.accuInit(), ast); + Expr accu = accuTv.z3Expr(); + taints.add(accuTv.isApproximate()); + boolean isMap = iterRangeExpr.exprKind().getKind() == ExprKind.Kind.MAP; + boolean isTwoVar = !comp.iterVar2().isEmpty(); + + for (IterationElement iterElem : iterationElements) { + Expr currentAccu = accu; + TranslatedValue[] condAndStep = + evaluateLoopCondAndStep( + comp, ast, iterElem.keyOrIndex, iterElem.value, currentAccu, isMap, isTwoVar); + Expr condition = condAndStep[0].z3Expr(); + Expr step = condAndStep[1].z3Expr(); + taints.add(condAndStep[1].isApproximate()); + + Expr stepVal = ctx.mkITE((BoolExpr) typeSystem.unwrapBool(condition), step, currentAccu); + Expr typeErrorOrStep = + typeSystem.withRuntimeError(stepVal, ctx.mkNot(typeSystem.isBool(condition))); + + accu = typeSystem.propagateErrorAndUnknown(typeErrorOrStep, condition); + } + + TranslatedValue resultTv = + withScope( + comp.accuVar(), + TranslatedValue.create(accu, typeSystem, ctx.mkFalse()), + () -> translateExpr(comp.result(), ast)); + taints.add(resultTv.isApproximate()); + Expr result = resultTv.z3Expr(); + return TranslatedValue.create( + typeSystem.propagateErrorAndUnknown(result, allRangeElems), + celExpr, + typeSystem, + CelZ3TypeSystem.mkOrFlattened(ctx, taints)); + } + + private static CelType extractAstTypeOrDefault(CelAbstractSyntaxTree ast, long id) { + return ast.getType(id).orElse(SimpleType.DYN); + } + + private static final class BoundedIteration { + final BoolExpr inBounds; + final TranslatedValue stepResult; + + BoundedIteration(BoolExpr inBounds, TranslatedValue stepResult) { + this.inBounds = inBounds; + this.stepResult = stepResult; + } + } + + private TranslatedValue translateDynamicComprehension( + CelExpr celExpr, CelAbstractSyntaxTree ast) { + CelComprehension comp = celExpr.comprehension(); + TranslatedValue iterRangeTv = translateExpr(comp.iterRange(), ast); + Expr iterRange = iterRangeTv.z3Expr(); + CelType rangeType = extractAstTypeOrDefault(ast, comp.iterRange().id()); + + boolean isList = rangeType instanceof ListType; + boolean isMap = rangeType instanceof MapType; + if (!isList && !isMap) { + BoolExpr isRuntimeListOrMap = + ctx.mkOr(typeSystem.isList(iterRange), typeSystem.isMap(iterRange)); + Expr result = ctx.mkITE(isRuntimeListOrMap, typeSystem.mkUnknown(), typeSystem.mkError()); + return TranslatedValue.create(result, celExpr, typeSystem, isRuntimeListOrMap); + } + + SeqExpr seq = + isMap + ? typeSystem.getMapKeys(typeSystem.getMapRef(iterRange)) + : typeSystem.getSeq(typeSystem.getListRef(iterRange)); + + ArithExpr lengthExpr = ctx.mkLength(seq); + ArrayExpr mapPresence = + isMap ? (ArrayExpr) typeSystem.getMapPresence(typeSystem.getMapRef(iterRange)) : null; + + if (isMap) { + applyBoundedMapBijection(mapPresence, seq, lengthExpr); + } + + BoolExpr isTruncated = ctx.mkGt(lengthExpr, ctx.mkInt(comprehensionUnrollLimit)); + truncationConditions.add(isTruncated); + + if (isAllMacro(comp) || isExistsMacro(comp)) { + return unrollAllAndExists( + celExpr, ast, seq, lengthExpr, mapPresence, isTruncated, iterRangeTv); + } else { + return unrollMapAndFilter( + celExpr, ast, seq, lengthExpr, mapPresence, isTruncated, iterRangeTv); + } + } + + private void applyBoundedMapBijection( + ArrayExpr mapPresence, SeqExpr seq, ArithExpr lengthExpr) { + for (int i = 0; i < comprehensionUnrollLimit; i++) { + for (int j = i + 1; j < comprehensionUnrollLimit; j++) { + BoolExpr validPair = ctx.mkLt(ctx.mkInt(j), lengthExpr); + BoolExpr notEqual = + ctx.mkNot(ctx.mkEq(ctx.mkNth(seq, ctx.mkInt(i)), ctx.mkNth(seq, ctx.mkInt(j)))); + typeConstraints.add(ctx.mkImplies(validPair, notEqual)); + } + } + + Expr kVar = ctx.mkFreshConst(MAP_BIJECTION_PREFIX, typeSystem.celValueSort()); + BoolExpr isValidKey = + ctx.mkOr( + typeSystem.isInt(kVar), typeSystem.isUint(kVar), + typeSystem.isBool(kVar), typeSystem.isString(kVar)); + BoolExpr inMap = (BoolExpr) ctx.mkSelect(mapPresence, kVar); + + List inSeqMatches = new ArrayList<>(); + for (int i = 0; i < comprehensionUnrollLimit; i++) { + BoolExpr match = + ctx.mkAnd( + ctx.mkLt(ctx.mkInt(i), lengthExpr), ctx.mkEq(kVar, ctx.mkNth(seq, ctx.mkInt(i)))); + inSeqMatches.add(match); + } + BoolExpr inSeq = CelZ3TypeSystem.mkOrFlattened(ctx, inSeqMatches); + + BoolExpr isNotTruncated = ctx.mkLe(lengthExpr, ctx.mkInt(comprehensionUnrollLimit)); + + Pattern inMapPattern = ctx.mkPattern(inMap); + + BoolExpr completeness = + ctx.mkForall( + new Expr[] {kVar}, + ctx.mkImplies(ctx.mkAnd(isNotTruncated, isValidKey, inMap), inSeq), + 1, + new Pattern[] {inMapPattern}, + null, + null, + null); + typeConstraints.add(completeness); + } + + private TranslatedValue[] evaluateLoopCondAndStep( + CelComprehension comp, + CelAbstractSyntaxTree ast, + Expr keyOrIndex, + Expr value, + Expr currentAccu, + boolean isMap, + boolean isTwoVar) { + Supplier evalBody = + () -> + new TranslatedValue[] { + translateExpr(comp.loopCondition(), ast), translateExpr(comp.loopStep(), ast) + }; + + Supplier bindAccu = + () -> + withScope( + comp.accuVar(), + TranslatedValue.create(currentAccu, typeSystem, ctx.mkFalse()), + evalBody); + + if (isTwoVar) { + return withScope( + comp.iterVar(), + TranslatedValue.create(keyOrIndex, typeSystem, ctx.mkFalse()), + () -> + withScope( + comp.iterVar2(), + TranslatedValue.create(value, typeSystem, ctx.mkFalse()), + bindAccu)); + } else { + Expr iterVal = isMap ? keyOrIndex : value; + return withScope( + comp.iterVar(), TranslatedValue.create(iterVal, typeSystem, ctx.mkFalse()), bindAccu); + } + } + + private IterationElement getIterationElement( + Expr xVal, IntExpr idx, Expr iterRange, boolean isMap, boolean isTwoVar) { + Expr keyOrIndex; + Expr value; + + if (isMap) { + keyOrIndex = xVal; + if (isTwoVar) { + Expr mapRef = typeSystem.getMapRef(iterRange); + ArrayExpr mapValues = (ArrayExpr) typeSystem.getMapValues(mapRef); + value = ctx.mkSelect(mapValues, keyOrIndex); + } else { + value = null; + } + } else { + if (isTwoVar) { + keyOrIndex = typeSystem.wrapInt(idx); + value = xVal; + } else { + keyOrIndex = null; + value = xVal; + } + } + return new IterationElement(keyOrIndex, value); + } + + private TranslatedValue unrollAllAndExists( + CelExpr celExpr, + CelAbstractSyntaxTree ast, + SeqExpr seq, + ArithExpr lengthExpr, + ArrayExpr mapPresence, + BoolExpr isTruncated, + TranslatedValue iterRangeTv) { + CelComprehension comp = celExpr.comprehension(); + List iterations = new ArrayList<>(); + TranslatedValue accuInitTv = translateExpr(comp.accuInit(), ast); + Expr accuInitExpr = accuInitTv.z3Expr(); + + boolean isMap = mapPresence != null; + boolean isTwoVar = !comp.iterVar2().isEmpty(); + + for (int i = 0; i < comprehensionUnrollLimit; i++) { + IntExpr idx = ctx.mkInt(i); + BoolExpr inBounds = ctx.mkLt(idx, lengthExpr); + Expr xVal = ctx.mkNth(seq, idx); + + constrainIterationElement(idx, lengthExpr, xVal, inBounds, mapPresence); + + IterationElement iterElem = + getIterationElement(xVal, idx, iterRangeTv.z3Expr(), isMap, isTwoVar); + + TranslatedValue[] condAndStep = + evaluateLoopCondAndStep( + comp, ast, iterElem.keyOrIndex, iterElem.value, accuInitExpr, isMap, isTwoVar); + iterations.add(new BoundedIteration(inBounds, condAndStep[1])); + } + + TranslatedValue reducedTv = + reduceAllOrExists(iterations, isTruncated, iterRangeTv, isAllMacro(comp)); + return TranslatedValue.create( + typeSystem.propagateErrorAndUnknown(reducedTv.z3Expr(), iterRangeTv.z3Expr()), + celExpr, + typeSystem, + reducedTv.isApproximate()); + } + + private TranslatedValue unrollMapAndFilter( + CelExpr celExpr, + CelAbstractSyntaxTree ast, + SeqExpr seq, + ArithExpr lengthExpr, + ArrayExpr mapPresence, + BoolExpr isTruncated, + TranslatedValue iterRangeTv) { + CelComprehension comp = celExpr.comprehension(); + // For macros like map and filter, we must sequentially thread the accumulator through the loop + TranslatedValue chainedAccuTv = translateExpr(comp.accuInit(), ast); + Expr chainedAccu = chainedAccuTv.z3Expr(); + + boolean isMap = mapPresence != null; + boolean isTwoVar = !comp.iterVar2().isEmpty(); + + List brokeConds = new ArrayList<>(); + List taints = new ArrayList<>(); + taints.add(chainedAccuTv.isApproximate()); + taints.add(iterRangeTv.isApproximate()); + taints.add(isTruncated); + + for (int i = 0; i < comprehensionUnrollLimit; i++) { + IntExpr idx = ctx.mkInt(i); + BoolExpr inBounds = ctx.mkLt(idx, lengthExpr); + Expr xVal = ctx.mkNth(seq, idx); + + constrainIterationElement(idx, lengthExpr, xVal, inBounds, mapPresence); + + Expr currentAccu = chainedAccu; + BoolExpr currentHasBroken = CelZ3TypeSystem.mkOrFlattened(ctx, brokeConds); + + IterationElement iterElem = + getIterationElement(xVal, idx, iterRangeTv.z3Expr(), isMap, isTwoVar); + + TranslatedValue[] condAndStep = + evaluateLoopCondAndStep( + comp, ast, iterElem.keyOrIndex, iterElem.value, currentAccu, isMap, isTwoVar); + Expr condExpr = condAndStep[0].z3Expr(); + Expr stepExpr = condAndStep[1].z3Expr(); + + BoolExpr condIsBool = typeSystem.isBool(condExpr); + BoolExpr condIsTrue = ctx.mkAnd(condIsBool, (BoolExpr) typeSystem.unwrapBool(condExpr)); + BoolExpr condIsNotTrue = ctx.mkNot(condIsTrue); + + BoolExpr isActive = ctx.mkAnd(inBounds, ctx.mkNot(currentHasBroken)); + + Expr stepVal = + ctx.mkITE((BoolExpr) typeSystem.unwrapBool(condExpr), stepExpr, currentAccu); + Expr typeErrorOrStep = typeSystem.withRuntimeError(stepVal, ctx.mkNot(condIsBool)); + taints.add(condAndStep[1].isApproximate()); + + chainedAccu = + ctx.mkITE( + isActive, + typeSystem.propagateErrorAndUnknown(typeErrorOrStep, condExpr), + currentAccu); + + brokeConds.add(ctx.mkAnd(inBounds, condIsNotTrue)); + } + + TranslatedValue resultTv = + withScope( + comp.accuVar(), + TranslatedValue.create(chainedAccu, typeSystem, ctx.mkFalse()), + () -> translateExpr(comp.result(), ast)); + + taints.add(resultTv.isApproximate()); + + return TranslatedValue.create( + typeSystem.propagateErrorAndUnknown( + ctx.mkITE(isTruncated, typeSystem.mkUnknown(), resultTv.z3Expr()), + iterRangeTv.z3Expr()), + celExpr, + typeSystem, + CelZ3TypeSystem.mkOrFlattened(ctx, taints)); + } + + private void constrainIterationElement( + IntExpr idx, ArithExpr lengthExpr, Expr xVal, BoolExpr inBounds, ArrayExpr mapPresence) { + // Constrain out-of-bounds elements to prevent MBQI from infinitely instantiating list/map + // axioms + BoolExpr outOfBounds = ctx.mkGe(idx, lengthExpr); + typeConstraints.add(ctx.mkImplies(outOfBounds, ctx.mkEq(xVal, typeSystem.mkUnknown()))); + + // If we are iterating over a map, we can optionally bind the sequence elements + // to the map presence array to ensure the solver knows they correspond. + if (mapPresence != null) { + Expr selectExpr = ctx.mkSelect(mapPresence, xVal); + typeConstraints.add(ctx.mkImplies(inBounds, (BoolExpr) selectExpr)); + } + } + + private TranslatedValue reduceAllOrExists( + List iterations, + BoolExpr isTruncated, + TranslatedValue iterRangeTv, + boolean isAll) { + List hasMatchList = new ArrayList<>(); + List hasErrorList = new ArrayList<>(); + List hasUnknownList = new ArrayList<>(); + + List hasSafeMatchList = new ArrayList<>(); + List hasSafeErrorList = new ArrayList<>(); + List hasSafeUnknownList = new ArrayList<>(); + List activeTaints = new ArrayList<>(); + + for (BoundedIteration iter : iterations) { + TranslatedValue stepTv = iter.stepResult; + Expr step = stepTv.z3Expr(); + BoolExpr isBool = typeSystem.isBool(step); + BoolExpr unwrapStep = (BoolExpr) typeSystem.unwrapBool(step); + + // 'all' short-circuits on False, 'exists' short-circuits on True + BoolExpr matchCond = isAll ? ctx.mkNot(unwrapStep) : unwrapStep; + BoolExpr isMatch = ctx.mkAnd(isBool, matchCond); + + BoolExpr isU = typeSystem.isUnknown(step); + BoolExpr isE = + ctx.mkOr(typeSystem.isError(step), ctx.mkAnd(ctx.mkNot(isBool), ctx.mkNot(isU))); + + BoolExpr isActive = iter.inBounds; + + hasMatchList.add(CelZ3TypeSystem.mkAndFlattened(ctx, Arrays.asList(isActive, isMatch))); + hasErrorList.add(CelZ3TypeSystem.mkAndFlattened(ctx, Arrays.asList(isActive, isE))); + hasUnknownList.add(CelZ3TypeSystem.mkAndFlattened(ctx, Arrays.asList(isActive, isU))); + + hasSafeMatchList.add( + CelZ3TypeSystem.mkAndFlattened( + ctx, + Arrays.asList( + isActive, isMatch, CelZ3TypeSystem.mkNotFlattened(ctx, stepTv.isApproximate())))); + hasSafeErrorList.add( + CelZ3TypeSystem.mkAndFlattened( + ctx, + Arrays.asList( + isActive, isE, CelZ3TypeSystem.mkNotFlattened(ctx, stepTv.isApproximate())))); + hasSafeUnknownList.add( + CelZ3TypeSystem.mkAndFlattened( + ctx, + Arrays.asList( + isActive, isU, CelZ3TypeSystem.mkNotFlattened(ctx, stepTv.isApproximate())))); + activeTaints.add( + CelZ3TypeSystem.mkAndFlattened(ctx, Arrays.asList(isActive, stepTv.isApproximate()))); + } + + BoolExpr hasMatch = CelZ3TypeSystem.mkOrFlattened(ctx, hasMatchList); + BoolExpr hasError = CelZ3TypeSystem.mkOrFlattened(ctx, hasErrorList); + BoolExpr hasUnknown = CelZ3TypeSystem.mkOrFlattened(ctx, hasUnknownList); + + BoolExpr hasSafeMatch = CelZ3TypeSystem.mkOrFlattened(ctx, hasSafeMatchList); + BoolExpr hasSafeError = CelZ3TypeSystem.mkOrFlattened(ctx, hasSafeErrorList); + BoolExpr hasSafeUnknown = CelZ3TypeSystem.mkOrFlattened(ctx, hasSafeUnknownList); + BoolExpr anyActiveTaint = CelZ3TypeSystem.mkOrFlattened(ctx, activeTaints); + + Expr result = + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(hasMatch, typeSystem.mkBool(!isAll)) + .addCase(ctx.mkOr(hasUnknown, isTruncated), typeSystem.mkUnknown()) + .addCase(hasError, typeSystem.mkError()) + .build(typeSystem.mkBool(isAll)); + + BoolExpr baseTaint = + CelZ3TypeSystem.mkOrFlattened( + ctx, Arrays.asList(anyActiveTaint, iterRangeTv.isApproximate())); + + // Taint identically shadows the value flow's short-circuit control structure + BoolExpr resultTaint = + (BoolExpr) + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(hasMatch, ctx.mkNot(hasSafeMatch)) + .addCase( + ctx.mkOr(hasUnknown, isTruncated), + ctx.mkOr(isTruncated, ctx.mkNot(hasSafeUnknown))) + .addCase(hasError, ctx.mkNot(hasSafeError)) + .build(baseTaint); + + return TranslatedValue.create(result, typeSystem, resultTaint); + } + + private static boolean isAllMacro(CelComprehension comp) { + return isBooleanAccuInit(comp, true) && isNotStrictlyFalseLoopCondition(comp); + } + + private static boolean isExistsMacro(CelComprehension comp) { + return isBooleanAccuInit(comp, false) && isNotStrictlyFalseLoopCondition(comp); + } + + private static boolean isBooleanAccuInit(CelComprehension comp, boolean expectedValue) { + return comp.accuInit().constantOrDefault().getKind() == CelConstant.Kind.BOOLEAN_VALUE + && comp.accuInit().constant().booleanValue() == expectedValue; + } + + private static boolean isNotStrictlyFalseLoopCondition(CelComprehension comp) { + CelExpr.CelCall call = comp.loopCondition().callOrDefault(); + return (call.function().equals(Operator.NOT_STRICTLY_FALSE.getFunction()) + || call.function().equals(Operator.OLD_NOT_STRICTLY_FALSE.getFunction())) + && call.args().size() == 1 + && call.args().get(0).identOrDefault().name().equals(comp.accuVar()); + } + + private BoolExpr createTypeConstraint(Expr val, long exprId, CelAbstractSyntaxTree ast) { + CelType type = + ast.getType(exprId) + .orElseThrow( + () -> new IllegalArgumentException("Type not found for expr ID: " + exprId)); + BoolExpr typeConstraint = createTypeConstraintForType(val, type); + return ctx.mkOr(typeSystem.isError(val), typeSystem.isUnknown(val), typeConstraint); + } + + private BoolExpr createTypeConstraintForType(Expr val, CelType type) { + if (type.equals(SimpleType.BOOL)) { + return (BoolExpr) ctx.mkApp(typeSystem.boolCons().getTesterDecl(), val); + } + if (type.equals(SimpleType.INT)) { + Expr unwrapped = ctx.mkApp(typeSystem.intCons().getAccessorDecls()[0], val); + return ctx.mkAnd( + ctx.mkApp(typeSystem.intCons().getTesterDecl(), val), + ctx.mkGe((ArithExpr) unwrapped, ctx.mkInt(CelZ3TypeSystem.MIN_INT64)), + ctx.mkLe((ArithExpr) unwrapped, ctx.mkInt(CelZ3TypeSystem.MAX_INT64))); + } + if (type.equals(SimpleType.UINT)) { + Expr unwrapped = ctx.mkApp(typeSystem.uintCons().getAccessorDecls()[0], val); + return ctx.mkAnd( + ctx.mkApp(typeSystem.uintCons().getTesterDecl(), val), + ctx.mkGe((ArithExpr) unwrapped, ctx.mkInt(0)), + ctx.mkLe((ArithExpr) unwrapped, ctx.mkInt(CelZ3TypeSystem.MAX_UINT64))); + } + if (type.equals(SimpleType.DOUBLE)) { + return (BoolExpr) ctx.mkApp(typeSystem.doubleCons().getTesterDecl(), val); + } + if (type.equals(SimpleType.STRING)) { + return (BoolExpr) ctx.mkApp(typeSystem.stringCons().getTesterDecl(), val); + } + if (type.equals(SimpleType.BYTES)) { + return (BoolExpr) ctx.mkApp(typeSystem.bytesCons().getTesterDecl(), val); + } + if (type instanceof ListType) { + // Lists are explicitly bounded (sequence theory). We're safe in using for-all quantifiers + // here. + BoolExpr isList = typeSystem.isList(val); + CelType elemType = ((ListType) type).elemType(); + if (elemType.equals(SimpleType.DYN)) { + return isList; + } + + // isList(val) ∧ ∀i. (0 <= i < length) ⇒ elemType(seq[i]) + Expr listRef = typeSystem.getListRef(val); + SeqExpr seq = typeSystem.getSeq(listRef); + Expr length = ctx.mkLength(seq); + + List boundsAndTypes = new ArrayList<>(); + boundsAndTypes.add(isList); + for (int i = 0; i < comprehensionUnrollLimit; i++) { + IntExpr idx = ctx.mkInt(i); + Expr elem = ctx.mkNth(seq, idx); + BoolExpr elemConstraint = createTypeConstraintForType(elem, elemType); + BoolExpr validIndex = ctx.mkLt(idx, length); + boundsAndTypes.add(ctx.mkImplies(validIndex, elemConstraint)); + BoolExpr outOfBounds = ctx.mkGe(idx, length); + boundsAndTypes.add(ctx.mkImplies(outOfBounds, ctx.mkEq(elem, typeSystem.mkUnknown()))); + } + + return CelZ3TypeSystem.mkAndFlattened(ctx, boundsAndTypes); + } + if (type instanceof MapType) { + // Do NOT emit a for-all quantifier over map keys here. + // Doing so forces MBQI into an infinite loop. Structural equivalence of dynamic keys is + // naturally constrained by the primitive key assertions in getStructuralEquality(). + return typeSystem.isMap(val); + } + if (type.kind() == CelKind.STRUCT) { + return ctx.mkAnd( + typeSystem.isMessage(val), + ctx.mkEq( + typeSystem.getMsgTypeName(typeSystem.getMessageRef(val)), ctx.mkString(type.name()))); + } + // Fallback: no type constraint + return ctx.mkTrue(); + } + + CelAstToZ3Translator( + Context ctx, + int comprehensionUnrollLimit, + ImmutableSet unknownIdentifiers, + CelZ3FunctionRegistry functionRegistry, + CelTypeProvider typeProvider) { + this.ctx = ctx; + this.comprehensionUnrollLimit = comprehensionUnrollLimit; + this.typeSystem = new CelZ3TypeSystem(ctx); + this.typeConstraints = new LinkedHashSet<>(); + this.operatorTranslator = + new CelZ3OperatorTranslator( + ctx, + typeSystem, + this.typeConstraints::add, + this::createTypeConstraintForType, + !unknownIdentifiers.isEmpty(), + functionRegistry); + this.symbolTable = new HashMap<>(); + this.unknownIdentifiers = unknownIdentifiers; + this.emptyMessageCache = new HashMap<>(); + this.listLiteralCache = new HashMap<>(); + this.typeProvider = typeProvider; + this.truncationConditions = new ArrayList<>(); + } + + private static class IterationElement { + final Expr keyOrIndex; + final Expr value; + + IterationElement(Expr keyOrIndex, Expr value) { + this.keyOrIndex = keyOrIndex; + this.value = value; + } + } + + private Optional toCacheKey(CelExpr expr) { + switch (expr.exprKind().getKind()) { + case CONSTANT: + return Optional.of(expr.constant()); + case LIST: + ImmutableList.Builder builder = ImmutableList.builder(); + for (CelExpr elem : expr.list().elements()) { + Optional elemKey = toCacheKey(elem); + if (!elemKey.isPresent()) { + return Optional.empty(); // Contains non-constants + } + builder.add(elemKey.get()); + } + return Optional.of(builder.build()); + default: + return Optional.empty(); + } + } +} diff --git a/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifier.java b/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifier.java new file mode 100644 index 000000000..f9ad77c93 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifier.java @@ -0,0 +1,30 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import dev.cel.policy.CelPolicy; +import dev.cel.policy.CelPolicyValidationException; + +/** Public interface for verifying CEL policies. */ +public interface CelPolicyVerifier { + + /** + * Verifies if two policies are strictly equivalent for all possible inputs. + * + * @throws CelPolicyValidationException if the provided policies fail policy-specific compilation. + */ + CelVerificationResult verifyEquivalence(CelPolicy policyA, CelPolicy policyB) + throws CelPolicyValidationException, CelVerificationException; +} diff --git a/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierBuilder.java b/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierBuilder.java new file mode 100644 index 000000000..fcaaf6ca8 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierBuilder.java @@ -0,0 +1,22 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +/** Interface for building an instance of CelPolicyVerifier. */ +public interface CelPolicyVerifierBuilder { + + /** Builds the {@link CelPolicyVerifier} instance. */ + CelPolicyVerifier build(); +} diff --git a/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierFactory.java b/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierFactory.java new file mode 100644 index 000000000..cb323d3e0 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierFactory.java @@ -0,0 +1,29 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import dev.cel.policy.CelPolicyCompiler; + +/** Factory class for producing policy verifiers. */ +public final class CelPolicyVerifierFactory { + + /** Create a new {@link CelPolicyVerifierBuilder} instance. */ + public static CelPolicyVerifierBuilder newVerifier( + CelPolicyCompiler compiler, CelVerifier astVerifier) { + return CelPolicyVerifierImpl.newBuilder(compiler, astVerifier); + } + + private CelPolicyVerifierFactory() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierImpl.java b/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierImpl.java new file mode 100644 index 000000000..8cc145117 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierImpl.java @@ -0,0 +1,59 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.policy.CelPolicy; +import dev.cel.policy.CelPolicyCompiler; +import dev.cel.policy.CelPolicyValidationException; + +/** Implementation of CelPolicyVerifier using a CelVerifier. */ +final class CelPolicyVerifierImpl implements CelPolicyVerifier { + + private final CelPolicyCompiler compiler; + private final CelVerifier astVerifier; + + static CelPolicyVerifierBuilder newBuilder(CelPolicyCompiler compiler, CelVerifier verifier) { + return new Builder(compiler, verifier); + } + + static final class Builder implements CelPolicyVerifierBuilder { + private final CelPolicyCompiler compiler; + private final CelVerifier astVerifier; + + private Builder(CelPolicyCompiler compiler, CelVerifier astVerifier) { + this.compiler = compiler; + this.astVerifier = astVerifier; + } + + @Override + public CelPolicyVerifier build() { + return new CelPolicyVerifierImpl(compiler, astVerifier); + } + } + + private CelPolicyVerifierImpl(CelPolicyCompiler compiler, CelVerifier verifier) { + this.compiler = compiler; + this.astVerifier = verifier; + } + + @Override + public CelVerificationResult verifyEquivalence(CelPolicy policyA, CelPolicy policyB) + throws CelPolicyValidationException, CelVerificationException { + CelAbstractSyntaxTree astA = compiler.compile(policyA); + CelAbstractSyntaxTree astB = compiler.compile(policyB); + return astVerifier.verifyEquivalence(astA, astB); + } +} diff --git a/verifier/src/main/java/dev/cel/verifier/CelVerificationException.java b/verifier/src/main/java/dev/cel/verifier/CelVerificationException.java new file mode 100644 index 000000000..93908b4c9 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelVerificationException.java @@ -0,0 +1,26 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +/** + * Exception thrown when the CEL verifier fails to complete verification. This can happen if the + * underlying solver encounters an unknown state, such as a timeout or an undecidable problem. + */ +public class CelVerificationException extends Exception { + + CelVerificationException(String message) { + super(message); + } +} diff --git a/verifier/src/main/java/dev/cel/verifier/CelVerificationResult.java b/verifier/src/main/java/dev/cel/verifier/CelVerificationResult.java new file mode 100644 index 000000000..b7510ccf0 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelVerificationResult.java @@ -0,0 +1,53 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import com.google.auto.value.AutoValue; + +/** Result object containing the outcome of a CEL AST verification check. */ +@AutoValue +public abstract class CelVerificationResult { + + /** Represents the outcome of the verification process. */ + public enum VerificationStatus { + /** The property was mathematically proven to hold for all possible inputs. */ + VERIFIED, + /** The property was disproven. A concrete counterexample was found. */ + VIOLATED, + /** The property could not be verified due to loop truncation or timeout. */ + INCONCLUSIVE + } + + /** Returns the status of the verification check. */ + public abstract VerificationStatus status(); + + /** + * Returns a message detailing why the verification failed or was inconclusive (e.g., the + * counterexample input or truncation reason). Empty if status is VERIFIED. + */ + public abstract String message(); + + static CelVerificationResult verified() { + return new AutoValue_CelVerificationResult(VerificationStatus.VERIFIED, ""); + } + + static CelVerificationResult failed(String message) { + return new AutoValue_CelVerificationResult(VerificationStatus.VIOLATED, message); + } + + static CelVerificationResult inconclusive(String message) { + return new AutoValue_CelVerificationResult(VerificationStatus.INCONCLUSIVE, message); + } +} diff --git a/verifier/src/main/java/dev/cel/verifier/CelVerifier.java b/verifier/src/main/java/dev/cel/verifier/CelVerifier.java new file mode 100644 index 000000000..7394886cb --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelVerifier.java @@ -0,0 +1,46 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.CelAbstractSyntaxTree; + +/** Public interface for formal verification of CEL ASTs. */ +@Immutable +public interface CelVerifier { + + /** + * Returns verified if there is at least one input combination where the AST evaluates to true. + * + * @param ast The input expression to verify. Must be a type-checked AST. + */ + CelVerificationResult isSatisfiable(CelAbstractSyntaxTree ast) throws CelVerificationException; + + /** + * Returns verified if the AST evaluates to true for ALL possible inputs. + * + * @param ast The input expression to verify. Must be a type-checked AST. + */ + CelVerificationResult isAlwaysTrue(CelAbstractSyntaxTree ast) throws CelVerificationException; + + /** + * Returns verified if astA and astB are logically equivalent for all inputs. + * + * @param astA The first input expression to verify. Must be a type-checked AST. + * @param astB The second input expression to verify. Must be a type-checked AST. + */ + CelVerificationResult verifyEquivalence(CelAbstractSyntaxTree astA, CelAbstractSyntaxTree astB) + throws CelVerificationException; +} diff --git a/verifier/src/main/java/dev/cel/verifier/CelVerifierBuilder.java b/verifier/src/main/java/dev/cel/verifier/CelVerifierBuilder.java new file mode 100644 index 000000000..c49bb4343 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelVerifierBuilder.java @@ -0,0 +1,88 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import dev.cel.common.types.CelTypeProvider; +import java.time.Duration; + +/** Interface for building an instance of CelVerifier. */ +public interface CelVerifierBuilder { + + /** + * Sets the timeout duration for the verifier. The timeout must be strictly positive. + * + *

Note that this is a soft timeout and is evaluated on a best-effort basis by the underlying + * SMT solver. The solver checks for interruptions periodically during its search phase. This + * means it may overrun the requested timeout, and in pathological cases, could hang indefinitely. + * + *

If the verification takes longer than the specified timeout, an {@link + * IllegalStateException} is thrown. + */ + @CanIgnoreReturnValue + CelVerifierBuilder setTimeout(Duration timeout); + + /** + * Registers a variable name that should be permitted to evaluate to `Unknown` during + * verification, mirroring partial evaluation semantics. + */ + @CanIgnoreReturnValue + CelVerifierBuilder addUnknownIdentifier(String identifier); + + /** + * Sets the type provider for looking up {@code CelType} definitions by name. + * + *

The verifier uses this to resolve structural type definitions (such as protocol buffer + * messages) during verification. If not set, the verifier will fall back to type information + * present in the AST, which may lose details such as wrapper types. + */ + @CanIgnoreReturnValue + CelVerifierBuilder setTypeProvider(CelTypeProvider typeProvider); + + /** + * Sets the unroll limit for comprehensions (such as {@code map}, {@code filter}, {@code all}, + * {@code exists}). + * + *

In CEL, lists and maps can be dynamically sized (e.g., passed in as variables). The verifier + * cannot simulate loops of infinite or unknown size. To safely verify comprehensions, it + * translates them using Bounded Model Checking, which simulates the loop by statically unrolling + * it up to this fixed limit. + * + *

What this means for CEL users: + * + *

    + *
  • If a comprehension iterates over a sequence (list or map), the verifier will assert that + * the sequence's size is less than or equal to this limit. + *
  • For statically sized sequences (e.g., {@code [1, 2, 3]}), the verifier simply checks if + * its exact length is within the limit. + *
  • For dynamically sized sequences (e.g., {@code my_list.all(...)}), the verifier must be + * able to mathematically prove that the sequence's size is within the limit. If it cannot + * prove this (e.g., because the size is completely unconstrained), the entire comprehension + * will safely evaluate to {@code Unknown} during verification. + *
  • To successfully verify comprehensions over dynamic variables, the CEL expression itself + * must logically constrain the size (e.g., {@code size(my_list) <= limit ? my_list.all(...) + * : true}). + *
  • Setting this limit too high will exponentially increase verification time and memory + * usage. Setting it too low will prevent the verifier from proving properties about + * moderately-sized sequences. + *
+ */ + @CanIgnoreReturnValue + CelVerifierBuilder setComprehensionUnrollLimit(int unrollLimit); + + /** Builds the {@link CelVerifier} instance. */ + CelVerifier build(); +} + diff --git a/verifier/src/main/java/dev/cel/verifier/CelVerifierFactory.java b/verifier/src/main/java/dev/cel/verifier/CelVerifierFactory.java new file mode 100644 index 000000000..da48ec484 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelVerifierFactory.java @@ -0,0 +1,27 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + + +/** Factory class for producing AST verifiers using Z3. */ +public final class CelVerifierFactory { + + /** Create a builder for configuring a {@link CelVerifier}. */ + public static CelVerifierBuilder newVerifier() { + return CelVerifierZ3Impl.newBuilder(); + } + + private CelVerifierFactory() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java b/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java new file mode 100644 index 000000000..dbe4615da --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java @@ -0,0 +1,393 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.Immutable; +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Context; +import com.microsoft.z3.Model; +import com.microsoft.z3.Params; +import com.microsoft.z3.Solver; +import com.microsoft.z3.Status; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypeProvider; +import dev.cel.verifier.axioms.CelZ3FunctionAxiom; +import dev.cel.verifier.axioms.CelZ3StandardAxioms; +import java.time.Duration; +import java.util.Arrays; +import java.util.Optional; +import org.jspecify.annotations.Nullable; + +/** Z3 implementation of the CelVerifier. */ +@Immutable +final class CelVerifierZ3Impl implements CelVerifier { + + @VisibleForTesting + static final CelTypeProvider EMPTY_TYPE_PROVIDER = + new CelTypeProvider() { + @Override + public ImmutableList types() { + return ImmutableList.of(); + } + + @Override + public Optional findType(String typeName) { + return Optional.empty(); + } + }; + + private final Duration timeout; + private final int comprehensionUnrollLimit; + private final ImmutableSet unknownIdentifiers; + private final CelZ3FunctionRegistry functionRegistry; + private final CelTypeProvider typeProvider; + + static Builder newBuilder() { + return new Builder(); + } + + static final class Builder implements CelVerifierBuilder { + private Duration timeout; + private int comprehensionUnrollLimit; + private final ImmutableSet.Builder unknownIdentifiers; + private final ImmutableList.Builder functionAxioms; + private CelTypeProvider typeProvider; + + private Builder() { + this.timeout = Duration.ofSeconds(10); + this.comprehensionUnrollLimit = 5; + this.unknownIdentifiers = ImmutableSet.builder(); + this.functionAxioms = ImmutableList.builder(); + this.typeProvider = EMPTY_TYPE_PROVIDER; + } + + @Override + @CanIgnoreReturnValue + public Builder setTimeout(Duration timeout) { + if (timeout.isZero() || timeout.isNegative()) { + throw new IllegalArgumentException("Timeout must be strictly positive."); + } + this.timeout = timeout; + return this; + } + + @Override + @CanIgnoreReturnValue + public CelVerifierBuilder addUnknownIdentifier(String identifier) { + unknownIdentifiers.add(identifier); + return this; + } + + @Override + @CanIgnoreReturnValue + public CelVerifierBuilder setTypeProvider(CelTypeProvider typeProvider) { + this.typeProvider = Preconditions.checkNotNull(typeProvider); + return this; + } + + @Override + @CanIgnoreReturnValue + public CelVerifierBuilder setComprehensionUnrollLimit(int unrollLimit) { + Preconditions.checkArgument(unrollLimit >= 0, "unrollLimit must be non-negative"); + this.comprehensionUnrollLimit = unrollLimit; + return this; + } + + @CanIgnoreReturnValue + Builder addFunctionAxioms(CelZ3FunctionAxiom... axioms) { + return addFunctionAxioms(Arrays.asList(axioms)); + } + + @CanIgnoreReturnValue + Builder addFunctionAxioms(Iterable axioms) { + functionAxioms.addAll(axioms); + return this; + } + + @Override + public CelVerifier build() { + ImmutableList allFunctionAxioms = + ImmutableList.builder() + .addAll(CelZ3StandardAxioms.functionAxioms()) + .addAll(functionAxioms.build()) + .build(); + + CelZ3FunctionRegistry registry = CelZ3FunctionRegistry.create(allFunctionAxioms); + return new CelVerifierZ3Impl( + timeout, comprehensionUnrollLimit, unknownIdentifiers.build(), registry, typeProvider); + } + } + + @Override + public CelVerificationResult isSatisfiable(CelAbstractSyntaxTree ast) + throws CelVerificationException { + Preconditions.checkArgument(ast.isChecked(), "AST must be type-checked."); + return checkSatisfiability(ast, false); + } + + @Override + public CelVerificationResult isAlwaysTrue(CelAbstractSyntaxTree ast) + throws CelVerificationException { + Preconditions.checkArgument(ast.isChecked(), "AST must be type-checked."); + return checkSatisfiability(ast, true); + } + + @Override + public CelVerificationResult verifyEquivalence( + CelAbstractSyntaxTree astA, CelAbstractSyntaxTree astB) throws CelVerificationException { + Preconditions.checkArgument(astA.isChecked(), "astA must be type-checked."); + Preconditions.checkArgument(astB.isChecked(), "astB must be type-checked."); + try (Context ctx = new Context(ImmutableMap.of("model", "true"))) { + CelAstToZ3Translator translator = + new CelAstToZ3Translator( + ctx, comprehensionUnrollLimit, unknownIdentifiers, functionRegistry, typeProvider); + + TranslatedValue tvA = translator.translate(astA); + TranslatedValue tvB = translator.translate(astB); + + BoolExpr divergenceCondition = ctx.mkNot(ctx.mkEq(tvA.z3Expr(), tvB.z3Expr())); + BoolExpr combinedTaint = ctx.mkOr(tvA.isApproximate(), tvB.isApproximate()); + + Solver solver = newSolver(ctx); + for (BoolExpr constraint : translator.getTypeConstraints()) { + solver.add(constraint); + } + + BoolExpr unknownCondition = + ctx.mkOr( + translator.getTypeSystem().isUnknown(tvA.z3Expr()), + translator.getTypeSystem().isUnknown(tvB.z3Expr())); + + SolverRunResult result = + runThreePassVerification( + ctx, solver, divergenceCondition, combinedTaint, unknownCondition, translator); + + switch (result.outcome) { + case EXACT_MATCH: + return CelVerificationResult.failed( + "Equivalence violation detected." + + getCounterexampleString(ctx, translator.getTypeSystem(), result.model)); + case APPROXIMATE_MATCH: + return CelVerificationResult.inconclusive( + "Inconclusive: a divergence may exist, but it depends on approximations, missing" + + " theories, or loop bounds." + + getCounterexampleString(ctx, translator.getTypeSystem(), result.model)); + case TRUNCATED: + return CelVerificationResult.inconclusive( + "Inconclusive: expressions are equivalent within the current loop unroll limit, but" + + " may diverge for larger collections."); + case NO_MATCH: + return CelVerificationResult.verified(); + case SOLVER_UNKNOWN: + return CelVerificationResult.inconclusive( + "Inconclusive: the solver returned unknown status (" + result.reason + ")."); + } + throw new AssertionError("Unknown verification outcome: " + result.outcome); + } + } + + private CelVerificationResult checkSatisfiability( + CelAbstractSyntaxTree ast, boolean searchForCounterexample) throws CelVerificationException { + try (Context ctx = new Context(ImmutableMap.of("model", "true"))) { + CelAstToZ3Translator translator = + new CelAstToZ3Translator( + ctx, comprehensionUnrollLimit, unknownIdentifiers, functionRegistry, typeProvider); + + TranslatedValue tv = translator.translate(ast); + BoolExpr condition = translator.isTrue(tv.z3Expr()); + if (searchForCounterexample) { + condition = ctx.mkNot(condition); + } + + Solver solver = newSolver(ctx); + for (BoolExpr constraint : translator.getTypeConstraints()) { + solver.add(constraint); + } + + SolverRunResult result = + runThreePassVerification( + ctx, + solver, + condition, + tv.isApproximate(), + translator.getTypeSystem().isUnknown(tv.z3Expr()), + translator); + + switch (result.outcome) { + case EXACT_MATCH: + return searchForCounterexample + ? CelVerificationResult.failed( + "Condition is not always true." + + getCounterexampleString(ctx, translator.getTypeSystem(), result.model)) + : CelVerificationResult.verified(); + + case APPROXIMATE_MATCH: + String prefix = + searchForCounterexample + ? "Inconclusive: a counterexample may exist, but it depends on approximations," + + " missing theories, or loop bounds." + : "Inconclusive: a satisfying model may exist, but it depends on" + + " approximations, missing theories, or loop bounds."; + return CelVerificationResult.inconclusive( + prefix + getCounterexampleString(ctx, translator.getTypeSystem(), result.model)); + + case TRUNCATED: + return CelVerificationResult.inconclusive( + searchForCounterexample + ? "Inconclusive: a counterexample may exist beyond the loop unroll limit." + : "Inconclusive: expression is not satisfiable within the current loop unroll" + + " limit, but may be satisfiable for larger collections."); + + case NO_MATCH: + return searchForCounterexample + ? CelVerificationResult.verified() + : CelVerificationResult.failed("Condition is not satisfiable."); + + case SOLVER_UNKNOWN: + return CelVerificationResult.inconclusive( + "Inconclusive: the solver returned unknown status (" + result.reason + ")."); + } + throw new AssertionError("Unknown verification outcome: " + result.outcome); + } + } + + private SolverRunResult runThreePassVerification( + Context ctx, + Solver solver, + BoolExpr condition, + BoolExpr taint, + BoolExpr unknownCondition, + CelAstToZ3Translator translator) + throws CelVerificationException { + + // Pass 1: Search for an exact match/counterexample + solver.push(); + solver.add(condition); + solver.add(ctx.mkNot(taint)); + Status status = solver.check(); + + if (status == Status.SATISFIABLE) { + return SolverRunResult.exactMatch(solver.getModel()); + } else if (status == Status.UNKNOWN) { + return SolverRunResult.solverUnknown(checkTimeoutOrGetReason(solver)); + } + + // Pass 2: Search for any tainted match/counterexample + solver.pop(); + solver.push(); + solver.add(condition); + Status approxStatus = solver.check(); + + if (approxStatus == Status.SATISFIABLE) { + return SolverRunResult.approximateMatch(solver.getModel()); + } else if (approxStatus == Status.UNKNOWN) { + return SolverRunResult.solverUnknown(checkTimeoutOrGetReason(solver)); + } + + // Pass 3: Check BMC truncation + solver.pop(); + solver.add(unknownCondition); + solver.add(translator.hasTruncation()); + Status truncationStatus = solver.check(); + if (truncationStatus == Status.SATISFIABLE) { + return SolverRunResult.truncated(); + } else if (truncationStatus == Status.UNKNOWN) { + return SolverRunResult.solverUnknown(checkTimeoutOrGetReason(solver)); + } + + return SolverRunResult.noMatch(); + } + + private static String checkTimeoutOrGetReason(Solver solver) throws CelVerificationException { + String reason = solver.getReasonUnknown(); + if (reason.equals("timeout") || reason.equals("canceled")) { + throw new CelVerificationException("Verification timed out: " + reason); + } + return reason; + } + + private Solver newSolver(Context ctx) { + Solver solver = ctx.mkSolver(); + Params params = ctx.mkParams(); + params.add("timeout", (int) timeout.toMillis()); + solver.setParameters(params); + return solver; + } + + private static String getCounterexampleString( + Context ctx, CelZ3TypeSystem typeSystem, Model model) { + return CelZ3CounterexampleGenerator.generate(ctx, typeSystem, model); + } + + CelVerifierZ3Impl( + Duration timeout, + int comprehensionUnrollLimit, + ImmutableSet unknownIdentifiers, + CelZ3FunctionRegistry functionRegistry, + CelTypeProvider typeProvider) { + this.timeout = timeout; + this.comprehensionUnrollLimit = comprehensionUnrollLimit; + this.unknownIdentifiers = unknownIdentifiers; + this.functionRegistry = functionRegistry; + this.typeProvider = typeProvider; + } + + private enum SolverOutcome { + EXACT_MATCH, + APPROXIMATE_MATCH, + TRUNCATED, + NO_MATCH, + SOLVER_UNKNOWN + } + + private static final class SolverRunResult { + final SolverOutcome outcome; + final @Nullable Model model; + final @Nullable String reason; + + static SolverRunResult exactMatch(Model model) { + return new SolverRunResult(SolverOutcome.EXACT_MATCH, model, null); + } + + static SolverRunResult approximateMatch(Model model) { + return new SolverRunResult(SolverOutcome.APPROXIMATE_MATCH, model, null); + } + + static SolverRunResult truncated() { + return new SolverRunResult(SolverOutcome.TRUNCATED, null, null); + } + + static SolverRunResult noMatch() { + return new SolverRunResult(SolverOutcome.NO_MATCH, null, null); + } + + static SolverRunResult solverUnknown(String reason) { + return new SolverRunResult(SolverOutcome.SOLVER_UNKNOWN, null, reason); + } + + private SolverRunResult(SolverOutcome outcome, @Nullable Model model, @Nullable String reason) { + this.outcome = outcome; + this.model = model; + this.reason = reason; + } + } +} diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java new file mode 100644 index 000000000..41a94b5a1 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java @@ -0,0 +1,252 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import com.google.common.base.Preconditions; +import com.microsoft.z3.ArrayExpr; +import com.microsoft.z3.Context; +import com.microsoft.z3.Expr; +import com.microsoft.z3.FPNum; +import com.microsoft.z3.FuncDecl; +import com.microsoft.z3.IntNum; +import com.microsoft.z3.Model; +import com.microsoft.z3.RatNum; +import java.util.ArrayList; +import java.util.List; +import org.jspecify.annotations.Nullable; + +/** Generates human-readable counterexample strings from Z3 models. */ +@SuppressWarnings({"unchecked", "rawtypes"}) // Z3 Java API uses raw types. +final class CelZ3CounterexampleGenerator { + + private CelZ3CounterexampleGenerator() {} + + static String generate(Context ctx, CelZ3TypeSystem typeSystem, Model model) { + FuncDecl[] constDecls = model.getConstDecls(); + + List bindings = new ArrayList<>(); + for (FuncDecl decl : constDecls) { + String name = decl.getName().toString(); + // Filter out internal solver-generated Skolem constants (e.g., k!1, seq.empty!0). + // `!` is not a valid CEL identifier. + if (name.contains("!")) { + continue; + } + Expr constInterp = model.getConstInterp(decl); + if (constInterp != null) { + bindings.add( + String.format("\n %s = %s", name, formatExpr(ctx, typeSystem, model, constInterp))); + } + } + + if (bindings.isEmpty()) { + return " (The expression fails unconditionally, regardless of input state)"; + } + + return " Counterexample input:" + String.join("", bindings); + } + + private static String formatExpr( + Context ctx, CelZ3TypeSystem typeSystem, Model model, @Nullable Expr expr) { + Preconditions.checkState(expr != null, "Z3 failed to evaluate the expression natively."); + + FuncDecl decl = expr.getFuncDecl(); + + // Handle CelType constructors wrapper unwrapping + if (decl.equals(typeSystem.intCons().ConstructorDecl())) { + return formatExpr(ctx, typeSystem, model, expr.getArgs()[0]); + } else if (decl.equals(typeSystem.uintCons().ConstructorDecl())) { + return formatExpr(ctx, typeSystem, model, expr.getArgs()[0]) + "u"; + } else if (decl.equals(typeSystem.boolCons().ConstructorDecl())) { + return formatExpr(ctx, typeSystem, model, expr.getArgs()[0]); + } else if (decl.equals(typeSystem.stringCons().ConstructorDecl())) { + return expr.getArgs()[0].toString(); + } else if (decl.equals(typeSystem.bytesCons().ConstructorDecl())) { + return "b" + expr.getArgs()[0]; + } else if (decl.equals(typeSystem.doubleCons().ConstructorDecl())) { + Expr doubleArg = expr.getArgs()[0]; + if (doubleArg instanceof FPNum) { + FPNum fpNum = (FPNum) doubleArg; + if (fpNum.isNaN()) { + return "NaN"; + } + if (fpNum.isInf()) { + return fpNum.isNegative() ? "-Infinity" : "Infinity"; + } + if (fpNum.isZero()) { + return fpNum.isNegative() ? "-0.0" : "0.0"; + } + Expr realExpr = ctx.mkFPToReal(fpNum).simplify(); + if (realExpr instanceof RatNum) { + RatNum ratNum = (RatNum) realExpr; + double val = + ratNum.getBigIntNumerator().doubleValue() + / ratNum.getBigIntDenominator().doubleValue(); + return Double.toString(val); + } + } + return doubleArg.toString(); + } else if (decl.equals(typeSystem.listCons().ConstructorDecl())) { + return reconstructList(ctx, typeSystem, model, expr.getArgs()[0]); + } else if (decl.equals(typeSystem.mapCons().ConstructorDecl())) { + return reconstructMap(ctx, typeSystem, model, expr.getArgs()[0]); + } else if (decl.equals(typeSystem.messageCons().ConstructorDecl())) { + return reconstructMessage(ctx, typeSystem, model, expr.getArgs()[0]); + } else if (decl.equals(typeSystem.errorCons().ConstructorDecl())) { + return "Error"; + } else if (decl.equals(typeSystem.unknownCons().ConstructorDecl())) { + return "Unknown"; + } + + return expr.toString(); + } + + private static String reconstructList( + Context ctx, CelZ3TypeSystem typeSystem, Model model, Expr listRef) { + Expr lenExpr = + evaluateStrict( + model, + ctx.mkLength(typeSystem.getSeq(listRef)), + String.format("Z3 failed to evaluate length for list %s", listRef)); + int length = ((IntNum) lenExpr).getInt(); + int printLimit = Math.min(length, 100); + List elements = new ArrayList<>(); + for (int i = 0; i < printLimit; i++) { + Expr elem = + evaluateStrict( + model, + ctx.mkNth(typeSystem.getSeq(listRef), ctx.mkInt(i)), + String.format( + "Z3 failed to evaluate list element at index %d for list %s", i, listRef)); + elements.add(formatExpr(ctx, typeSystem, model, elem)); + } + if (length > printLimit) { + elements.add("... (" + (length - printLimit) + " more elements)"); + } + + return "[" + String.join(", ", elements) + "]"; + } + + private static String reconstructMap( + Context ctx, CelZ3TypeSystem typeSystem, Model model, Expr mapRef) { + Expr presenceArray = + evaluateStrict( + model, + typeSystem.getMapPresence(mapRef), + String.format("Z3 failed to evaluate presence array natively for map %s", mapRef)); + + List> keys = new ArrayList<>(); + extractKeys(presenceArray, keys); + + List entries = new ArrayList<>(); + for (Expr key : keys) { + Expr presence = + evaluateStrict( + model, + ctx.mkSelect((ArrayExpr) typeSystem.getMapPresence(mapRef), key), + String.format( + "Z3 failed to evaluate map presence for key %s in map %s", key, mapRef)); + + if (presence.isTrue()) { + Expr value = + evaluateStrict( + model, + ctx.mkSelect((ArrayExpr) typeSystem.getMapValues(mapRef), key), + String.format("Z3 failed to evaluate map value for key %s in map %s", key, mapRef)); + entries.add( + formatExpr(ctx, typeSystem, model, key) + + ": " + + formatExpr(ctx, typeSystem, model, value)); + } + } + + return "{" + String.join(", ", entries) + "}"; + } + + private static String reconstructMessage( + Context ctx, CelZ3TypeSystem typeSystem, Model model, Expr msgRef) { + Expr valuesArray = + evaluateStrict( + model, + typeSystem.getMsgValues(msgRef), + String.format("Z3 failed to evaluate values array natively for msg %s", msgRef)); + + Expr typeNameExpr = + evaluateStrict( + model, + typeSystem.getMsgTypeName(msgRef), + String.format("Z3 failed to evaluate type name natively for msg %s", msgRef)); + + String typeName = formatExpr(ctx, typeSystem, model, typeNameExpr).replace("\"", ""); + + List> keys = new ArrayList<>(); + extractKeys(valuesArray, keys); + + List entries = new ArrayList<>(); + for (Expr key : keys) { + Expr presence = + evaluateStrict( + model, + ctx.mkSelect((ArrayExpr) typeSystem.getMsgPresence(msgRef), key), + String.format( + "Z3 failed to evaluate msg presence for key %s in msg %s", key, msgRef)); + + if (presence.isTrue()) { + Expr value = + evaluateStrict( + model, + ctx.mkSelect((ArrayExpr) typeSystem.getMsgValues(msgRef), key), + String.format("Z3 failed to evaluate msg value for key %s in msg %s", key, msgRef)); + + String fieldName = formatExpr(ctx, typeSystem, model, key).replace("\"", ""); + entries.add(fieldName + ": " + formatExpr(ctx, typeSystem, model, value)); + } + } + + return typeName + "{" + String.join(", ", entries) + "}"; + } + + private static void extractKeys(Expr arrayExpr, List> keys) { + int iterations = 0; + while (true) { + if (++iterations > 100_000) { + throw new IllegalStateException("Exceeded maximum number of extractKeys iterations."); + } + FuncDecl decl = arrayExpr.getFuncDecl(); + String declName = decl.getName().toString(); + + if (!declName.equals("store")) { + break; + } + + Expr[] args = arrayExpr.getArgs(); + Preconditions.checkState( + args.length == 3, "Z3 store array operation must have exactly 3 arguments"); + keys.add(args[1]); + + arrayExpr = args[0]; + } + } + + private static Expr evaluateStrict(Model model, Expr expr, String errorMessage) { + // There are no free variables remaining after the solver is ran, so the completion flag has + // no effect. + Expr evaluated = model.evaluate(expr, /* completion= */ true); + if (evaluated == null) { + throw new IllegalStateException(errorMessage); + } + return evaluated; + } +} diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3ExtensionalityAxioms.java b/verifier/src/main/java/dev/cel/verifier/CelZ3ExtensionalityAxioms.java new file mode 100644 index 000000000..db325b429 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3ExtensionalityAxioms.java @@ -0,0 +1,161 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import com.google.common.collect.ImmutableList; +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Context; +import com.microsoft.z3.Expr; +import com.microsoft.z3.FuncDecl; +import com.microsoft.z3.Sort; +import java.util.Set; + +/** + * Generates extensionality axioms for CEL Z3 reference types. + * + *

Z3 models collections and messages as references. Without these axioms, the solver performs a + * shallow pointer equality check, which fails for identical but distinctly allocated collection + * literals (e.g., [[1]] == [[1]]). + * + *

Uses O(N) ground instantiation with Uninterpreted Functions to avoid O(N^2) implication graphs + * and E-matching pattern overhead. + * + *

Formal derivation of the extensionality property via ground uninterpreted function assertions: + * + *

    + *
  • Ground assertion: {@code f_ref(seq(R)) = R} + *
  • Congruence closure rule: {@code ∀X, Y : X = Y ⟹ f_ref(X) = f_ref(Y)} + *
  • Substituting {@code seq(R1)} and {@code seq(R2)}: {@code seq(R1) = seq(R2) ⟹ f_ref(seq(R1)) + * = f_ref(seq(R2))} + *
  • Applying the ground assertion yields the extensionality property: {@code ∀R1, R2 : seq(R1) + * = seq(R2) ⟹ R1 = R2} + *
+ */ +final class CelZ3ExtensionalityAxioms { + + private static final String FUNC_MK_LIST_REF = "!mkListRef"; + private static final String FUNC_MK_MAP_REF = "!mkMapRef"; + private static final String FUNC_MK_MSG_REF = "!mkMsgRef"; + + static ImmutableList generateAxioms( + Context ctx, + CelZ3TypeSystem typeSystem, + Set> listRefs, + Set> mapRefs, + Set> msgRefs) { + + ImmutableList.Builder axioms = ImmutableList.builder(); + + if (!listRefs.isEmpty()) { + addListAxioms(ctx, typeSystem, listRefs, axioms); + } + if (!mapRefs.isEmpty()) { + addMapAxioms(ctx, typeSystem, mapRefs, axioms); + } + if (!msgRefs.isEmpty()) { + addMessageAxioms(ctx, typeSystem, msgRefs, axioms); + } + + return axioms.build(); + } + + private static void addListAxioms( + Context ctx, + CelZ3TypeSystem typeSystem, + Set> refs, + ImmutableList.Builder axioms) { + + Sort listRefSort = typeSystem.listRefSort(); + Sort seqSort = ctx.mkSeqSort(typeSystem.celValueSort()); + FuncDecl mkListRef = ctx.mkFuncDecl(FUNC_MK_LIST_REF, new Sort[] {seqSort}, listRefSort); + + for (Expr ref : refs) { + Expr ufApp = ctx.mkApp(mkListRef, typeSystem.getSeq(ref)); + // Ground assertion: f_list(seq(ref)) = ref + BoolExpr axiom = ctx.mkEq(ufApp, ref); + + // Guard the axiom with type check if the reference is extracted from a CelValue + if (ref.isApp() && ref.getFuncDecl().equals(typeSystem.listCons().getAccessorDecls()[0])) { + Expr inner = ref.getArgs()[0]; + axiom = ctx.mkImplies(typeSystem.isList(inner), axiom); + } + axioms.add(axiom); + } + } + + private static void addMapAxioms( + Context ctx, + CelZ3TypeSystem typeSystem, + Set> refs, + ImmutableList.Builder axioms) { + + Sort mapRefSort = typeSystem.mapRefSort(); + Sort valuesSort = ctx.mkArraySort(typeSystem.celValueSort(), typeSystem.celValueSort()); + Sort presenceSort = ctx.mkArraySort(typeSystem.celValueSort(), ctx.getBoolSort()); + + FuncDecl mkMapRef = + ctx.mkFuncDecl(FUNC_MK_MAP_REF, new Sort[] {valuesSort, presenceSort}, mapRefSort); + + for (Expr ref : refs) { + Expr ufApp = + ctx.mkApp(mkMapRef, typeSystem.getMapValues(ref), typeSystem.getMapPresence(ref)); + // Ground assertion: f_map(values(ref), presence(ref)) = ref + BoolExpr axiom = ctx.mkEq(ufApp, ref); + + // Guard the axiom with type check if the reference is extracted from a CelValue + if (ref.isApp() && ref.getFuncDecl().equals(typeSystem.mapCons().getAccessorDecls()[0])) { + Expr inner = ref.getArgs()[0]; + axiom = ctx.mkImplies(typeSystem.isMap(inner), axiom); + } + axioms.add(axiom); + } + } + + private static void addMessageAxioms( + Context ctx, + CelZ3TypeSystem typeSystem, + Set> refs, + ImmutableList.Builder axioms) { + + Sort msgRefSort = typeSystem.messageRefSort(); + Sort typeNameSort = ctx.getStringSort(); + Sort valuesSort = ctx.mkArraySort(ctx.getStringSort(), typeSystem.celValueSort()); + Sort presenceSort = ctx.mkArraySort(ctx.getStringSort(), ctx.getBoolSort()); + + FuncDecl mkMsgRef = + ctx.mkFuncDecl( + FUNC_MK_MSG_REF, new Sort[] {typeNameSort, valuesSort, presenceSort}, msgRefSort); + + for (Expr ref : refs) { + Expr ufApp = + ctx.mkApp( + mkMsgRef, + typeSystem.getMsgTypeName(ref), + typeSystem.getMsgValues(ref), + typeSystem.getMsgPresence(ref)); + // Ground assertion: f_msg(typeName(ref), values(ref), presence(ref)) = ref + BoolExpr axiom = ctx.mkEq(ufApp, ref); + + // Guard the axiom with type check if the reference is extracted from a CelValue + if (ref.isApp() && ref.getFuncDecl().equals(typeSystem.messageCons().getAccessorDecls()[0])) { + Expr inner = ref.getArgs()[0]; + axiom = ctx.mkImplies(typeSystem.isMessage(inner), axiom); + } + axioms.add(axiom); + } + } + + private CelZ3ExtensionalityAxioms() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3FunctionRegistry.java b/verifier/src/main/java/dev/cel/verifier/CelZ3FunctionRegistry.java new file mode 100644 index 000000000..212353d5f --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3FunctionRegistry.java @@ -0,0 +1,63 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.CelFunctionDecl; +import dev.cel.verifier.axioms.CelZ3FunctionAxiom; +import dev.cel.verifier.axioms.CelZ3OverloadTranslator; +import java.util.Optional; + +/** Internal registry managing SMT translations for CEL functions. */ +@Immutable +final class CelZ3FunctionRegistry { + private final ImmutableMap declarations; + private final ImmutableMap translators; + + private CelZ3FunctionRegistry( + ImmutableMap declarations, + ImmutableMap translators) { + this.declarations = declarations; + this.translators = translators; + } + + /** Retrieves the canonical declaration for a given function name. */ + Optional getDeclaration(String functionName) { + return Optional.ofNullable(declarations.get(functionName)); + } + + /** Retrieves the Z3 translator for a specific overload ID. */ + Optional getTranslator(String overloadId) { + return Optional.ofNullable(translators.get(overloadId)); + } + + /** Builds a validated registry from a collection of axioms. */ + static CelZ3FunctionRegistry create(Iterable axioms) { + ImmutableMap.Builder declsBuilder = ImmutableMap.builder(); + ImmutableMap.Builder translatorsBuilder = + ImmutableMap.builder(); + + for (CelZ3FunctionAxiom axiom : axioms) { + CelFunctionDecl decl = axiom.declaration(); + declsBuilder.put(decl.name(), decl); + + translatorsBuilder.putAll(axiom.overloadTranslators()); + } + + return new CelZ3FunctionRegistry( + declsBuilder.buildOrThrow(), translatorsBuilder.buildOrThrow()); + } +} diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java new file mode 100644 index 000000000..3c24cc20d --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java @@ -0,0 +1,726 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.google.common.collect.ImmutableList; +import com.microsoft.z3.ArithExpr; +import com.microsoft.z3.ArrayExpr; +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Context; +import com.microsoft.z3.Expr; +import com.microsoft.z3.FPExpr; +import com.microsoft.z3.IntExpr; +import com.microsoft.z3.SeqExpr; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOverloadDecl; +import dev.cel.common.Operator; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.ast.CelExpr.ExprKind; +import dev.cel.common.ast.CelReference; +import dev.cel.common.types.CelKind; +import dev.cel.common.types.CelType; +import dev.cel.common.types.ListType; +import dev.cel.common.types.MapType; +import dev.cel.common.types.OptionalType; +import dev.cel.common.types.SimpleType; +import dev.cel.verifier.axioms.CelZ3OverloadResult; +import dev.cel.verifier.axioms.CelZ3OverloadTranslator; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Consumer; + +/** Handles mapping CEL Operators to Z3 SMT logic. */ +@SuppressWarnings({"unchecked", "rawtypes"}) // Z3 Java API uses raw types. +final class CelZ3OperatorTranslator { + private final Context ctx; + private final CelZ3TypeSystem typeSystem; + private final Consumer constraintSink; + private final BiFunction, CelType, BoolExpr> typeConstraintGenerator; + private final boolean allowUnknowns; + private final CelZ3FunctionRegistry functionRegistry; + + Optional translateFunctionCall( + String functionName, List args, long exprId, CelAbstractSyntaxTree ast) { + Optional opOpt = Operator.findReverse(functionName); + ImmutableList> z3Args = + args.stream().map(TranslatedValue::z3Expr).collect(toImmutableList()); + ImmutableList argApproximations = + args.stream().map(TranslatedValue::isApproximate).collect(toImmutableList()); + + TranslatedValue resultChain = + opOpt + .map(op -> translateOperatorCall(op, args, ast)) + .orElseGet( + () -> TranslatedValue.propagateStrict(ctx, typeSystem, typeSystem.mkError(), args)); + + CelReference reference = ast.getReferenceMap().get(exprId); + CelFunctionDecl decl = functionRegistry.getDeclaration(functionName).orElse(null); + + if (decl == null) { + return opOpt.isPresent() ? Optional.of(resultChain) : Optional.empty(); + } + + boolean matchedAny = false; + Expr currentZ3Result = resultChain.z3Expr(); + BoolExpr currentApprox = resultChain.isApproximate(); + // Z3 is bottom up, but since we are replacing errors with the next matching overload, + // we should actually process the overloads in reverse order to build the ITE chain. + for (CelOverloadDecl overload : decl.overloads().asList().reverse()) { + // Bypasses the SMT ITE soup for cases where we know the exact function call signature. + if (reference != null && !reference.overloadIds().isEmpty()) { + if (!reference.overloadIds().contains(overload.overloadId())) { + continue; + } + } + + if (overload.parameterTypes().size() != args.size()) { + continue; + } + + CelZ3OverloadTranslator translator = + functionRegistry.getTranslator(overload.overloadId()).orElse(null); + if (translator == null) { + continue; + } + + Optional evaluatedResultOpt = + translator.translate(ctx, typeSystem, constraintSink, z3Args, argApproximations); + + if (!evaluatedResultOpt.isPresent()) { + continue; + } + + CelZ3OverloadResult evaluatedResult = evaluatedResultOpt.get(); + + matchedAny = true; + List typeGuards = new ArrayList<>(); + for (int i = 0; i < z3Args.size(); i++) { + typeGuards.add(mkTypeGuard(z3Args.get(i), overload.parameterTypes().get(i))); + } + + BoolExpr typeGuard = CelZ3TypeSystem.mkAndFlattened(ctx, typeGuards); + currentZ3Result = ctx.mkITE(typeGuard, evaluatedResult.z3Expr(), currentZ3Result); + currentApprox = + (BoolExpr) ctx.mkITE(typeGuard, evaluatedResult.isApproximate(), currentApprox); + } + + if (!matchedAny) { + return opOpt.isPresent() ? Optional.of(resultChain) : Optional.empty(); + } + + return Optional.of( + TranslatedValue.create( + typeSystem.propagateErrorAndUnknown(currentZ3Result, z3Args), + typeSystem, + currentApprox)); + } + + private BoolExpr mkTypeGuard(Expr arg, CelType expectedType) { + switch (expectedType.kind()) { + case LIST: + return typeSystem.isList(arg); + case MAP: + return typeSystem.isMap(arg); + case ANY: + case TYPE_PARAM: + case DYN: + // These match everything structurally type-wise, although we might refine this later. + return ctx.mkTrue(); + case INT: + case TIMESTAMP: + case DURATION: + // Safe to map int, timestamp, and duration to IntSort because CEL's static checker prevents + // invalid cross-type usage and their operator axioms translate to identical Z3 ASTs. + return typeSystem.isInt(arg); + case UINT: + return typeSystem.isUint(arg); + case DOUBLE: + return typeSystem.isDouble(arg); + case BOOL: + return typeSystem.isBool(arg); + case STRING: + return typeSystem.isString(arg); + case BYTES: + return typeSystem.isBytes(arg); + case STRUCT: + return typeSystem.isStruct(arg); + case NULL_TYPE: + return typeSystem.isNull(arg); + case OPAQUE: + if (expectedType.name().equals(OptionalType.NAME)) { + return typeSystem.isOptional(arg); + } + // Fallthrough + default: + throw new UnsupportedOperationException( + "Unsupported type for dynamic type guard: " + expectedType.kind()); + } + } + + private TranslatedValue translateOperatorCall( + Operator op, List args, CelAbstractSyntaxTree ast) { + switch (op) { + case NEGATE: + case LOGICAL_NOT: + case NOT_STRICTLY_FALSE: + case OLD_NOT_STRICTLY_FALSE: + if (args.size() != 1) { + throw new IllegalArgumentException( + String.format( + "Malformed AST: operator %s expects 1 argument, got %d", op, args.size())); + } + break; + case CONDITIONAL: + if (args.size() != 3) { + throw new IllegalArgumentException( + String.format( + "Malformed AST: operator %s expects 3 arguments, got %d", op, args.size())); + } + break; + case LOGICAL_AND: + case LOGICAL_OR: + if (args.size() < 2) { + throw new IllegalArgumentException( + String.format( + "Malformed AST: operator %s expects at least 2 arguments, got %d", + op, args.size())); + } + break; + default: + // All other supported ops are binary (e.g. EQUALS, LESS, ADD, IN) + if (args.size() != 2) { + throw new IllegalArgumentException( + String.format( + "Malformed AST: operator %s expects 2 arguments, got %d", op, args.size())); + } + break; + } + + switch (op) { + case LOGICAL_AND: + return translateLogicalAndOr(args, true); + case LOGICAL_OR: + return translateLogicalAndOr(args, false); + case LOGICAL_NOT: + return translateLogicalNot(args, ast); + case EQUALS: + return translateEquals(args.get(0), args.get(1), ast); + case NOT_EQUALS: + return translateNotEquals(args.get(0), args.get(1), ast); + case LESS: + case GREATER: + case LESS_EQUALS: + case GREATER_EQUALS: + case ADD: + case SUBTRACT: + case MULTIPLY: + case DIVIDE: + case MODULO: + case NEGATE: + case IN: + // Indicates a type-mismatch in an operator that's not handled + // by our axioms + return TranslatedValue.propagateStrict(ctx, typeSystem, typeSystem.mkError(), args); + case INDEX: + return translateIndex(args, ast); + case CONDITIONAL: + return translateConditional(args, ast); + case NOT_STRICTLY_FALSE: + case OLD_NOT_STRICTLY_FALSE: + return translateNotStrictlyFalse(args); + default: + // For operators we haven't implemented cleanly, just wrap uninterpreted for now. + return TranslatedValue.propagateStrict(ctx, typeSystem, typeSystem.mkUnknown(), args) + .withApproximation(ctx.mkTrue()); + } + } + + private TranslatedValue translateLogicalAndOr(List args, boolean isAnd) { + TranslatedValue result = args.get(0); + for (int i = 1; i < args.size(); i++) { + result = translateBinaryLogicalAndOr(result, args.get(i), isAnd); + } + return result; + } + + private TranslatedValue translateBinaryLogicalAndOr( + TranslatedValue a, TranslatedValue b, boolean isAnd) { + BoolExpr aIsBool = a.isZ3Bool(); + BoolExpr bIsBool = b.isZ3Bool(); + BoolExpr aTrue = ctx.mkAnd(aIsBool, (BoolExpr) a.unwrapZ3Bool()); + BoolExpr bTrue = ctx.mkAnd(bIsBool, (BoolExpr) b.unwrapZ3Bool()); + BoolExpr aFalse = ctx.mkAnd(aIsBool, ctx.mkNot((BoolExpr) a.unwrapZ3Bool())); + BoolExpr bFalse = ctx.mkAnd(bIsBool, ctx.mkNot((BoolExpr) b.unwrapZ3Bool())); + + BoolExpr hasMatch = isAnd ? ctx.mkOr(aFalse, bFalse) : ctx.mkOr(aTrue, bTrue); + BoolExpr hasUnknown = ctx.mkOr(a.isZ3Unknown(), b.isZ3Unknown()); + BoolExpr hasError = ctx.mkOr(a.isZ3Error(), b.isZ3Error()); + + BoolExpr aMatch = isAnd ? aFalse : aTrue; + BoolExpr bMatch = isAnd ? bFalse : bTrue; + + BoolExpr hasSafeMatch = + ctx.mkOr( + ctx.mkAnd(aMatch, ctx.mkNot(a.isApproximate())), + ctx.mkAnd(bMatch, ctx.mkNot(b.isApproximate()))); + + BoolExpr hasSafeError = + ctx.mkOr( + ctx.mkAnd(a.isZ3Error(), ctx.mkNot(a.isApproximate())), + ctx.mkAnd(b.isZ3Error(), ctx.mkNot(b.isApproximate()))); + + BoolExpr hasSafeUnknown = + ctx.mkOr( + ctx.mkAnd(a.isZ3Unknown(), ctx.mkNot(a.isApproximate())), + ctx.mkAnd(b.isZ3Unknown(), ctx.mkNot(b.isApproximate()))); + + Expr resultZ3 = + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(hasMatch, typeSystem.mkBool(!isAnd)) + .addCase(hasUnknown, typeSystem.mkUnknown()) + .addCase(hasError, typeSystem.mkError()) + .build(typeSystem.mkBool(isAnd)); + + BoolExpr resultTaint = + (BoolExpr) + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(hasMatch, ctx.mkNot(hasSafeMatch)) + .addCase(hasUnknown, ctx.mkNot(hasSafeUnknown)) + .addCase(hasError, ctx.mkNot(hasSafeError)) + .build(ctx.mkOr(a.isApproximate(), b.isApproximate())); + + return TranslatedValue.create(resultZ3, typeSystem, resultTaint); + } + + private TranslatedValue translateLogicalNot( + List args, CelAbstractSyntaxTree ast) { + TranslatedValue arg = args.get(0); + CelType type = extractAstTypeOrDefault(arg, ast); + + Expr baseResult = typeSystem.wrapBool(ctx.mkNot((BoolExpr) arg.unwrapZ3Bool())); + if (!type.equals(SimpleType.BOOL)) { + baseResult = typeSystem.withRuntimeError(baseResult, ctx.mkNot(arg.isZ3Bool())); + } + + return TranslatedValue.propagateStrict(ctx, typeSystem, baseResult, args); + } + + private BoolExpr isNumeric(Expr arg) { + return ctx.mkOr(typeSystem.isInt(arg), typeSystem.isUint(arg), typeSystem.isDouble(arg)); + } + + private BoolExpr getNumericEqualityWithConstant( + Expr symVal, CelConstant constant, CelType symType) { + Long intVal = null; + String uintVal = null; + double doubleVal; + + switch (constant.getKind()) { + case INT64_VALUE: + long vInt = constant.int64Value(); + intVal = vInt; + // Z3's infinite precision automatically evaluates `uint == -1` to false, + // but pruning it here keeps the formula smaller. + if (vInt >= 0) { + uintVal = Long.toString(vInt); + } + doubleVal = (double) vInt; + break; + case UINT64_VALUE: + long vUint = constant.uint64Value().longValue(); + if (vUint >= 0) { + intVal = vUint; + } + uintVal = constant.uint64Value().toString(); + doubleVal = constant.uint64Value().doubleValue(); + break; + case DOUBLE_VALUE: + double vDouble = constant.doubleValue(); + doubleVal = vDouble; + if (vDouble == Math.floor(vDouble) && !Double.isInfinite(vDouble)) { + if (vDouble >= Long.MIN_VALUE && vDouble <= Long.MAX_VALUE) { + intVal = (long) vDouble; + } + if (vDouble >= 0 && vDouble <= Double.parseDouble(CelZ3TypeSystem.MAX_UINT64)) { + uintVal = BigDecimal.valueOf(vDouble).toBigInteger().toString(); + } + } + break; + default: + throw new IllegalArgumentException( + "Unexpected numeric constant kind: " + constant.getKind()); + } + + if (isStaticallyKnown(symType)) { + if (symType.kind() == CelKind.INT) { + return (intVal != null) + ? ctx.mkEq(typeSystem.getInt(symVal), ctx.mkInt(intVal)) + : ctx.mkFalse(); + } else if (symType.kind() == CelKind.UINT) { + return (uintVal != null) + ? ctx.mkEq(typeSystem.getUint(symVal), ctx.mkInt(uintVal)) + : ctx.mkFalse(); + } else if (symType.kind() == CelKind.DOUBLE) { + return ctx.mkFPEq((FPExpr) typeSystem.getDouble(symVal), typeSystem.mkFpDouble(doubleVal)); + } + } + + BoolExpr intEq = + (intVal != null) ? ctx.mkEq(typeSystem.getInt(symVal), ctx.mkInt(intVal)) : ctx.mkFalse(); + BoolExpr uintEq = + (uintVal != null) + ? ctx.mkEq(typeSystem.getUint(symVal), ctx.mkInt(uintVal)) + : ctx.mkFalse(); + BoolExpr doubleEq = + ctx.mkFPEq((FPExpr) typeSystem.getDouble(symVal), typeSystem.mkFpDouble(doubleVal)); + + return (BoolExpr) + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(typeSystem.isInt(symVal), intEq) + .addCase(typeSystem.isUint(symVal), uintEq) + .addCase(typeSystem.isDouble(symVal), doubleEq) + .build(ctx.mkFalse()); + } + + private BoolExpr getNumericEquality( + TranslatedValue arg0, TranslatedValue arg1, CelAbstractSyntaxTree ast) { + if (arg0.isNumericConstant()) { + return getNumericEqualityWithConstant( + arg1.z3Expr(), arg0.celExpr().get().constant(), extractAstTypeOrDefault(arg1, ast)); + } else if (arg1.isNumericConstant()) { + return getNumericEqualityWithConstant( + arg0.z3Expr(), arg1.celExpr().get().constant(), extractAstTypeOrDefault(arg0, ast)); + } + + CelType type0 = extractAstTypeOrDefault(arg0, ast); + CelType type1 = extractAstTypeOrDefault(arg1, ast); + if (isStaticallyKnown(type0) && isStaticallyKnown(type1)) { + return getStaticallyKnownNumericEquality(arg0.z3Expr(), type0, arg1.z3Expr()); + } + + return getDynamicNumericEquality(arg0.z3Expr(), arg1.z3Expr()); + } + + /** + * Evaluates numeric equality when types are statically known. + */ + private BoolExpr getStaticallyKnownNumericEquality( + Expr z3Expr0, CelType type0, Expr z3Expr1) { + switch (type0.kind()) { + case INT: + return ctx.mkEq(typeSystem.getInt(z3Expr0), typeSystem.getInt(z3Expr1)); + case UINT: + return ctx.mkEq(typeSystem.getUint(z3Expr0), typeSystem.getUint(z3Expr1)); + case DOUBLE: + return ctx.mkFPEq( + (FPExpr) typeSystem.getDouble(z3Expr0), (FPExpr) typeSystem.getDouble(z3Expr1)); + default: + return ctx.mkFalse(); + } + } + + private BoolExpr getDynamicNumericEquality(Expr z3Expr0, Expr z3Expr1) { + BoolExpr isIntOrUint0 = ctx.mkOr(typeSystem.isInt(z3Expr0), typeSystem.isUint(z3Expr0)); + BoolExpr isIntOrUint1 = ctx.mkOr(typeSystem.isInt(z3Expr1), typeSystem.isUint(z3Expr1)); + BoolExpr bothIntOrUint = ctx.mkAnd(isIntOrUint0, isIntOrUint1); + + IntExpr val0 = + (IntExpr) + ctx.mkITE( + typeSystem.isInt(z3Expr0), typeSystem.getInt(z3Expr0), typeSystem.getUint(z3Expr0)); + IntExpr val1 = + (IntExpr) + ctx.mkITE( + typeSystem.isInt(z3Expr1), typeSystem.getInt(z3Expr1), typeSystem.getUint(z3Expr1)); + + BoolExpr bothDouble = ctx.mkAnd(typeSystem.isDouble(z3Expr0), typeSystem.isDouble(z3Expr1)); + + return (BoolExpr) + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(bothIntOrUint, ctx.mkEq(val0, val1)) + .addCase( + bothDouble, + ctx.mkFPEq( + (FPExpr) typeSystem.getDouble(z3Expr0), (FPExpr) typeSystem.getDouble(z3Expr1))) + .build(ctx.mkFalse()); + } + + private BoolExpr unrollListEquality( + TranslatedValue listA, TranslatedValue listB, CelAbstractSyntaxTree ast) { + CelExpr literalListAst = + listA.isLiteral(ExprKind.Kind.LIST) ? listA.celExpr().get() : listB.celExpr().get(); + + SeqExpr seq0 = typeSystem.getSeq(typeSystem.getListRef(listA.z3Expr())); + SeqExpr seq1 = typeSystem.getSeq(typeSystem.getListRef(listB.z3Expr())); + + List equalities = new ArrayList<>(); + equalities.add(ctx.mkEq(ctx.mkLength(seq0), ctx.mkLength(seq1))); + + int size = literalListAst.list().elements().size(); + for (int i = 0; i < size; i++) { + Expr elem0 = ctx.mkNth(seq0, ctx.mkInt(i)); + Expr elem1 = ctx.mkNth(seq1, ctx.mkInt(i)); + + TranslatedValue elemA = + TranslatedValue.create(elem0, listA.listElementAt(i), typeSystem, listA.isApproximate()); + TranslatedValue elemB = + TranslatedValue.create(elem1, listB.listElementAt(i), typeSystem, listB.isApproximate()); + + TranslatedValue elemEquality = translateEquals(elemA, elemB, ast); + Expr eqZ3 = elemEquality.z3Expr(); + + BoolExpr isBool = typeSystem.isBool(eqZ3); + BoolExpr isTrue = ctx.mkAnd(isBool, (BoolExpr) typeSystem.unwrapBool(eqZ3)); + + equalities.add(isTrue); + } + + return CelZ3TypeSystem.mkAndFlattened(ctx, equalities); + } + + private TranslatedValue translateEquality( + TranslatedValue arg0, TranslatedValue arg1, CelAbstractSyntaxTree ast, boolean isNotEquals) { + Expr z3Arg0 = arg0.z3Expr(); + Expr z3Arg1 = arg1.z3Expr(); + + CelType type0 = + arg0.celExpr().map(node -> ast.getTypeOrThrow(node.id())).orElse(SimpleType.DYN); + CelType type1 = + arg1.celExpr().map(node -> ast.getTypeOrThrow(node.id())).orElse(SimpleType.DYN); + + BoolExpr equality; + + if (isNumericType(type0) && isNumericType(type1)) { + equality = getNumericEquality(arg0, arg1, ast); + } else if (type0.kind() == CelKind.LIST + && type1.kind() == CelKind.LIST + && (arg0.isLiteral(ExprKind.Kind.LIST) || arg1.isLiteral(ExprKind.Kind.LIST))) { + equality = unrollListEquality(arg0, arg1, ast); + } else if (isStaticallyKnown(type0) && isStaticallyKnown(type1)) { + equality = typeSystem.getStructuralEquality(z3Arg0, z3Arg1); + } else { + BoolExpr bothNumeric = ctx.mkAnd(isNumeric(z3Arg0), isNumeric(z3Arg1)); + + // Check if one side is an explicit LIST that we can unroll + BoolExpr structuralEq = typeSystem.getStructuralEquality(z3Arg0, z3Arg1); + if (arg0.isLiteral(ExprKind.Kind.LIST) || arg1.isLiteral(ExprKind.Kind.LIST)) { + structuralEq = + (BoolExpr) + ctx.mkITE( + ctx.mkAnd(typeSystem.isList(z3Arg0), typeSystem.isList(z3Arg1)), + unrollListEquality(arg0, arg1, ast), + structuralEq); + } + + equality = + (BoolExpr) ctx.mkITE(bothNumeric, getNumericEquality(arg0, arg1, ast), structuralEq); + } + + if (isNotEquals) { + equality = ctx.mkNot(equality); + } + + Expr equalityExpr = typeSystem.wrapBool(equality); + + // If the operands are structurally identical, the equality result is exact (not approximated) + // because X == X is a tautology (or propagates errors/unknowns exactly). + if (z3Arg0.equals(z3Arg1)) { + Expr finalResult = typeSystem.propagateErrorAndUnknown(equalityExpr, z3Arg0); + return TranslatedValue.create(finalResult, typeSystem, ctx.mkFalse()); + } + + BoolExpr bothIntOrUint = + ctx.mkAnd( + ctx.mkOr(typeSystem.isInt(z3Arg0), typeSystem.isUint(z3Arg0)), + ctx.mkOr(typeSystem.isInt(z3Arg1), typeSystem.isUint(z3Arg1))); + BoolExpr bothDouble = ctx.mkAnd(typeSystem.isDouble(z3Arg0), typeSystem.isDouble(z3Arg1)); + BoolExpr bothNumeric = ctx.mkAnd(isNumeric(z3Arg0), isNumeric(z3Arg1)); + BoolExpr sameNumericType = ctx.mkOr(bothIntOrUint, bothDouble); + + // crossNumeric equality is only an approximation if we rely on getDynamicNumericEquality. + // getNumericEqualityWithConstant is exact because it evaluates heterogeneous numeric equality + // accurately. + BoolExpr crossNumeric; + if (arg0.isNumericConstant() || arg1.isNumericConstant()) { + crossNumeric = ctx.mkFalse(); + } else { + crossNumeric = ctx.mkAnd(bothNumeric, ctx.mkNot(sameNumericType)); + } + + return TranslatedValue.propagateStrict(ctx, typeSystem, equalityExpr, arg0, arg1) + .withApproximation(crossNumeric); + } + + private TranslatedValue translateEquals( + TranslatedValue arg0, TranslatedValue arg1, CelAbstractSyntaxTree ast) { + return translateEquality(arg0, arg1, ast, false); + } + + private TranslatedValue translateNotEquals( + TranslatedValue arg0, TranslatedValue arg1, CelAbstractSyntaxTree ast) { + return translateEquality(arg0, arg1, ast, true); + } + + private Expr buildListIndex(Expr lhsTrans, Expr rhsTrans) { + Expr listRef = typeSystem.getListRef(lhsTrans); + SeqExpr seq = typeSystem.getSeq(listRef); + Expr index = ctx.mkApp(typeSystem.intCons().getAccessorDecls()[0], rhsTrans); + BoolExpr inBounds = + ctx.mkAnd( + ctx.mkGe((ArithExpr) index, ctx.mkInt(0)), + ctx.mkLt((ArithExpr) index, ctx.mkLength(seq))); + + Expr val = ctx.mkNth(seq, (ArithExpr) index); + BoolExpr valNotError = ctx.mkNot(ctx.mkEq(val, typeSystem.mkError())); + constraintSink.accept(ctx.mkImplies(inBounds, valNotError)); + if (!allowUnknowns) { + BoolExpr valNotUnknown = ctx.mkNot(ctx.mkEq(val, typeSystem.mkUnknown())); + constraintSink.accept(ctx.mkImplies(inBounds, valNotUnknown)); + } + + return ctx.mkITE(inBounds, val, typeSystem.mkError()); + } + + private Expr buildMapIndex(Expr lhsTrans, Expr rhsTrans) { + Expr mapRef = typeSystem.getMapRef(lhsTrans); + ArrayExpr mapValues = (ArrayExpr) typeSystem.getMapValues(mapRef); + ArrayExpr mapPresence = (ArrayExpr) typeSystem.getMapPresence(mapRef); + BoolExpr inMap = (BoolExpr) ctx.mkSelect(mapPresence, rhsTrans); + + Expr val = ctx.mkSelect(mapValues, rhsTrans); + BoolExpr valNotError = ctx.mkNot(ctx.mkEq(val, typeSystem.mkError())); + constraintSink.accept(ctx.mkImplies(inMap, valNotError)); + if (!allowUnknowns) { + BoolExpr valNotUnknown = ctx.mkNot(ctx.mkEq(val, typeSystem.mkUnknown())); + constraintSink.accept(ctx.mkImplies(inMap, valNotUnknown)); + } + + return ctx.mkITE(inMap, val, typeSystem.mkError()); + } + + private TranslatedValue translateIndex(List args, CelAbstractSyntaxTree ast) { + Expr lhsTrans = args.get(0).z3Expr(); + Expr rhsTrans = args.get(1).z3Expr(); + + TranslatedValue lhs = args.get(0); + TranslatedValue rhs = args.get(1); + CelType lhsType = extractAstTypeOrDefault(lhs, ast); + CelType rhsType = extractAstTypeOrDefault(rhs, ast); + + Expr actualValue; + if (lhsType.kind() == CelKind.LIST && rhsType.kind() == CelKind.INT) { + actualValue = buildListIndex(lhsTrans, rhsTrans); + constraintSink.accept( + ctx.mkImplies( + ctx.mkNot(typeSystem.isError(actualValue)), + typeConstraintGenerator.apply(actualValue, ((ListType) lhsType).elemType()))); + } else if (lhsType.kind() == CelKind.MAP) { + actualValue = buildMapIndex(lhsTrans, rhsTrans); + constraintSink.accept( + ctx.mkImplies( + ctx.mkNot(typeSystem.isError(actualValue)), + typeConstraintGenerator.apply(actualValue, ((MapType) lhsType).valueType()))); + } else { + actualValue = + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase( + ctx.mkAnd(typeSystem.isList(lhsTrans), typeSystem.isInt(rhsTrans)), + buildListIndex(lhsTrans, rhsTrans)) + .addCase(typeSystem.isMap(lhsTrans), buildMapIndex(lhsTrans, rhsTrans)) + .build(typeSystem.mkError()); + } + + return TranslatedValue.propagateStrict(ctx, typeSystem, actualValue, args); + } + + private TranslatedValue translateConditional( + List args, CelAbstractSyntaxTree ast) { + TranslatedValue cond = args.get(0); + TranslatedValue trueBranch = args.get(1); + TranslatedValue falseBranch = args.get(2); + CelType condType = extractAstTypeOrDefault(cond, ast); + + BoolExpr condTrue = ctx.mkAnd(cond.isZ3Bool(), (BoolExpr) cond.unwrapZ3Bool()); + + BoolExpr hasError = cond.isZ3Error(); + BoolExpr hasUnknown = cond.isZ3Unknown(); + if (!condType.equals(SimpleType.BOOL)) { + hasError = ctx.mkOr(hasError, ctx.mkNot(cond.isZ3Bool())); + } + + Expr resultZ3 = + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(hasUnknown, typeSystem.mkUnknown()) + .addCase(hasError, typeSystem.mkError()) + .addCase(condTrue, trueBranch.z3Expr()) + .build(falseBranch.z3Expr()); + + BoolExpr hasSafeError = ctx.mkAnd(hasError, ctx.mkNot(cond.isApproximate())); + BoolExpr hasSafeUnknown = ctx.mkAnd(hasUnknown, ctx.mkNot(cond.isApproximate())); + + BoolExpr resultTaint = + (BoolExpr) + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(hasUnknown, ctx.mkNot(hasSafeUnknown)) + .addCase(hasError, ctx.mkNot(hasSafeError)) + .addCase(condTrue, ctx.mkOr(cond.isApproximate(), trueBranch.isApproximate())) + .build(ctx.mkOr(cond.isApproximate(), falseBranch.isApproximate())); + + return TranslatedValue.create(resultZ3, typeSystem, resultTaint); + } + + private TranslatedValue translateNotStrictlyFalse(List args) { + TranslatedValue arg = args.get(0); + BoolExpr isFalse = ctx.mkAnd(arg.isZ3Bool(), ctx.mkNot((BoolExpr) arg.unwrapZ3Bool())); + return TranslatedValue.propagateStrict( + ctx, typeSystem, typeSystem.wrapBool(ctx.mkNot(isFalse)), arg); + } + + private static CelType extractAstTypeOrDefault(TranslatedValue val, CelAbstractSyntaxTree ast) { + return val.celExpr().map(node -> ast.getTypeOrThrow(node.id())).orElse(SimpleType.DYN); + } + + private static boolean isStaticallyKnown(CelType type) { + CelKind kind = type.kind(); + return !kind.isDyn() && !kind.isTypeParam(); + } + + private static boolean isNumericType(CelType type) { + CelKind kind = type.kind(); + return kind == CelKind.INT || kind == CelKind.UINT || kind == CelKind.DOUBLE; + } + + CelZ3OperatorTranslator( + Context ctx, + CelZ3TypeSystem typeSystem, + Consumer constraintSink, + BiFunction, CelType, BoolExpr> typeConstraintGenerator, + boolean allowUnknowns, + CelZ3FunctionRegistry functionRegistry) { + this.ctx = ctx; + this.typeSystem = typeSystem; + this.constraintSink = constraintSink; + this.typeConstraintGenerator = typeConstraintGenerator; + this.allowUnknowns = allowUnknowns; + this.functionRegistry = functionRegistry; + } +} diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java new file mode 100644 index 000000000..284050d44 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java @@ -0,0 +1,916 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import com.google.common.collect.Lists; +import com.google.common.collect.ObjectArrays; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.microsoft.z3.ArithExpr; +import com.microsoft.z3.ArrayExpr; +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Constructor; +import com.microsoft.z3.Context; +import com.microsoft.z3.DatatypeSort; +import com.microsoft.z3.Expr; +import com.microsoft.z3.FPExpr; +import com.microsoft.z3.FuncDecl; +import com.microsoft.z3.IntExpr; +import com.microsoft.z3.SeqExpr; +import com.microsoft.z3.SeqSort; +import com.microsoft.z3.Sort; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Type system for mapping CEL values to Z3 expressions. + * + *

Thread Safety & Lifetime: This class is NOT thread-safe. Its lifetime is + * strictly bound to a single Z3 {@link Context} during a single verification run. Instances of this + * class must never be cached, stored as fields, shared across threads, or reused across + * multiple verification runs. + * + *

When implementing custom axioms via {@code CelZ3FunctionAxiom}, only use the instance provided + * to {@code translateOverload} for the duration of that method call. Do not leak references to this + * type system or its produced expressions outside of the overload translation. + */ +@SuppressWarnings({"unchecked", "rawtypes", "AvoidObjectArrays"}) // Z3 Java API uses raw types. +public final class CelZ3TypeSystem { + + public static final String MIN_INT64 = "-9223372036854775808"; + public static final String MAX_INT64 = "9223372036854775807"; + public static final String MAX_UINT64 = "18446744073709551615"; + + private static final String TYPE_CEL_VALUE = "CelValue"; + private static final String CONS_BOOL = "Bool"; + private static final String IS_BOOL = "isBool"; + private static final String GET_BOOL = "getBool"; + + private static final String CONS_INT = "Int"; + private static final String IS_INT = "isInt"; + private static final String GET_INT = "getInt"; + + private static final String CONS_UINT = "Uint"; + private static final String IS_UINT = "isUint"; + private static final String GET_UINT = "getUint"; + + private static final String CONS_DOUBLE = "Double"; + private static final String IS_DOUBLE = "isDouble"; + private static final String GET_DOUBLE = "getDouble"; + + private static final String CONS_STRING = "String"; + private static final String IS_STRING = "isString"; + private static final String GET_STRING = "getString"; + + private static final String CONS_BYTES = "Bytes"; + private static final String IS_BYTES = "isBytes"; + private static final String GET_BYTES = "getBytes"; + + private static final String CONS_ERROR = "CelError"; + private static final String IS_ERROR = "isError"; + + private static final String CONS_UNKNOWN = "CelUnknown"; + private static final String IS_UNKNOWN = "isUnknown"; + + private static final String CONS_NULL = "CelNull"; + private static final String IS_NULL = "isNull"; + + private static final String CONS_OPTIONAL = "Optional"; + private static final String IS_OPTIONAL = "isOptional"; + + private static final String SORT_OPTIONAL_REF = "OptionalRef"; + private static final String GET_OPTIONAL_REF = "getOptionalRef"; + private static final String FUNC_OPT_VALUE = "opt_value"; + private static final String FUNC_OPT_OF_REF = "!optionalOfRef"; + + private static final String SORT_LIST_REF = "ListRef"; + private static final String CONS_LIST = "List"; + private static final String IS_LIST = "isList"; + private static final String GET_LIST_REF = "getListRef"; + private static final String FUNC_AS_SEQ = "as_seq"; + + private static final String SORT_MAP_REF = "MapRef"; + private static final String CONS_MAP = "Map"; + private static final String IS_MAP = "isMap"; + private static final String GET_MAP_REF = "getMapRef"; + + private static final String FUNC_MAP_VALUES = "map_values"; + private static final String FUNC_MAP_KEYS = "map_keys"; + private static final String FUNC_MAP_PRESENCE = "map_presence"; + + private static final String SORT_MESSAGE_REF = "MessageRef"; + private static final String CONS_MESSAGE = "Message"; + private static final String IS_MESSAGE = "isMessage"; + private static final String GET_MESSAGE_REF = "getMessageRef"; + + private static final String FUNC_MSG_VALUES = "msg_values"; + private static final String FUNC_MSG_PRESENCE = "msg_presence"; + private static final String FUNC_MSG_TYPE_NAME = "msg_type_name"; + + private final Context ctx; + + private static final class FuncDeclKey { + private final String name; + private final Sort[] domain; + private final Sort range; + + FuncDeclKey(String name, Sort[] domain, Sort range) { + this.name = name; + this.domain = domain; + this.range = range; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FuncDeclKey)) { + return false; + } + FuncDeclKey that = (FuncDeclKey) o; + return name.equals(that.name) + && Arrays.equals(domain, that.domain) + && range.equals(that.range); + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + Arrays.hashCode(domain); + result = 31 * result + range.hashCode(); + return result; + } + } + + private final Map> funcDeclCache = new HashMap<>(); + + private final DatatypeSort celValueSort; + private final Constructor boolCons; + private final Constructor intCons; + private final Constructor uintCons; + private final Constructor doubleCons; + private final Constructor stringCons; + private final Constructor bytesCons; + private final Constructor errorCons; + private final Constructor unknownCons; + private final Constructor nullCons; + private final Constructor optionalCons; + + private final Sort optionalRefSort; + private final FuncDecl optionalValueFunc; + private final FuncDecl optionalOfRefFunc; + + private final Sort listRefSort; + private final Constructor listCons; + private final FuncDecl asSeqFunc; + + private final Sort mapRefSort; + private final Constructor mapCons; + private final FuncDecl mapValuesFunc; + private final FuncDecl mapKeysFunc; + private final FuncDecl mapPresenceFunc; + + private final Sort messageRefSort; + private final Constructor messageCons; + private final FuncDecl msgValuesFunc; + private final FuncDecl msgPresenceFunc; + private final FuncDecl msgTypeNameFunc; + + public Expr mkListRefConst(String prefix) { + return ctx.mkFreshConst(prefix, listRefSort); + } + + public Expr mkMapRefConst(String prefix) { + return ctx.mkFreshConst(prefix, mapRefSort); + } + + public Expr mkMessageRefConst(String prefix) { + return ctx.mkFreshConst(prefix, messageRefSort); + } + + /** + * Interns and retrieves a Z3 function declaration by name and signature. + * + *

Repeated calls with the same name will return the same cached {@link FuncDecl} instance, + * avoiding redundant JNI calls to Z3. + */ + public FuncDecl internFuncDecl(String name, Sort[] domain, Sort range) { + FuncDeclKey cacheKey = new FuncDeclKey(name, domain, range); + return funcDeclCache.computeIfAbsent(cacheKey, k -> ctx.mkFuncDecl(name, domain, range)); + } + + /** Gets the underlying Z3 datatype sort representing a CEL value. */ + public DatatypeSort celValueSort() { + return celValueSort; + } + + public Context ctx() { + return ctx; + } + + /** Gets the uninterpreted sort used as a reference to a list. */ + public Sort listRefSort() { + return listRefSort; + } + + Constructor boolCons() { + return boolCons; + } + + Constructor intCons() { + return intCons; + } + + Constructor uintCons() { + return uintCons; + } + + Constructor doubleCons() { + return doubleCons; + } + + Constructor stringCons() { + return stringCons; + } + + Constructor bytesCons() { + return bytesCons; + } + + /** Creates a CelValue containing a boolean. */ + public Expr mkBool(boolean val) { + return ctx.mkApp(boolCons.ConstructorDecl(), ctx.mkBool(val)); + } + + /** Wraps a Z3 boolean expression into a CelValue. */ + public Expr wrapBool(Expr expr) { + return ctx.mkApp(boolCons.ConstructorDecl(), expr); + } + + /** Wraps a Z3 integer expression into a CelValue. */ + public Expr wrapInt(IntExpr expr) { + return ctx.mkApp(intCons.ConstructorDecl(), expr); + } + + /** Wraps a Z3 string expression into a CelValue. */ + public Expr wrapString(Expr expr) { + return ctx.mkApp(stringCons.ConstructorDecl(), expr); + } + + /** Wraps a Z3 unsigned integer expression into a CelValue. */ + public Expr wrapUint(IntExpr expr) { + return ctx.mkApp(uintCons.ConstructorDecl(), expr); + } + + /** Wraps a Z3 double (floating-point) expression into a CelValue. */ + public Expr wrapDouble(FPExpr expr) { + return ctx.mkApp(doubleCons.ConstructorDecl(), expr); + } + + /** Wraps a Z3 bytes sequence expression into a CelValue. */ + public Expr wrapBytes(Expr expr) { + return ctx.mkApp(bytesCons.ConstructorDecl(), expr); + } + + /** Creates a CelValue containing an integer. */ + public Expr mkInt(long val) { + return ctx.mkApp(intCons.ConstructorDecl(), ctx.mkInt(val)); + } + + /** Creates a CelValue containing an unsigned integer. */ + public Expr mkUint(long val) { + return ctx.mkApp(uintCons.ConstructorDecl(), ctx.mkInt(val)); + } + + /** Creates a CelValue containing a double. */ + public Expr mkDouble(double val) { + return ctx.mkApp(doubleCons.ConstructorDecl(), mkFpDouble(val)); + } + + public FPExpr mkFpDouble(double val) { + return ctx.mkFP(val, ctx.mkFPSortDouble()); + } + + /** Checks if the given CelValue is a double. */ + public BoolExpr isDouble(Expr val) { + return (BoolExpr) ctx.mkApp(doubleCons.getTesterDecl(), val); + } + + /** Extracts the double reference from a double CelValue. */ + public Expr getDouble(Expr val) { + return ctx.mkApp(doubleCons.getAccessorDecls()[0], val); + } + + /** + * Returns a Z3 boolean expression asserting structural equality between two CEL values. + * + *

In CEL, equality for collections and messages is structural (comparing contents), whereas Z3 + * models them as uninterpreted references. This method explicitly unrolls the equality check into + * their underlying Z3 sequences, arrays, and properties, and strictly enforces IEEE-754 semantics + * for floating-point comparisons. + */ + public BoolExpr getStructuralEquality(Expr arg0, Expr arg1) { + // Lists are backed by Z3 Sequences. We assert equality on the underlying Seq objects. + BoolExpr isListEq = ctx.mkAnd(isList(arg0), isList(arg1)); + BoolExpr seqEq = ctx.mkEq(getSeq(getListRef(arg0)), getSeq(getListRef(arg1))); + + // Maps are backed by Z3 Arrays for both values and key presence. + // Two maps are equal if and only if both their presence arrays and value arrays are identical. + BoolExpr isMapEq = ctx.mkAnd(isMap(arg0), isMap(arg1)); + Expr mapRef0 = getMapRef(arg0); + Expr mapRef1 = getMapRef(arg1); + + ArrayExpr mapValues0 = (ArrayExpr) getMapValues(mapRef0); + ArrayExpr mapValues1 = (ArrayExpr) getMapValues(mapRef1); + ArrayExpr mapPresence0 = (ArrayExpr) getMapPresence(mapRef0); + ArrayExpr mapPresence1 = (ArrayExpr) getMapPresence(mapRef1); + + BoolExpr presenceEq = ctx.mkEq(mapPresence0, mapPresence1); + BoolExpr valuesEq = ctx.mkEq(mapValues0, mapValues1); + BoolExpr mapEq = ctx.mkAnd(presenceEq, valuesEq); + + // Messages are backed by Z3 Arrays for field values and presence, plus a type name. + // Two messages are equal if their type names, presence arrays, and value arrays match. + BoolExpr isMsgEq = ctx.mkAnd(isMessage(arg0), isMessage(arg1)); + Expr msgRef0 = getMessageRef(arg0); + Expr msgRef1 = getMessageRef(arg1); + + BoolExpr msgTypeNameEq = ctx.mkEq(getMsgTypeName(msgRef0), getMsgTypeName(msgRef1)); + BoolExpr msgValuesEq = ctx.mkEq(getMsgValues(msgRef0), getMsgValues(msgRef1)); + BoolExpr msgPresenceEq = ctx.mkEq(getMsgPresence(msgRef0), getMsgPresence(msgRef1)); + BoolExpr msgEq = ctx.mkAnd(msgTypeNameEq, msgValuesEq, msgPresenceEq); + + // Doubles must be compared using native floating-point equality to follow IEEE-754. + // Z3's structural mkEq evaluates NaN == NaN as true and 0.0 == -0.0 as false. + BoolExpr isDoubleEq = ctx.mkAnd(isDouble(arg0), isDouble(arg1)); + BoolExpr doubleEq = ctx.mkFPEq((FPExpr) getDouble(arg0), (FPExpr) getDouble(arg1)); + + // For primitives, generic equality matches the direct Z3 datatype wrapper. + BoolExpr genericEq = ctx.mkEq(arg0, arg1); + return (BoolExpr) + SwitchBuilder.newBuilder(ctx) + .addCase(isListEq, seqEq) + .addCase(isMapEq, mapEq) + .addCase(isMsgEq, msgEq) + .addCase(isDoubleEq, doubleEq) + .build(genericEq); + } + + /** Creates a CelValue containing a string. */ + public Expr mkString(String val) { + return ctx.mkApp(stringCons.ConstructorDecl(), ctx.mkString(val)); + } + + /** Creates a CelValue containing bytes. */ + public Expr mkBytes(String val) { + return ctx.mkApp(bytesCons.ConstructorDecl(), ctx.mkString(val)); + } + + Expr wrap(Constructor cons, Expr expr) { + return ctx.mkApp(cons.ConstructorDecl(), expr); + } + + /** Creates a CelValue representing an error. */ + public Expr mkError() { + return ctx.mkConst(errorCons.ConstructorDecl()); + } + + /** Creates a CelValue representing null. */ + public Expr mkNull() { + return ctx.mkConst(nullCons.ConstructorDecl()); + } + + Constructor errorCons() { + return errorCons; + } + + /** Creates a CelValue representing an unknown value. */ + public Expr mkUnknown() { + return ctx.mkConst(unknownCons.ConstructorDecl()); + } + + /** + * Wraps the result in an ITE expression that short-circuits to Error or Unknown. + * + * @see #propagateErrorAndUnknown(Expr, Collection) + */ + Expr propagateErrorAndUnknown(Expr result, Expr... args) { + return propagateErrorAndUnknown(result, Arrays.asList(args)); + } + + /** + * Wraps the result in an ITE expression that short-circuits to Error or Unknown if any of the + * provided arguments evaluate to Error or Unknown. + */ + Expr propagateErrorAndUnknown(Expr result, Collection> args) { + if (args.isEmpty()) { + return result; + } + BoolExpr[] errors = new BoolExpr[args.size()]; + BoolExpr[] unknowns = new BoolExpr[args.size()]; + int i = 0; + for (Expr arg : args) { + errors[i] = isError(arg); + unknowns[i] = isUnknown(arg); + i++; + } + BoolExpr hasError = ctx.mkOr(errors); + BoolExpr hasUnknown = ctx.mkOr(unknowns); + // Unknowns have higher precedence than error + return SwitchBuilder.newBuilder(ctx) + .addCase(hasUnknown, mkUnknown()) + .addCase(hasError, mkError()) + .build(result); + } + + /** + * Wraps the result in an ITE expression that short-circuits to Error if any of the provided + * runtime error conditions are true. + */ + public Expr withRuntimeError( + Expr result, BoolExpr firstCondition, BoolExpr... remainingConditions) { + BoolExpr condition = + remainingConditions.length == 0 + ? firstCondition + : ctx.mkOr(ObjectArrays.concat(firstCondition, remainingConditions)); + return ctx.mkITE(condition, mkError(), result); + } + + Constructor unknownCons() { + return unknownCons; + } + + /** Checks if the given CelValue is an error. */ + public BoolExpr isError(Expr val) { + return (BoolExpr) ctx.mkApp(errorCons.getTesterDecl(), val); + } + + /** Checks if the given CelValue is an unknown value. */ + public BoolExpr isUnknown(Expr val) { + return (BoolExpr) ctx.mkApp(unknownCons.getTesterDecl(), val); + } + + /** Checks if the given CelValue is a boolean. */ + public BoolExpr isBool(Expr val) { + return (BoolExpr) ctx.mkApp(boolCons.getTesterDecl(), val); + } + + /** Extracts the Z3 boolean expression from a boolean CelValue. */ + public Expr unwrapBool(Expr val) { + return ctx.mkApp(boolCons.getAccessorDecls()[0], val); + } + + /** Creates a CelValue representing optional.none(). */ + public Expr mkOptionalNone() { + return ctx.mkApp(optionalCons.ConstructorDecl(), mkNoneOptionalRef()); + } + + /** Gets the globally unique constant representing the reference of an empty optional. */ + public Expr mkNoneOptionalRef() { + return ctx.mkConst("!optionalNoneRef", optionalRefSort); + } + + /** Creates a CelValue representing optional.of(ref). */ + public Expr mkOptionalOf(Expr ref) { + return ctx.mkApp(optionalCons.ConstructorDecl(), ref); + } + + /** Gets the uninterpreted function used to compute the optional reference for a given value. */ + public FuncDecl optionalOfRefFunc() { + return optionalOfRefFunc; + } + + /** Checks if the given CelValue is an optional. */ + public BoolExpr isOptional(Expr val) { + return (BoolExpr) ctx.mkApp(optionalCons.getTesterDecl(), val); + } + + /** Extracts the OptionalRef expression from an optional CelValue. */ + public Expr getOptionalRef(Expr val) { + return ctx.mkApp(optionalCons.getAccessorDecls()[0], val); + } + + /** Checks if the given optional reference contains a value. */ + public BoolExpr optHasValue(Expr optRef) { + return ctx.mkNot(ctx.mkEq(optRef, mkNoneOptionalRef())); + } + + /** Gets the value of the optional from its reference. */ + public Expr getOptionalValue(Expr optRef) { + return ctx.mkApp(optionalValueFunc, optRef); + } + + /** Checks if the given CelValue is an integer. */ + public BoolExpr isInt(Expr val) { + return (BoolExpr) ctx.mkApp(intCons.getTesterDecl(), val); + } + + /** Extracts the integer expression from an integer CelValue. */ + public IntExpr getInt(Expr val) { + return (IntExpr) ctx.mkApp(intCons.getAccessorDecls()[0], val); + } + + /** Checks if the given CelValue is an unsigned integer. */ + public BoolExpr isUint(Expr val) { + return (BoolExpr) ctx.mkApp(uintCons.getTesterDecl(), val); + } + + /** Extracts the integer expression from an unsigned integer CelValue. */ + public IntExpr getUint(Expr val) { + return (IntExpr) ctx.mkApp(uintCons.getAccessorDecls()[0], val); + } + + /** Checks if the given CelValue is a string. */ + public BoolExpr isString(Expr val) { + return (BoolExpr) ctx.mkApp(stringCons.getTesterDecl(), val); + } + + /** Checks if the given CelValue is bytes. */ + public BoolExpr isBytes(Expr val) { + return (BoolExpr) ctx.mkApp(bytesCons.getTesterDecl(), val); + } + + /** Extracts the Z3 sequence expression from a string CelValue. */ + public Expr getString(Expr val) { + return ctx.mkApp(stringCons.getAccessorDecls()[0], val); + } + + /** Extracts the Z3 sequence expression from a bytes CelValue. */ + public Expr getBytes(Expr val) { + return ctx.mkApp(bytesCons.getAccessorDecls()[0], val); + } + + /** Checks if the given CelValue is a struct (message). */ + public BoolExpr isStruct(Expr val) { + return isMessage(val); + } + + /** Checks if the given CelValue is null. */ + public BoolExpr isNull(Expr val) { + return (BoolExpr) ctx.mkApp(nullCons.getTesterDecl(), val); + } + + /** Wraps a list reference into a CelValue. */ + public Expr wrapList(Expr listRef) { + return ctx.mkApp(listCons.ConstructorDecl(), listRef); + } + + Constructor listCons() { + return listCons; + } + + /** Checks if the given CelValue is a list. */ + public BoolExpr isList(Expr val) { + return (BoolExpr) ctx.mkApp(listCons.getTesterDecl(), val); + } + + /** Extracts the list reference from a list CelValue. */ + public Expr getListRef(Expr val) { + return ctx.mkApp(listCons.getAccessorDecls()[0], val); + } + + /** Gets the sequence expression corresponding to the given list reference. */ + public SeqExpr getSeq(Expr listRef) { + return (SeqExpr) ctx.mkApp(asSeqFunc, listRef); + } + + /** Gets the uninterpreted sort used as a reference to a map. */ + public Sort mapRefSort() { + return mapRefSort; + } + + Constructor mapCons() { + return mapCons; + } + + /** Wraps a map reference into a CelValue. */ + public Expr wrapMap(Expr mapRef) { + return ctx.mkApp(mapCons.ConstructorDecl(), mapRef); + } + + /** Checks if the given CelValue is a map. */ + public BoolExpr isMap(Expr val) { + return (BoolExpr) ctx.mkApp(mapCons.getTesterDecl(), val); + } + + /** Extracts the map reference from a map CelValue. */ + public Expr getMapRef(Expr val) { + return ctx.mkApp(mapCons.getAccessorDecls()[0], val); + } + + /** Gets the array of values corresponding to the given map reference. */ + public Expr getMapValues(Expr mapRef) { + return ctx.mkApp(mapValuesFunc, mapRef); + } + + /** Gets the sequence of keys corresponding to the given map reference. */ + public SeqExpr getMapKeys(Expr mapRef) { + return (SeqExpr) ctx.mkApp(mapKeysFunc, mapRef); + } + + /** Gets the presence array corresponding to the given map reference. */ + public Expr getMapPresence(Expr mapRef) { + return ctx.mkApp(mapPresenceFunc, mapRef); + } + + /** Gets the uninterpreted sort used as a reference to a message. */ + public Sort messageRefSort() { + return messageRefSort; + } + + Constructor messageCons() { + return messageCons; + } + + /** Wraps a message reference into a CelValue. */ + public Expr wrapMessage(Expr msgRef) { + return ctx.mkApp(messageCons.ConstructorDecl(), msgRef); + } + + /** Checks if the given CelValue is a message. */ + public BoolExpr isMessage(Expr val) { + return (BoolExpr) ctx.mkApp(messageCons.getTesterDecl(), val); + } + + /** Extracts the message reference from a message CelValue. */ + public Expr getMessageRef(Expr val) { + return ctx.mkApp(messageCons.getAccessorDecls()[0], val); + } + + /** Gets the array of field values corresponding to the given message reference. */ + public Expr getMsgValues(Expr msgRef) { + return ctx.mkApp(msgValuesFunc, msgRef); + } + + /** Gets the presence array corresponding to the given message reference. */ + public Expr getMsgPresence(Expr msgRef) { + return ctx.mkApp(msgPresenceFunc, msgRef); + } + + /** Gets the type name string expression corresponding to the given message reference. */ + public Expr getMsgTypeName(Expr msgRef) { + return ctx.mkApp(msgTypeNameFunc, msgRef); + } + + /** Checks if the given arithmetic expression overflows a 64-bit integer. */ + public BoolExpr checkIntOverflow(ArithExpr result) { + return ctx.mkOr(ctx.mkGt(result, ctx.mkInt(MAX_INT64)), ctx.mkLt(result, ctx.mkInt(MIN_INT64))); + } + + /** Checks if the given arithmetic expression overflows a 64-bit unsigned integer. */ + public BoolExpr checkUintOverflow(ArithExpr result) { + return ctx.mkOr(ctx.mkGt(result, ctx.mkInt(MAX_UINT64)), ctx.mkLt(result, ctx.mkInt(0))); + } + + /** Safely concatenates two Z3 sequences. */ + @SuppressWarnings("unchecked") // Callers verify arguments are SeqExprs. + public SeqExpr mkConcatSafe(Expr arg1, Expr arg2) { + return ctx.mkConcat((Expr>) arg1, (Expr>) arg2); + } + + /** + * Helper to build a chain of nested ITE (If-Then-Else) conditions. + * + *

Conditions are evaluated in the order they are added. + */ + public static final class SwitchBuilder { + + private static final class SwitchCase { + final BoolExpr condition; + final Expr value; + + SwitchCase(BoolExpr condition, Expr value) { + this.condition = condition; + this.value = value; + } + } + + private final Context ctx; + private final List cases; + + public static SwitchBuilder newBuilder(Context ctx) { + return new SwitchBuilder(ctx); + } + + @CanIgnoreReturnValue + public SwitchBuilder addCase(BoolExpr condition, Expr value) { + cases.add(new SwitchCase(condition, value)); + return this; + } + + public Expr build(Expr defaultFallback) { + Expr result = defaultFallback; + for (SwitchCase c : Lists.reverse(cases)) { + result = ctx.mkITE(c.condition, c.value, result); + } + return result; + } + + private SwitchBuilder(Context ctx) { + this.ctx = ctx; + this.cases = new ArrayList<>(); + } + } + + /** + * Helper to construct a flattened logical OR expression to avoid deep left-leaning ASTs. + * + *

Returns {@code false} if the list is empty. + */ + public static BoolExpr mkOrFlattened(Context ctx, List args) { + // Pruning true/false constants in Java is significantly faster than building + // larger ASTs and letting Z3 process them natively. + List filteredArgs = new ArrayList<>(); + for (BoolExpr arg : args) { + if (arg.isTrue()) { + return ctx.mkTrue(); + } + if (!arg.isFalse()) { + filteredArgs.add(arg); + } + } + if (filteredArgs.isEmpty()) { + return ctx.mkFalse(); + } + if (filteredArgs.size() == 1) { + return filteredArgs.get(0); + } + return ctx.mkOr(filteredArgs.toArray(new BoolExpr[0])); + } + + /** + * Helper to construct a flattened logical AND expression to avoid deep left-leaning ASTs. + * + *

Returns {@code true} if the list is empty. + */ + public static BoolExpr mkAndFlattened(Context ctx, List args) { + // Pruning true/false constants in Java is significantly faster than building + // larger ASTs and letting Z3 process them natively. + List filteredArgs = new ArrayList<>(); + for (BoolExpr arg : args) { + if (arg.isFalse()) { + return ctx.mkFalse(); + } + if (!arg.isTrue()) { + filteredArgs.add(arg); + } + } + if (filteredArgs.isEmpty()) { + return ctx.mkTrue(); + } + if (filteredArgs.size() == 1) { + return filteredArgs.get(0); + } + return ctx.mkAnd(filteredArgs.toArray(new BoolExpr[0])); + } + + /** Helper to construct a logical NOT expression while avoiding redundant NOT operations. */ + public static BoolExpr mkNotFlattened(Context ctx, BoolExpr arg) { + if (arg.isTrue()) { + return ctx.mkFalse(); + } + if (arg.isFalse()) { + return ctx.mkTrue(); + } + return ctx.mkNot(arg); + } + + CelZ3TypeSystem(Context ctx) { + this.ctx = ctx; + this.boolCons = + ctx.mkConstructor( + CONS_BOOL, IS_BOOL, new String[] {GET_BOOL}, new Sort[] {ctx.getBoolSort()}, null); + // Note: Z3's IntSort models unbounded mathematical integers. We do not currently use + // BitVecSort(64), which means CEL integer overflow semantics are not natively modeled, + // and bitwise operations are unsupported. We enforce 64-bit value bounds explicitly + // during variable constraint generation instead. + this.intCons = + ctx.mkConstructor( + CONS_INT, IS_INT, new String[] {GET_INT}, new Sort[] {ctx.getIntSort()}, null); + this.uintCons = + ctx.mkConstructor( + CONS_UINT, IS_UINT, new String[] {GET_UINT}, new Sort[] {ctx.getIntSort()}, null); + this.doubleCons = + ctx.mkConstructor( + CONS_DOUBLE, + IS_DOUBLE, + new String[] {GET_DOUBLE}, + new Sort[] {ctx.mkFPSortDouble()}, + null); + this.stringCons = + ctx.mkConstructor( + CONS_STRING, + IS_STRING, + new String[] {GET_STRING}, + new Sort[] {ctx.getStringSort()}, + null); + this.bytesCons = + ctx.mkConstructor( + CONS_BYTES, IS_BYTES, new String[] {GET_BYTES}, new Sort[] {ctx.getStringSort()}, null); + this.errorCons = ctx.mkConstructor(CONS_ERROR, IS_ERROR, null, null, null); + this.unknownCons = ctx.mkConstructor(CONS_UNKNOWN, IS_UNKNOWN, null, null, null); + this.nullCons = ctx.mkConstructor(CONS_NULL, IS_NULL, null, null, null); + this.optionalRefSort = ctx.mkUninterpretedSort(SORT_OPTIONAL_REF); + this.optionalCons = + ctx.mkConstructor( + CONS_OPTIONAL, + IS_OPTIONAL, + new String[] {GET_OPTIONAL_REF}, + new Sort[] {optionalRefSort}, + null); + + this.listRefSort = ctx.mkUninterpretedSort(SORT_LIST_REF); + this.listCons = + ctx.mkConstructor( + CONS_LIST, IS_LIST, new String[] {GET_LIST_REF}, new Sort[] {this.listRefSort}, null); + + this.mapRefSort = ctx.mkUninterpretedSort(SORT_MAP_REF); + this.mapCons = + ctx.mkConstructor( + CONS_MAP, IS_MAP, new String[] {GET_MAP_REF}, new Sort[] {this.mapRefSort}, null); + + this.messageRefSort = ctx.mkUninterpretedSort(SORT_MESSAGE_REF); + this.messageCons = + ctx.mkConstructor( + CONS_MESSAGE, + IS_MESSAGE, + new String[] {GET_MESSAGE_REF}, + new Sort[] {this.messageRefSort}, + null); + + this.celValueSort = + ctx.mkDatatypeSort( + TYPE_CEL_VALUE, + new Constructor[] { + this.boolCons, + this.intCons, + this.uintCons, + this.doubleCons, + this.stringCons, + this.bytesCons, + this.errorCons, + this.unknownCons, + this.optionalCons, + this.listCons, + this.mapCons, + this.messageCons, + this.nullCons + }); + + this.optionalValueFunc = + ctx.mkFuncDecl(FUNC_OPT_VALUE, new Sort[] {this.optionalRefSort}, this.celValueSort); + this.optionalOfRefFunc = + ctx.mkFuncDecl(FUNC_OPT_OF_REF, new Sort[] {this.celValueSort}, this.optionalRefSort); + + // Java specific workaround: Z3 Java API prevents recursive Datatype constructors from taking + // SeqSort or ArraySort. + // We wrap an opaque 'ListRef' or 'MapRef' inside the datatype instead, and map them + // to native Z3 sequences/arrays using these uninterpreted functions. + this.asSeqFunc = + ctx.mkFuncDecl( + FUNC_AS_SEQ, new Sort[] {this.listRefSort}, ctx.mkSeqSort(this.celValueSort)); + + this.mapValuesFunc = + ctx.mkFuncDecl( + FUNC_MAP_VALUES, + new Sort[] {this.mapRefSort}, + ctx.mkArraySort(this.celValueSort, this.celValueSort)); + this.mapKeysFunc = + ctx.mkFuncDecl( + FUNC_MAP_KEYS, new Sort[] {this.mapRefSort}, ctx.mkSeqSort(this.celValueSort)); + this.mapPresenceFunc = + ctx.mkFuncDecl( + FUNC_MAP_PRESENCE, + new Sort[] {this.mapRefSort}, + ctx.mkArraySort(this.celValueSort, ctx.getBoolSort())); + + this.msgValuesFunc = + ctx.mkFuncDecl( + FUNC_MSG_VALUES, + new Sort[] {this.messageRefSort}, + ctx.mkArraySort(ctx.getStringSort(), this.celValueSort)); + this.msgPresenceFunc = + ctx.mkFuncDecl( + FUNC_MSG_PRESENCE, + new Sort[] {this.messageRefSort}, + ctx.mkArraySort(ctx.getStringSort(), ctx.getBoolSort())); + this.msgTypeNameFunc = + ctx.mkFuncDecl(FUNC_MSG_TYPE_NAME, new Sort[] {this.messageRefSort}, ctx.getStringSort()); + } +} diff --git a/verifier/src/main/java/dev/cel/verifier/TranslatedValue.java b/verifier/src/main/java/dev/cel/verifier/TranslatedValue.java new file mode 100644 index 000000000..336e5a7ed --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/TranslatedValue.java @@ -0,0 +1,217 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import com.google.auto.value.AutoValue; +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Context; +import com.microsoft.z3.Expr; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.ast.CelExpr.ExprKind; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +/** Encapsulates a Z3 expression and its corresponding CEL AST node. */ +@AutoValue +abstract class TranslatedValue { + abstract Expr z3Expr(); + + abstract Optional celExpr(); + + abstract CelZ3TypeSystem typeSystem(); + + abstract BoolExpr isApproximate(); + + /** Safely checks if this is a specific literal type */ + boolean isLiteral(ExprKind.Kind kind) { + return celExpr().map(node -> node.exprKind().getKind() == kind).orElse(false); + } + + /** Safely extracts a list element AST if it exists */ + Optional listElementAt(int index) { + return celExpr() + .filter(node -> node.exprKind().getKind() == ExprKind.Kind.LIST) + .filter(node -> index < node.list().elements().size()) + .map(node -> node.list().elements().get(index)); + } + + boolean isNumericConstant() { + return celExpr() + .filter(node -> node.exprKind().getKind() == ExprKind.Kind.CONSTANT) + .map(node -> node.constant().getKind()) + .map( + kind -> + kind == CelConstant.Kind.INT64_VALUE + || kind == CelConstant.Kind.UINT64_VALUE + || kind == CelConstant.Kind.DOUBLE_VALUE) + .orElse(false); + } + + BoolExpr isZ3Bool() { + return typeSystem().isBool(z3Expr()); + } + + Expr unwrapZ3Bool() { + return typeSystem().unwrapBool(z3Expr()); + } + + BoolExpr isZ3Error() { + return typeSystem().isError(z3Expr()); + } + + BoolExpr isZ3Unknown() { + return typeSystem().isUnknown(z3Expr()); + } + + static TranslatedValue create( + Expr z3Expr, CelExpr celExpr, CelZ3TypeSystem typeSystem, BoolExpr isApproximate) { + return new AutoValue_TranslatedValue(z3Expr, Optional.of(celExpr), typeSystem, isApproximate); + } + + static TranslatedValue create( + Expr z3Expr, + Optional celExpr, + CelZ3TypeSystem typeSystem, + BoolExpr isApproximate) { + return new AutoValue_TranslatedValue(z3Expr, celExpr, typeSystem, isApproximate); + } + + static TranslatedValue create( + Expr z3Expr, CelZ3TypeSystem typeSystem, BoolExpr isApproximate) { + return new AutoValue_TranslatedValue(z3Expr, Optional.empty(), typeSystem, isApproximate); + } + + /** + * Applies strict CEL evaluation semantics. + * + *

If any argument is an Exact Error, the result safely short-circuits to Error. + * + *

If any argument is an Exact Unknown (and no Errors exist), it safely short-circuits to + * Unknown. + * + *

Otherwise, it computes whether the final result is tainted by any approximate values. + */ + static TranslatedValue propagateStrict( + Context ctx, CelZ3TypeSystem ts, Expr baseResult, Collection args) { + return propagateStrict(ctx, ts, baseResult, Optional.empty(), args); + } + + static TranslatedValue propagateStrict( + Context ctx, CelZ3TypeSystem ts, Expr baseResult, TranslatedValue... args) { + return propagateStrict(ctx, ts, baseResult, Optional.empty(), Arrays.asList(args)); + } + + static TranslatedValue propagateStrict( + Context ctx, + CelZ3TypeSystem ts, + Expr baseResult, + CelExpr celExpr, + Collection args) { + return propagateStrict(ctx, ts, baseResult, Optional.of(celExpr), args); + } + + static TranslatedValue propagateStrict( + Context ctx, + CelZ3TypeSystem ts, + Expr baseResult, + Optional celExpr, + Collection args) { + return propagateStrict(ctx, ts, baseResult, celExpr, ctx.mkFalse(), args); + } + + static TranslatedValue propagateStrict( + Context ctx, + CelZ3TypeSystem ts, + Expr baseResult, + Optional celExpr, + BoolExpr baseTaint, + Collection args) { + List exactErrors = new ArrayList<>(); + List exactUnknowns = new ArrayList<>(); + List errors = new ArrayList<>(); + List unknowns = new ArrayList<>(); + List taints = new ArrayList<>(); + taints.add(baseTaint); + + boolean hasNonConstantArgs = false; + for (TranslatedValue arg : args) { + taints.add(arg.isApproximate()); + if (arg.isLiteral(ExprKind.Kind.CONSTANT)) { + continue; + } + hasNonConstantArgs = true; + + Expr z3Expr = arg.z3Expr(); + BoolExpr isApprox = arg.isApproximate(); + BoolExpr isError = ts.isError(z3Expr); + BoolExpr isUnknown = ts.isUnknown(z3Expr); + + errors.add(isError); + unknowns.add(isUnknown); + + exactErrors.add( + CelZ3TypeSystem.mkAndFlattened( + ctx, Arrays.asList(isError, CelZ3TypeSystem.mkNotFlattened(ctx, isApprox)))); + exactUnknowns.add( + CelZ3TypeSystem.mkAndFlattened( + ctx, Arrays.asList(isUnknown, CelZ3TypeSystem.mkNotFlattened(ctx, isApprox)))); + } + + BoolExpr anyTaint = CelZ3TypeSystem.mkOrFlattened(ctx, taints); + if (!hasNonConstantArgs) { + return create(baseResult, celExpr, ts, anyTaint); + } + + BoolExpr hasExactError = CelZ3TypeSystem.mkOrFlattened(ctx, exactErrors); + BoolExpr hasExactUnknown = CelZ3TypeSystem.mkOrFlattened(ctx, exactUnknowns); + BoolExpr hasError = CelZ3TypeSystem.mkOrFlattened(ctx, errors); + BoolExpr hasUnknown = CelZ3TypeSystem.mkOrFlattened(ctx, unknowns); + + Expr finalResult = + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(hasUnknown, ts.mkUnknown()) + .addCase(hasError, ts.mkError()) + .build(baseResult); + + BoolExpr isSafe = + CelZ3TypeSystem.mkOrFlattened( + ctx, + Arrays.asList( + hasExactUnknown, + CelZ3TypeSystem.mkAndFlattened( + ctx, + Arrays.asList(hasExactError, CelZ3TypeSystem.mkNotFlattened(ctx, hasUnknown))), + CelZ3TypeSystem.mkNotFlattened(ctx, anyTaint))); + + return create(finalResult, celExpr, ts, CelZ3TypeSystem.mkNotFlattened(ctx, isSafe)); + } + + /** + * Returns a new TranslatedValue with an additional approximation condition OR'd into the + * approximation flag. + */ + TranslatedValue withApproximation(BoolExpr approxCondition) { + return create( + z3Expr(), + celExpr(), + typeSystem(), + typeSystem().ctx().mkOr(isApproximate(), approxCondition)); + } +} + diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/AddAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/AddAxiom.java new file mode 100644 index 000000000..072f75088 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/AddAxiom.java @@ -0,0 +1,100 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Expr; +import com.microsoft.z3.FPExpr; +import com.microsoft.z3.FuncDecl; +import com.microsoft.z3.IntExpr; +import com.microsoft.z3.SeqExpr; +import com.microsoft.z3.Sort; +import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import java.util.Optional; + +/** Axiomatization for CEL's addition operator (+). */ +final class AddAxiom { + + static final CelZ3FunctionAxiom INSTANCE = + CelZ3FunctionAxiom.newBuilder(StandardFunction.ADD.functionDecl()) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.ADD_INT64.celOverloadDecl(), + (ctx, ts, sink, l, r) -> { + IntExpr a1 = ts.getInt(l); + IntExpr a2 = ts.getInt(r); + Expr result = ts.wrapInt((IntExpr) ctx.mkAdd(a1, a2)); + BoolExpr overflow = ts.checkIntOverflow(ctx.mkAdd(a1, a2)); + return Optional.of(ts.withRuntimeError(result, overflow)); + }) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.ADD_UINT64.celOverloadDecl(), + (ctx, ts, sink, l, r) -> { + IntExpr a1 = ts.getUint(l); + IntExpr a2 = ts.getUint(r); + Expr result = ts.wrapUint((IntExpr) ctx.mkAdd(a1, a2)); + BoolExpr overflow = ts.checkUintOverflow(ctx.mkAdd(a1, a2)); + return Optional.of(ts.withRuntimeError(result, overflow)); + }) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.ADD_DOUBLE.celOverloadDecl(), + (ctx, ts, sink, l, r) -> + Optional.of( + ts.wrapDouble( + ctx.mkFPAdd( + ctx.mkFPRoundNearestTiesToEven(), + (FPExpr) ts.getDouble(l), + (FPExpr) ts.getDouble(r))))) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.ADD_STRING.celOverloadDecl(), + (ctx, ts, sink, l, r) -> + Optional.of(ts.wrapString(ts.mkConcatSafe(ts.getString(l), ts.getString(r))))) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.ADD_BYTES.celOverloadDecl(), + (ctx, ts, sink, l, r) -> + Optional.of(ts.wrapBytes(ts.mkConcatSafe(ts.getBytes(l), ts.getBytes(r))))) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.ADD_LIST.celOverloadDecl(), + (ctx, ts, sink, l, r) -> { + Expr listRef1 = ts.getListRef(l); + Expr listRef2 = ts.getListRef(r); + + FuncDecl listAddFunc = + ts.internFuncDecl( + StandardFunction.Overload.Arithmetic.ADD_LIST + .celOverloadDecl() + .overloadId(), + new Sort[] {ts.listRefSort(), ts.listRefSort()}, + ts.listRefSort()); + + // The result of list_add(l1, l2) is a new list + Expr resultListRef = ctx.mkApp(listAddFunc, listRef1, listRef2); + Expr resultCelValue = ts.wrapList(resultListRef); + + SeqExpr seq1 = ts.getSeq(listRef1); + SeqExpr seq2 = ts.getSeq(listRef2); + SeqExpr seqRes = ts.getSeq(resultListRef); + + // We add a constraint that the sequence representation of the result is the + // concatenation of the sequence representation of l1 and l2. + BoolExpr typeGuard = ctx.mkAnd(ts.isList(l), ts.isList(r)); + BoolExpr constraint = ctx.mkEq(seqRes, ts.mkConcatSafe(seq1, seq2)); + sink.accept(ctx.mkImplies(typeGuard, constraint)); + + return Optional.of(resultCelValue); + }) + .build(); + + private AddAxiom() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/AxiomHelpers.java b/verifier/src/main/java/dev/cel/verifier/axioms/AxiomHelpers.java new file mode 100644 index 000000000..a249b4fe9 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/AxiomHelpers.java @@ -0,0 +1,51 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Context; +import com.microsoft.z3.IntExpr; + +/** Helper methods for Z3 axioms operations. */ +final class AxiomHelpers { + + /** + * Translates CEL's truncated integer division semantics into Z3. + * + *

Z3's `mkDiv` uses Euclidean division (floor division for positive divisors), while CEL + * requires truncation towards zero. + */ + static IntExpr mkTruncatedDiv(Context ctx, IntExpr a, IntExpr b) { + IntExpr zero = ctx.mkInt(0); + BoolExpr aIsNeg = ctx.mkLt(a, zero); + BoolExpr bIsNeg = ctx.mkLt(b, zero); + IntExpr absA = (IntExpr) ctx.mkITE(aIsNeg, ctx.mkUnaryMinus(a), a); + IntExpr absB = (IntExpr) ctx.mkITE(bIsNeg, ctx.mkUnaryMinus(b), b); + IntExpr divAbs = (IntExpr) ctx.mkDiv(absA, absB); + BoolExpr diffSign = ctx.mkXor(aIsNeg, bIsNeg); + return (IntExpr) ctx.mkITE(diffSign, ctx.mkUnaryMinus(divAbs), divAbs); + } + + /** + * Translates CEL's truncated integer modulo semantics into Z3. + * + *

Defined as `a - (a / b) * b` using truncated division. + */ + static IntExpr mkTruncatedMod(Context ctx, IntExpr a, IntExpr b) { + return (IntExpr) ctx.mkSub(a, ctx.mkMul(mkTruncatedDiv(ctx, a, b), b)); + } + + private AxiomHelpers() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/BUILD.bazel b/verifier/src/main/java/dev/cel/verifier/axioms/BUILD.bazel new file mode 100644 index 000000000..d5f9bf74a --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/BUILD.bazel @@ -0,0 +1,27 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//verifier/axioms:__pkg__"], +) + +java_library( + name = "axioms", + srcs = glob(["*.java"]), + compatible_with = [], + tags = [ + ], + deps = [ + "//:auto_value", + "//checker:standard_decl", + "//common:compiler_common", + "//common/annotations", + "//common/types", + "//extensions:comprehensions", + "//extensions:optional_library", + "//verifier:type_system", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:tools_aqua_z3_turnkey", + ], +) diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/CelZ3FunctionAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/CelZ3FunctionAxiom.java new file mode 100644 index 000000000..2d1129618 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/CelZ3FunctionAxiom.java @@ -0,0 +1,190 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.Immutable; +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Context; +import com.microsoft.z3.Expr; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOverloadDecl; +import dev.cel.verifier.CelZ3TypeSystem; +import java.util.Optional; +import java.util.function.Consumer; + +/** Defines a self-contained translation and axiomatization for a specific CEL function. */ +@AutoValue +@Immutable +public abstract class CelZ3FunctionAxiom { + + /** Functional interface for translating unary overloads. */ + @FunctionalInterface + @Immutable + public interface UnaryTranslator { + Optional> translate( + Context ctx, CelZ3TypeSystem typeSystem, Consumer constraintSink, Expr arg); + } + + /** Functional interface for translating binary overloads. */ + @FunctionalInterface + @Immutable + public interface BinaryTranslator { + Optional> translate( + Context ctx, + CelZ3TypeSystem typeSystem, + Consumer constraintSink, + Expr arg1, + Expr arg2); + } + + /** Functional interface for translating ternary overloads. */ + @FunctionalInterface + @Immutable + public interface TernaryTranslator { + Optional> translate( + Context ctx, + CelZ3TypeSystem typeSystem, + Consumer constraintSink, + Expr arg1, + Expr arg2, + Expr arg3); + } + + /** The canonical CEL function declaration handled by this axiom. */ + public abstract CelFunctionDecl declaration(); + + /** Mapping from overloadId to its SMT translation strategy. */ + public abstract ImmutableMap overloadTranslators(); + + public static Builder newBuilder(CelFunctionDecl declaration) { + return new AutoValue_CelZ3FunctionAxiom.Builder().setDeclaration(declaration); + } + + /** Builder for {@link CelZ3FunctionAxiom}. */ + @AutoValue.Builder + public abstract static class Builder { + @CanIgnoreReturnValue + public abstract Builder setDeclaration(CelFunctionDecl value); + + abstract ImmutableMap.Builder overloadTranslatorsBuilder(); + + @CanIgnoreReturnValue + public Builder addOverloadTranslator(String overloadId, CelZ3OverloadTranslator translator) { + overloadTranslatorsBuilder().put(overloadId, translator); + return this; + } + + @CanIgnoreReturnValue + public Builder addOverloadTranslator( + CelOverloadDecl overloadDecl, CelZ3OverloadTranslator translator) { + return addOverloadTranslator(overloadDecl.overloadId(), translator); + } + + @CanIgnoreReturnValue + public Builder addUnaryOverloadTranslator( + String overloadId, UnaryTranslator translator, boolean isApproximated) { + CelZ3OverloadTranslator overloadTranslator = + (ctx, ts, sink, args, argApproximations) -> { + Preconditions.checkArgument( + args.size() == 1, "%s overload requires exactly 1 argument", overloadId); + Optional> res = translator.translate(ctx, ts, sink, args.get(0)); + if (!res.isPresent()) { + return Optional.empty(); + } + Expr val = res.get(); + BoolExpr approx = argApproximations.get(0); + if (isApproximated) { + BoolExpr isErrorOrUnknown = ctx.mkOr(ts.isError(val), ts.isUnknown(val)); + approx = (BoolExpr) ctx.mkITE(isErrorOrUnknown, approx, ctx.mkTrue()); + } + return Optional.of(CelZ3OverloadResult.create(val, approx)); + }; + return addOverloadTranslator(overloadId, overloadTranslator); + } + + @CanIgnoreReturnValue + public Builder addUnaryOverloadTranslator( + CelOverloadDecl overloadDecl, UnaryTranslator translator, boolean isApproximated) { + return addUnaryOverloadTranslator(overloadDecl.overloadId(), translator, isApproximated); + } + + @CanIgnoreReturnValue + public Builder addUnaryOverloadTranslator(String overloadId, UnaryTranslator translator) { + return addUnaryOverloadTranslator(overloadId, translator, false); + } + + @CanIgnoreReturnValue + public Builder addUnaryOverloadTranslator( + CelOverloadDecl overloadDecl, UnaryTranslator translator) { + return addUnaryOverloadTranslator(overloadDecl.overloadId(), translator, false); + } + + @CanIgnoreReturnValue + public Builder addBinaryOverloadTranslator(String overloadId, BinaryTranslator translator) { + return addOverloadTranslator( + overloadId, + (ctx, ts, sink, args, argApproximations) -> { + Preconditions.checkArgument( + args.size() == 2, "%s overload requires exactly 2 arguments", overloadId); + Optional> res = translator.translate(ctx, ts, sink, args.get(0), args.get(1)); + if (!res.isPresent()) { + return Optional.empty(); + } + Expr val = res.get(); + BoolExpr approx = ctx.mkOr(argApproximations.get(0), argApproximations.get(1)); + return Optional.of(CelZ3OverloadResult.create(val, approx)); + }); + } + + @CanIgnoreReturnValue + public Builder addBinaryOverloadTranslator( + CelOverloadDecl overloadDecl, BinaryTranslator translator) { + return addBinaryOverloadTranslator(overloadDecl.overloadId(), translator); + } + + @CanIgnoreReturnValue + public Builder addTernaryOverloadTranslator(String overloadId, TernaryTranslator translator) { + return addOverloadTranslator( + overloadId, + (ctx, ts, sink, args, argApproximations) -> { + Preconditions.checkArgument( + args.size() == 3, "%s overload requires exactly 3 arguments", overloadId); + Optional> res = + translator.translate(ctx, ts, sink, args.get(0), args.get(1), args.get(2)); + if (!res.isPresent()) { + return Optional.empty(); + } + Expr val = res.get(); + BoolExpr approx = + ctx.mkOr( + argApproximations.get(0), + ctx.mkOr(argApproximations.get(1), argApproximations.get(2))); + return Optional.of(CelZ3OverloadResult.create(val, approx)); + }); + } + + @CanIgnoreReturnValue + public Builder addTernaryOverloadTranslator( + CelOverloadDecl overloadDecl, TernaryTranslator translator) { + return addTernaryOverloadTranslator(overloadDecl.overloadId(), translator); + } + + public abstract CelZ3FunctionAxiom build(); + } +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/CelZ3OverloadResult.java b/verifier/src/main/java/dev/cel/verifier/axioms/CelZ3OverloadResult.java new file mode 100644 index 000000000..d15541bbc --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/CelZ3OverloadResult.java @@ -0,0 +1,44 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Expr; + +/** + * Result of translating a CEL overload to Z3, carrying both the Z3 expression and its + * approximation. + */ +public final class CelZ3OverloadResult { + private final Expr z3Expr; + private final BoolExpr isApproximate; + + private CelZ3OverloadResult(Expr z3Expr, BoolExpr isApproximate) { + this.z3Expr = z3Expr; + this.isApproximate = isApproximate; + } + + public Expr z3Expr() { + return z3Expr; + } + + public BoolExpr isApproximate() { + return isApproximate; + } + + public static CelZ3OverloadResult create(Expr z3Expr, BoolExpr isApproximate) { + return new CelZ3OverloadResult(z3Expr, isApproximate); + } +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/CelZ3OverloadTranslator.java b/verifier/src/main/java/dev/cel/verifier/axioms/CelZ3OverloadTranslator.java new file mode 100644 index 000000000..c1c7b500d --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/CelZ3OverloadTranslator.java @@ -0,0 +1,56 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.google.errorprone.annotations.Immutable; +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Context; +import com.microsoft.z3.Expr; +import dev.cel.verifier.CelZ3TypeSystem; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +/** Translates a specific CEL overload into a Z3 expression. */ +@FunctionalInterface +@Immutable +public interface CelZ3OverloadTranslator { + + /** + * Translates a specific CEL overload into a Z3 expression along with its approximation flag. + * + *

Warning: The provided {@link Context} and {@link CelZ3TypeSystem} are strictly scoped + * to the current verification run. Do not store references to them as fields or attempt to + * share them across threads, as doing so will lead to undefined behavior or native memory + * violations in Z3. + * + * @param ctx The Z3 Context strictly bound to the current verification run. + * @param typeSystem The CEL-to-Z3 type system for wrapping/unwrapping values. + * @param constraintSink A callback to inject global constraints (axioms) into the Z3 solver. + * Warning: You MUST guard any injected constraints with an {@code ctx.mkImplies} type + * guard (e.g., checking that the arguments are actually the correct types). Failure to do so + * will pollute the solver with invalid constraints for other overloads! + * @param unwrappedArgs The function arguments, already unwrapped by the framework's dynamic + * type-checking ITE chain. + * @param argApproximations The approximation flags for each of the arguments. + * @return Optional.empty() if the overload is not handled by this translator. + */ + Optional translate( + Context ctx, + CelZ3TypeSystem typeSystem, + Consumer constraintSink, + List> unwrappedArgs, + List argApproximations); +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/CelZ3StandardAxioms.java b/verifier/src/main/java/dev/cel/verifier/axioms/CelZ3StandardAxioms.java new file mode 100644 index 000000000..106186eeb --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/CelZ3StandardAxioms.java @@ -0,0 +1,48 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.google.common.collect.ImmutableList; +import dev.cel.common.annotations.Internal; + +/** Registry for CEL standard Z3 axioms. */ +@Internal +public final class CelZ3StandardAxioms { + + public static ImmutableList functionAxioms() { + return ImmutableList.builder() + .add( + AddAxiom.INSTANCE, + SubtractAxiom.INSTANCE, + MultiplyAxiom.INSTANCE, + DivideAxiom.INSTANCE, + ModuloAxiom.INSTANCE, + NegateAxiom.INSTANCE, + MapInsertAxiom.INSTANCE, + TypeAxiom.INSTANCE, + InAxiom.INSTANCE, + LessAxiom.INSTANCE, + LessEqualsAxiom.INSTANCE, + GreaterAxiom.INSTANCE, + GreaterEqualsAxiom.INSTANCE, + SizeAxiom.INSTANCE) + .addAll(OptionalAxioms.ALL_AXIOMS) + .addAll(StringAxioms.ALL_AXIOMS) + .addAll(TypeConversionAxioms.ALL_AXIOMS) + .build(); + } + + private CelZ3StandardAxioms() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/DivideAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/DivideAxiom.java new file mode 100644 index 000000000..0e5cb3004 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/DivideAxiom.java @@ -0,0 +1,61 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the existing language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Expr; +import com.microsoft.z3.FPExpr; +import com.microsoft.z3.IntExpr; +import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import java.util.Optional; + +/** Axiomatization for CEL's division operator (/). */ +final class DivideAxiom { + + static final CelZ3FunctionAxiom INSTANCE = + CelZ3FunctionAxiom.newBuilder(StandardFunction.DIVIDE.functionDecl()) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.DIVIDE_INT64.celOverloadDecl(), + (ctx, ts, sink, l, r) -> { + IntExpr a1 = ts.getInt(l); + IntExpr a2 = ts.getInt(r); + IntExpr div = AxiomHelpers.mkTruncatedDiv(ctx, a1, a2); + Expr result = ts.wrapInt(div); + BoolExpr divByZero = ctx.mkEq(a2, ctx.mkInt(0)); + BoolExpr overflow = ts.checkIntOverflow(div); + return Optional.of(ts.withRuntimeError(result, divByZero, overflow)); + }) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.DIVIDE_UINT64.celOverloadDecl(), + (ctx, ts, sink, l, r) -> { + IntExpr a1 = ts.getUint(l); + IntExpr a2 = ts.getUint(r); + Expr result = ts.wrapUint((IntExpr) ctx.mkDiv(a1, a2)); + BoolExpr divByZero = ctx.mkEq(a2, ctx.mkInt(0)); + return Optional.of(ts.withRuntimeError(result, divByZero)); + }) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.DIVIDE_DOUBLE.celOverloadDecl(), + (ctx, ts, sink, l, r) -> + Optional.of( + ts.wrapDouble( + ctx.mkFPDiv( + ctx.mkFPRoundNearestTiesToEven(), + (FPExpr) ts.getDouble(l), + (FPExpr) ts.getDouble(r))))) + .build(); + + private DivideAxiom() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/GreaterAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/GreaterAxiom.java new file mode 100644 index 000000000..2dce5c8e6 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/GreaterAxiom.java @@ -0,0 +1,137 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.microsoft.z3.ArithExpr; +import com.microsoft.z3.FPExpr; +import com.microsoft.z3.SeqExpr; +import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import dev.cel.checker.CelStandardDeclarations.StandardFunction.Overload.Comparison; +import java.util.Optional; + +/** Axiomatization for CEL's greater operator. */ +@SuppressWarnings({"unchecked", "rawtypes"}) // Z3 Java API uses raw types. +final class GreaterAxiom { + + static final CelZ3FunctionAxiom INSTANCE = + CelZ3FunctionAxiom.newBuilder(StandardFunction.GREATER.functionDecl()) + .addBinaryOverloadTranslator( + Comparison.GREATER_INT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGt( + (ArithExpr) typeSystem.getInt(lhs), + (ArithExpr) typeSystem.getInt(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_TIMESTAMP.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGt( + (ArithExpr) typeSystem.getInt(lhs), + (ArithExpr) typeSystem.getInt(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_DURATION.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGt( + (ArithExpr) typeSystem.getInt(lhs), + (ArithExpr) typeSystem.getInt(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGt( + (ArithExpr) typeSystem.getUint(lhs), + (ArithExpr) typeSystem.getUint(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkFPGt( + (FPExpr) typeSystem.getDouble(lhs), + (FPExpr) typeSystem.getDouble(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_STRING.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.MkStringLt( + (SeqExpr) typeSystem.getString(rhs), + (SeqExpr) typeSystem.getString(lhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_BYTES.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.MkStringLt( + (SeqExpr) typeSystem.getBytes(rhs), + (SeqExpr) typeSystem.getBytes(lhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_INT64_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGt( + ctx.mkInt2Real(typeSystem.getInt(lhs)), + ctx.mkFPToReal((FPExpr) typeSystem.getDouble(rhs)))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_UINT64_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGt( + ctx.mkInt2Real(typeSystem.getUint(lhs)), + ctx.mkFPToReal((FPExpr) typeSystem.getDouble(rhs)))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_DOUBLE_INT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGt( + ctx.mkFPToReal((FPExpr) typeSystem.getDouble(lhs)), + ctx.mkInt2Real(typeSystem.getInt(rhs)))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_DOUBLE_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGt( + ctx.mkFPToReal((FPExpr) typeSystem.getDouble(lhs)), + ctx.mkInt2Real(typeSystem.getUint(rhs)))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_INT64_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGt( + (ArithExpr) typeSystem.getInt(lhs), + (ArithExpr) typeSystem.getUint(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_UINT64_INT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGt( + (ArithExpr) typeSystem.getUint(lhs), + (ArithExpr) typeSystem.getInt(rhs))))) + .build(); + + private GreaterAxiom() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/GreaterEqualsAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/GreaterEqualsAxiom.java new file mode 100644 index 000000000..b3c401aa7 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/GreaterEqualsAxiom.java @@ -0,0 +1,137 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.microsoft.z3.ArithExpr; +import com.microsoft.z3.FPExpr; +import com.microsoft.z3.SeqExpr; +import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import dev.cel.checker.CelStandardDeclarations.StandardFunction.Overload.Comparison; +import java.util.Optional; + +/** Axiomatization for CEL's greaterequals operator. */ +@SuppressWarnings({"unchecked", "rawtypes"}) // Z3 Java API uses raw types. +final class GreaterEqualsAxiom { + + static final CelZ3FunctionAxiom INSTANCE = + CelZ3FunctionAxiom.newBuilder(StandardFunction.GREATER_EQUALS.functionDecl()) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_INT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGe( + (ArithExpr) typeSystem.getInt(lhs), + (ArithExpr) typeSystem.getInt(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_TIMESTAMP.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGe( + (ArithExpr) typeSystem.getInt(lhs), + (ArithExpr) typeSystem.getInt(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_DURATION.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGe( + (ArithExpr) typeSystem.getInt(lhs), + (ArithExpr) typeSystem.getInt(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGe( + (ArithExpr) typeSystem.getUint(lhs), + (ArithExpr) typeSystem.getUint(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkFPGEq( + (FPExpr) typeSystem.getDouble(lhs), + (FPExpr) typeSystem.getDouble(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_STRING.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.MkStringLe( + (SeqExpr) typeSystem.getString(rhs), + (SeqExpr) typeSystem.getString(lhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_BYTES.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.MkStringLe( + (SeqExpr) typeSystem.getBytes(rhs), + (SeqExpr) typeSystem.getBytes(lhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_INT64_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGe( + ctx.mkInt2Real(typeSystem.getInt(lhs)), + ctx.mkFPToReal((FPExpr) typeSystem.getDouble(rhs)))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_UINT64_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGe( + ctx.mkInt2Real(typeSystem.getUint(lhs)), + ctx.mkFPToReal((FPExpr) typeSystem.getDouble(rhs)))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_DOUBLE_INT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGe( + ctx.mkFPToReal((FPExpr) typeSystem.getDouble(lhs)), + ctx.mkInt2Real(typeSystem.getInt(rhs)))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_DOUBLE_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGe( + ctx.mkFPToReal((FPExpr) typeSystem.getDouble(lhs)), + ctx.mkInt2Real(typeSystem.getUint(rhs)))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_INT64_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGe( + (ArithExpr) typeSystem.getInt(lhs), + (ArithExpr) typeSystem.getUint(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_UINT64_INT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGe( + (ArithExpr) typeSystem.getUint(lhs), + (ArithExpr) typeSystem.getInt(rhs))))) + .build(); + + private GreaterEqualsAxiom() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/InAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/InAxiom.java new file mode 100644 index 000000000..809d93725 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/InAxiom.java @@ -0,0 +1,107 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.microsoft.z3.ArrayExpr; +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Expr; +import com.microsoft.z3.FPExpr; +import com.microsoft.z3.IntExpr; +import com.microsoft.z3.SeqExpr; +import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import java.util.Optional; + +/** Axiomatization for CEL's in operator/function (list and map overloads). */ +@SuppressWarnings({"unchecked", "rawtypes"}) // Z3 Java API uses raw types. +public final class InAxiom { + + static final CelZ3FunctionAxiom INSTANCE = + CelZ3FunctionAxiom.newBuilder(StandardFunction.IN.functionDecl()) + .addBinaryOverloadTranslator( + StandardFunction.Overload.InternalOperator.IN_LIST.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhsTrans, rhsTrans) -> { + Expr listRef = typeSystem.getListRef(rhsTrans); + SeqExpr seq = typeSystem.getSeq(listRef); + + BoolExpr structContains = ctx.mkContains((Expr) seq, (Expr) ctx.mkUnit(lhsTrans)); + + BoolExpr isDouble = typeSystem.isDouble(lhsTrans); + FPExpr fpVal = (FPExpr) typeSystem.getDouble(lhsTrans); + + BoolExpr isNaN = ctx.mkFPIsNaN(fpVal); + BoolExpr isDoubleZero = ctx.mkFPIsZero(fpVal); + + Expr posZeroCel = typeSystem.wrapDouble(typeSystem.mkFpDouble(0.0)); + Expr negZeroCel = typeSystem.wrapDouble(typeSystem.mkFpDouble(-0.0)); + Expr intZeroCel = typeSystem.wrapInt(ctx.mkInt(0)); + Expr uintZeroCel = typeSystem.wrapUint(ctx.mkInt(0)); + + // CEL follows IEEE-754 where 0.0 == -0.0, and cross-type equality means they also + // equal int 0 and uint 0. + BoolExpr containsAnyZero = + ctx.mkOr( + ctx.mkContains((Expr) seq, (Expr) ctx.mkUnit(posZeroCel)), + ctx.mkContains((Expr) seq, (Expr) ctx.mkUnit(negZeroCel)), + ctx.mkContains((Expr) seq, (Expr) ctx.mkUnit(intZeroCel)), + ctx.mkContains((Expr) seq, (Expr) ctx.mkUnit(uintZeroCel))); + + // CEL follows IEEE-754 where NaN != NaN. + BoolExpr doubleInList = + (BoolExpr) + ctx.mkITE( + isNaN, + ctx.mkFalse(), + ctx.mkITE(isDoubleZero, containsAnyZero, structContains)); + + BoolExpr isInt = typeSystem.isInt(lhsTrans); + BoolExpr isUint = typeSystem.isUint(lhsTrans); + BoolExpr isIntOrUint = ctx.mkOr(isInt, isUint); + + IntExpr intVal = + (IntExpr) + ctx.mkITE(isInt, typeSystem.getInt(lhsTrans), typeSystem.getUint(lhsTrans)); + BoolExpr isIntOrUintZero = ctx.mkEq(intVal, ctx.mkInt(0)); + + BoolExpr containsIntOrUint = + ctx.mkOr( + ctx.mkContains((Expr) seq, (Expr) ctx.mkUnit(typeSystem.wrapInt(intVal))), + ctx.mkContains((Expr) seq, (Expr) ctx.mkUnit(typeSystem.wrapUint(intVal)))); + + BoolExpr intOrUintInList = + (BoolExpr) ctx.mkITE(isIntOrUintZero, containsAnyZero, containsIntOrUint); + + BoolExpr isMatch = + (BoolExpr) + ctx.mkITE( + isDouble, + doubleInList, + ctx.mkITE(isIntOrUint, intOrUintInList, structContains)); + + return Optional.of(typeSystem.wrapBool(isMatch)); + }) + .addBinaryOverloadTranslator( + StandardFunction.Overload.InternalOperator.IN_MAP.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhsTrans, rhsTrans) -> { + Expr mapRef = typeSystem.getMapRef(rhsTrans); + ArrayExpr mapPresence = (ArrayExpr) typeSystem.getMapPresence(mapRef); + + BoolExpr inMap = (BoolExpr) ctx.mkSelect(mapPresence, lhsTrans); + + return Optional.of(typeSystem.wrapBool(inMap)); + }) + .build(); + + private InAxiom() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/LessAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/LessAxiom.java new file mode 100644 index 000000000..9c34709d0 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/LessAxiom.java @@ -0,0 +1,137 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.microsoft.z3.ArithExpr; +import com.microsoft.z3.FPExpr; +import com.microsoft.z3.SeqExpr; +import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import dev.cel.checker.CelStandardDeclarations.StandardFunction.Overload.Comparison; +import java.util.Optional; + +/** Axiomatization for CEL's less operator. */ +@SuppressWarnings({"unchecked", "rawtypes"}) // Z3 Java API uses raw types. +final class LessAxiom { + + static final CelZ3FunctionAxiom INSTANCE = + CelZ3FunctionAxiom.newBuilder(StandardFunction.LESS.functionDecl()) + .addBinaryOverloadTranslator( + Comparison.LESS_INT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLt( + (ArithExpr) typeSystem.getInt(lhs), + (ArithExpr) typeSystem.getInt(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_TIMESTAMP.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLt( + (ArithExpr) typeSystem.getInt(lhs), + (ArithExpr) typeSystem.getInt(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_DURATION.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLt( + (ArithExpr) typeSystem.getInt(lhs), + (ArithExpr) typeSystem.getInt(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLt( + (ArithExpr) typeSystem.getUint(lhs), + (ArithExpr) typeSystem.getUint(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkFPLt( + (FPExpr) typeSystem.getDouble(lhs), + (FPExpr) typeSystem.getDouble(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_STRING.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.MkStringLt( + (SeqExpr) typeSystem.getString(lhs), + (SeqExpr) typeSystem.getString(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_BYTES.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.MkStringLt( + (SeqExpr) typeSystem.getBytes(lhs), + (SeqExpr) typeSystem.getBytes(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_INT64_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLt( + ctx.mkInt2Real(typeSystem.getInt(lhs)), + ctx.mkFPToReal((FPExpr) typeSystem.getDouble(rhs)))))) + .addBinaryOverloadTranslator( + Comparison.LESS_UINT64_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLt( + ctx.mkInt2Real(typeSystem.getUint(lhs)), + ctx.mkFPToReal((FPExpr) typeSystem.getDouble(rhs)))))) + .addBinaryOverloadTranslator( + Comparison.LESS_DOUBLE_INT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLt( + ctx.mkFPToReal((FPExpr) typeSystem.getDouble(lhs)), + ctx.mkInt2Real(typeSystem.getInt(rhs)))))) + .addBinaryOverloadTranslator( + Comparison.LESS_DOUBLE_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLt( + ctx.mkFPToReal((FPExpr) typeSystem.getDouble(lhs)), + ctx.mkInt2Real(typeSystem.getUint(rhs)))))) + .addBinaryOverloadTranslator( + Comparison.LESS_INT64_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLt( + (ArithExpr) typeSystem.getInt(lhs), + (ArithExpr) typeSystem.getUint(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_UINT64_INT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLt( + (ArithExpr) typeSystem.getUint(lhs), + (ArithExpr) typeSystem.getInt(rhs))))) + .build(); + + private LessAxiom() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/LessEqualsAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/LessEqualsAxiom.java new file mode 100644 index 000000000..750961515 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/LessEqualsAxiom.java @@ -0,0 +1,137 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.microsoft.z3.ArithExpr; +import com.microsoft.z3.FPExpr; +import com.microsoft.z3.SeqExpr; +import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import dev.cel.checker.CelStandardDeclarations.StandardFunction.Overload.Comparison; +import java.util.Optional; + +/** Axiomatization for CEL's less_equals operator. */ +@SuppressWarnings({"unchecked", "rawtypes"}) // Z3 Java API uses raw types. +final class LessEqualsAxiom { + + static final CelZ3FunctionAxiom INSTANCE = + CelZ3FunctionAxiom.newBuilder(StandardFunction.LESS_EQUALS.functionDecl()) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_INT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLe( + (ArithExpr) typeSystem.getInt(lhs), + (ArithExpr) typeSystem.getInt(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_TIMESTAMP.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLe( + (ArithExpr) typeSystem.getInt(lhs), + (ArithExpr) typeSystem.getInt(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_DURATION.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLe( + (ArithExpr) typeSystem.getInt(lhs), + (ArithExpr) typeSystem.getInt(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLe( + (ArithExpr) typeSystem.getUint(lhs), + (ArithExpr) typeSystem.getUint(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkFPLEq( + (FPExpr) typeSystem.getDouble(lhs), + (FPExpr) typeSystem.getDouble(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_STRING.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.MkStringLe( + (SeqExpr) typeSystem.getString(lhs), + (SeqExpr) typeSystem.getString(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_BYTES.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.MkStringLe( + (SeqExpr) typeSystem.getBytes(lhs), + (SeqExpr) typeSystem.getBytes(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_INT64_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLe( + ctx.mkInt2Real(typeSystem.getInt(lhs)), + ctx.mkFPToReal((FPExpr) typeSystem.getDouble(rhs)))))) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_UINT64_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLe( + ctx.mkInt2Real(typeSystem.getUint(lhs)), + ctx.mkFPToReal((FPExpr) typeSystem.getDouble(rhs)))))) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_DOUBLE_INT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLe( + ctx.mkFPToReal((FPExpr) typeSystem.getDouble(lhs)), + ctx.mkInt2Real(typeSystem.getInt(rhs)))))) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_DOUBLE_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLe( + ctx.mkFPToReal((FPExpr) typeSystem.getDouble(lhs)), + ctx.mkInt2Real(typeSystem.getUint(rhs)))))) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_INT64_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLe( + (ArithExpr) typeSystem.getInt(lhs), + (ArithExpr) typeSystem.getUint(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_UINT64_INT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLe( + (ArithExpr) typeSystem.getUint(lhs), + (ArithExpr) typeSystem.getInt(rhs))))) + .build(); + + private LessEqualsAxiom() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/MapInsertAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/MapInsertAxiom.java new file mode 100644 index 000000000..5d061df08 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/MapInsertAxiom.java @@ -0,0 +1,82 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + + +import com.microsoft.z3.ArrayExpr; +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Expr; +import com.microsoft.z3.SeqExpr; +import dev.cel.extensions.CelComprehensionsExtensions; +import java.util.Optional; + +/** Axiomatization for CEL's cel.@mapInsert function. */ +@SuppressWarnings({"unchecked", "rawtypes"}) // Z3 Java API uses raw types. +final class MapInsertAxiom { + + static final CelZ3FunctionAxiom INSTANCE = + CelZ3FunctionAxiom.newBuilder(CelComprehensionsExtensions.Function.MAP_INSERT.functionDecl()) + .addTernaryOverloadTranslator( + "cel_@mapInsert_map_key_value", + (ctx, typeSystem, constraintSink, mapVal, keyVal, valueVal) -> { + BoolExpr isMap = typeSystem.isMap(mapVal); + BoolExpr isValidKey = + ctx.mkOr( + typeSystem.isString(keyVal), + typeSystem.isInt(keyVal), + typeSystem.isUint(keyVal), + typeSystem.isBool(keyVal)); + BoolExpr isMapAndValidKey = ctx.mkAnd(isMap, isValidKey); + + // CEL maps are immutable; we represent insertion by creating a new MapRef + // constrained to the + // updated data. + Expr newMapRef = typeSystem.mkMapRefConst("mapInsertRef"); + + Expr oldMapRef = typeSystem.getMapRef(mapVal); + ArrayExpr oldValues = (ArrayExpr) typeSystem.getMapValues(oldMapRef); + ArrayExpr oldPresence = (ArrayExpr) typeSystem.getMapPresence(oldMapRef); + + ArrayExpr newValues = ctx.mkStore(oldValues, keyVal, valueVal); + ArrayExpr newPresence = ctx.mkStore(oldPresence, keyVal, ctx.mkTrue()); + + BoolExpr keyAlreadyPresent = (BoolExpr) ctx.mkSelect(oldPresence, keyVal); + SeqExpr oldKeys = typeSystem.getMapKeys(oldMapRef); + Expr newKeys = + ctx.mkITE( + keyAlreadyPresent, + oldKeys, + typeSystem.mkConcatSafe(oldKeys, ctx.mkUnit(keyVal))); + + constraintSink.accept( + ctx.mkImplies( + isMapAndValidKey, ctx.mkEq(typeSystem.getMapValues(newMapRef), newValues))); + constraintSink.accept( + ctx.mkImplies( + isMapAndValidKey, + ctx.mkEq(typeSystem.getMapPresence(newMapRef), newPresence))); + constraintSink.accept( + ctx.mkImplies( + isMapAndValidKey, ctx.mkEq(typeSystem.getMapKeys(newMapRef), newKeys))); + + Expr result = + ctx.mkITE( + isMapAndValidKey, typeSystem.wrapMap(newMapRef), typeSystem.mkError()); + return Optional.of(result); + }) + .build(); + + private MapInsertAxiom() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/ModuloAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/ModuloAxiom.java new file mode 100644 index 000000000..e6567d0e2 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/ModuloAxiom.java @@ -0,0 +1,50 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the existing language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Expr; +import com.microsoft.z3.IntExpr; +import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import java.util.Optional; + +/** Axiomatization for CEL's modulo operator (%). */ +final class ModuloAxiom { + + static final CelZ3FunctionAxiom INSTANCE = + CelZ3FunctionAxiom.newBuilder(StandardFunction.MODULO.functionDecl()) + // Int Mod + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.MODULO_INT64.celOverloadDecl(), + (ctx, ts, sink, l, r) -> { + IntExpr a1 = ts.getInt(l); + IntExpr a2 = ts.getInt(r); + Expr result = ts.wrapInt(AxiomHelpers.mkTruncatedMod(ctx, a1, a2)); + BoolExpr divByZero = ctx.mkEq(a2, ctx.mkInt(0)); + return Optional.of(ts.withRuntimeError(result, divByZero)); + }) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.MODULO_UINT64.celOverloadDecl(), + (ctx, ts, sink, l, r) -> { + IntExpr a1 = ts.getUint(l); + IntExpr a2 = ts.getUint(r); + Expr result = ts.wrapUint(ctx.mkMod(a1, a2)); + BoolExpr divByZero = ctx.mkEq(a2, ctx.mkInt(0)); + return Optional.of(ts.withRuntimeError(result, divByZero)); + }) + .build(); + + private ModuloAxiom() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/MultiplyAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/MultiplyAxiom.java new file mode 100644 index 000000000..eebd415ab --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/MultiplyAxiom.java @@ -0,0 +1,62 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the existing language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Expr; +import com.microsoft.z3.FPExpr; +import com.microsoft.z3.IntExpr; +import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import java.util.Optional; + +/** Axiomatization for CEL's multiplication operator (*). */ +final class MultiplyAxiom { + + static final CelZ3FunctionAxiom INSTANCE = + CelZ3FunctionAxiom.newBuilder(StandardFunction.MULTIPLY.functionDecl()) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.MULTIPLY_INT64.celOverloadDecl(), + (ctx, ts, sink, l, r) -> { + IntExpr a1 = ts.getInt(l); + IntExpr a2 = ts.getInt(r); + Expr result = ts.wrapInt((IntExpr) ctx.mkMul(a1, a2)); + BoolExpr overflow = ts.checkIntOverflow(ctx.mkMul(a1, a2)); + return Optional.of(ts.withRuntimeError(result, overflow)); + }) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.MULTIPLY_UINT64.celOverloadDecl(), + (ctx, ts, sink, l, r) -> { + IntExpr a1 = ts.getUint(l); + IntExpr a2 = ts.getUint(r); + Expr result = ts.wrapUint((IntExpr) ctx.mkMul(a1, a2)); + BoolExpr overflow = ts.checkUintOverflow(ctx.mkMul(a1, a2)); + return Optional.of(ts.withRuntimeError(result, overflow)); + }) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.MULTIPLY_DOUBLE.celOverloadDecl(), + (ctx, ts, sink, l, r) -> + Optional.of( + ts.wrapDouble( + ctx.mkFPMul( + ctx.mkFPRoundNearestTiesToEven(), + (FPExpr) ts.getDouble(l), + (FPExpr) ts.getDouble(r))))) + .build(); + + private MultiplyAxiom() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/NegateAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/NegateAxiom.java new file mode 100644 index 000000000..b5e608211 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/NegateAxiom.java @@ -0,0 +1,46 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the existing language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Expr; +import com.microsoft.z3.FPExpr; +import com.microsoft.z3.IntExpr; +import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import java.util.Optional; + +/** Axiomatization for CEL's unary negation operator (-). */ +final class NegateAxiom { + + static final CelZ3FunctionAxiom INSTANCE = + CelZ3FunctionAxiom.newBuilder(StandardFunction.NEGATE.functionDecl()) + // Int Negate + .addUnaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.NEGATE_INT64.celOverloadDecl(), + (ctx, ts, sink, arg) -> { + IntExpr a = ts.getInt(arg); + Expr result = ts.wrapInt((IntExpr) ctx.mkUnaryMinus(a)); + BoolExpr overflow = ts.checkIntOverflow(ctx.mkUnaryMinus(a)); + return Optional.of(ts.withRuntimeError(result, overflow)); + }) + // Double Negate + .addUnaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.NEGATE_DOUBLE.celOverloadDecl(), + (ctx, ts, sink, arg) -> + Optional.of(ts.wrapDouble(ctx.mkFPNeg((FPExpr) ts.getDouble(arg))))) + .build(); + + private NegateAxiom() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/OptionalAxioms.java b/verifier/src/main/java/dev/cel/verifier/axioms/OptionalAxioms.java new file mode 100644 index 000000000..9a76bec36 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/OptionalAxioms.java @@ -0,0 +1,101 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.google.common.collect.ImmutableList; +import com.microsoft.z3.Expr; +import dev.cel.common.CelFunctionDecl; +import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.extensions.CelOptionalLibrary.Function; +import java.util.Optional; + +/** Axiomatization for CEL's optional library functions. */ +final class OptionalAxioms { + + static final ImmutableList ALL_AXIOMS = + ImmutableList.of( + createAxiom( + Function.OPTIONAL_NONE, + "optional_none", + (ctx, ts, sink, args, argApproximations) -> + Optional.of(CelZ3OverloadResult.create(ts.mkOptionalNone(), ctx.mkFalse()))), + createUnaryAxiom( + Function.OPTIONAL_OF, + "optional_of", + (ctx, ts, sink, value) -> { + Expr optRef = ctx.mkApp(ts.optionalOfRefFunc(), value); + sink.accept(ctx.mkEq(ts.getOptionalValue(optRef), value)); + sink.accept(ts.optHasValue(optRef)); + return Optional.of(ts.mkOptionalOf(optRef)); + }), + createUnaryAxiom( + Function.HAS_VALUE, + "optional_hasValue", + (ctx, ts, sink, val) -> + Optional.of(ts.wrapBool(ts.optHasValue(ts.getOptionalRef(val))))), + createUnaryAxiom( + Function.VALUE, + "optional_value", + (ctx, ts, sink, val) -> { + Expr optRef = ts.getOptionalRef(val); + return Optional.of( + ctx.mkITE(ts.optHasValue(optRef), ts.getOptionalValue(optRef), ts.mkError())); + }), + createBinaryAxiom( + Function.OR_VALUE, + "optional_orValue_value", + (ctx, ts, sink, val, other) -> { + Expr optRef = ts.getOptionalRef(val); + return Optional.of( + ctx.mkITE(ts.optHasValue(optRef), ts.getOptionalValue(optRef), other)); + }), + createBinaryAxiom( + Function.OR, + "optional_or_optional", + (ctx, ts, sink, val, other) -> { + Expr optRef = ts.getOptionalRef(val); + return Optional.of(ctx.mkITE(ts.optHasValue(optRef), val, other)); + })); + + private static CelFunctionDecl getDecl(Function funcEnum) { + return CelOptionalLibrary.INSTANCE.functions().stream() + .filter(d -> d.name().equals(funcEnum.getFunction())) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unknown function: " + funcEnum)); + } + + private static CelZ3FunctionAxiom createAxiom( + Function funcEnum, String overloadId, CelZ3OverloadTranslator translator) { + return CelZ3FunctionAxiom.newBuilder(getDecl(funcEnum)) + .addOverloadTranslator(overloadId, translator) + .build(); + } + + private static CelZ3FunctionAxiom createUnaryAxiom( + Function funcEnum, String overloadId, CelZ3FunctionAxiom.UnaryTranslator translator) { + return CelZ3FunctionAxiom.newBuilder(getDecl(funcEnum)) + .addUnaryOverloadTranslator(overloadId, translator) + .build(); + } + + private static CelZ3FunctionAxiom createBinaryAxiom( + Function funcEnum, String overloadId, CelZ3FunctionAxiom.BinaryTranslator translator) { + return CelZ3FunctionAxiom.newBuilder(getDecl(funcEnum)) + .addBinaryOverloadTranslator(overloadId, translator) + .build(); + } + + private OptionalAxioms() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/SizeAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/SizeAxiom.java new file mode 100644 index 000000000..6ad4cd855 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/SizeAxiom.java @@ -0,0 +1,74 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the applicable language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Context; +import com.microsoft.z3.Expr; +import com.microsoft.z3.IntExpr; +import com.microsoft.z3.SeqExpr; +import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import dev.cel.checker.CelStandardDeclarations.StandardFunction.Overload.Size; +import dev.cel.verifier.CelZ3TypeSystem; +import java.util.Optional; +import java.util.function.Consumer; + +/** Axiomatization for CEL's size operator/function. */ +@SuppressWarnings({"unchecked", "rawtypes"}) // Z3 Java API uses raw types. +final class SizeAxiom { + + private static final int MAX_CONTAINER_SIZE = Integer.MAX_VALUE; + + static final CelZ3FunctionAxiom INSTANCE = + CelZ3FunctionAxiom.newBuilder(StandardFunction.SIZE.functionDecl()) + .addUnaryOverloadTranslator( + Size.SIZE_STRING.celOverloadDecl(), + (ctx, ts, sink, val) -> buildBoundedLength(ts.getString(val), ctx, ts, sink)) + .addUnaryOverloadTranslator( + Size.STRING_SIZE.celOverloadDecl(), + (ctx, ts, sink, val) -> buildBoundedLength(ts.getString(val), ctx, ts, sink)) + .addUnaryOverloadTranslator( + Size.SIZE_BYTES.celOverloadDecl(), + (ctx, ts, sink, val) -> buildBoundedLength(ts.getBytes(val), ctx, ts, sink)) + .addUnaryOverloadTranslator( + Size.BYTES_SIZE.celOverloadDecl(), + (ctx, ts, sink, val) -> buildBoundedLength(ts.getBytes(val), ctx, ts, sink)) + .addUnaryOverloadTranslator( + Size.SIZE_LIST.celOverloadDecl(), + (ctx, ts, sink, val) -> + buildBoundedLength(ts.getSeq(ts.getListRef(val)), ctx, ts, sink)) + .addUnaryOverloadTranslator( + Size.LIST_SIZE.celOverloadDecl(), + (ctx, ts, sink, val) -> + buildBoundedLength(ts.getSeq(ts.getListRef(val)), ctx, ts, sink)) + .addUnaryOverloadTranslator( + Size.SIZE_MAP.celOverloadDecl(), + (ctx, ts, sink, val) -> + buildBoundedLength(ts.getMapKeys(ts.getMapRef(val)), ctx, ts, sink)) + .addUnaryOverloadTranslator( + Size.MAP_SIZE.celOverloadDecl(), + (ctx, ts, sink, val) -> + buildBoundedLength(ts.getMapKeys(ts.getMapRef(val)), ctx, ts, sink)) + .build(); + + private static Optional> buildBoundedLength( + Expr seq, Context ctx, CelZ3TypeSystem typeSystem, Consumer constraintSink) { + IntExpr length = ctx.mkLength((SeqExpr) seq); + constraintSink.accept(ctx.mkLe(length, ctx.mkInt(MAX_CONTAINER_SIZE))); + return Optional.of(typeSystem.wrapInt(length)); + } + + private SizeAxiom() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/StringAxioms.java b/verifier/src/main/java/dev/cel/verifier/axioms/StringAxioms.java new file mode 100644 index 000000000..aef57c1a4 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/StringAxioms.java @@ -0,0 +1,66 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the applicable language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.google.common.collect.ImmutableList; +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Expr; +import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import dev.cel.checker.CelStandardDeclarations.StandardFunction.Overload.StringMatchers; +import dev.cel.common.CelOverloadDecl; +import java.util.Optional; + +/** Axiomatization for CEL's string functions. */ +@SuppressWarnings({"unchecked", "rawtypes"}) // Z3 Java API uses raw types. +final class StringAxioms { + + static final ImmutableList ALL_AXIOMS = + ImmutableList.of( + createBinaryAxiom( + StandardFunction.CONTAINS, + StringMatchers.CONTAINS_STRING.celOverloadDecl(), + (ctx, ts, sink, str, substr) -> { + BoolExpr res = + ctx.mkContains((Expr) ts.getString(str), (Expr) ts.getString(substr)); + return Optional.of(ts.wrapBool(res)); + }), + createBinaryAxiom( + StandardFunction.STARTS_WITH, + StringMatchers.STARTS_WITH_STRING.celOverloadDecl(), + (ctx, ts, sink, str, prefix) -> { + BoolExpr res = + ctx.mkPrefixOf((Expr) ts.getString(prefix), (Expr) ts.getString(str)); + return Optional.of(ts.wrapBool(res)); + }), + createBinaryAxiom( + StandardFunction.ENDS_WITH, + StringMatchers.ENDS_WITH_STRING.celOverloadDecl(), + (ctx, ts, sink, str, suffix) -> { + BoolExpr res = + ctx.mkSuffixOf((Expr) ts.getString(suffix), (Expr) ts.getString(str)); + return Optional.of(ts.wrapBool(res)); + })); + + private static CelZ3FunctionAxiom createBinaryAxiom( + StandardFunction stdFunc, + CelOverloadDecl overloadDecl, + CelZ3FunctionAxiom.BinaryTranslator translator) { + return CelZ3FunctionAxiom.newBuilder(stdFunc.functionDecl()) + .addBinaryOverloadTranslator(overloadDecl, translator) + .build(); + } + + private StringAxioms() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/SubtractAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/SubtractAxiom.java new file mode 100644 index 000000000..bd0ecf882 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/SubtractAxiom.java @@ -0,0 +1,59 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Expr; +import com.microsoft.z3.FPExpr; +import com.microsoft.z3.IntExpr; +import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import java.util.Optional; + +/** Axiomatization for CEL's subtraction operator (-). */ +final class SubtractAxiom { + + static final CelZ3FunctionAxiom INSTANCE = + CelZ3FunctionAxiom.newBuilder(StandardFunction.SUBTRACT.functionDecl()) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.SUBTRACT_INT64.celOverloadDecl(), + (ctx, ts, sink, l, r) -> { + IntExpr a1 = ts.getInt(l); + IntExpr a2 = ts.getInt(r); + Expr result = ts.wrapInt((IntExpr) ctx.mkSub(a1, a2)); + BoolExpr overflow = ts.checkIntOverflow(ctx.mkSub(a1, a2)); + return Optional.of(ts.withRuntimeError(result, overflow)); + }) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.SUBTRACT_UINT64.celOverloadDecl(), + (ctx, ts, sink, l, r) -> { + IntExpr a1 = ts.getUint(l); + IntExpr a2 = ts.getUint(r); + Expr result = ts.wrapUint((IntExpr) ctx.mkSub(a1, a2)); + BoolExpr overflow = ts.checkUintOverflow(ctx.mkSub(a1, a2)); + return Optional.of(ts.withRuntimeError(result, overflow)); + }) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.SUBTRACT_DOUBLE.celOverloadDecl(), + (ctx, ts, sink, l, r) -> + Optional.of( + ts.wrapDouble( + ctx.mkFPSub( + ctx.mkFPRoundNearestTiesToEven(), + (FPExpr) ts.getDouble(l), + (FPExpr) ts.getDouble(r))))) + .build(); + + private SubtractAxiom() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/TypeAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/TypeAxiom.java new file mode 100644 index 000000000..5b7fdba3a --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/TypeAxiom.java @@ -0,0 +1,87 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import com.google.common.base.Preconditions; +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Context; +import com.microsoft.z3.Expr; +import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import dev.cel.common.types.ListType; +import dev.cel.common.types.MapType; +import dev.cel.common.types.OptionalType; +import dev.cel.common.types.SimpleType; +import dev.cel.verifier.CelZ3TypeSystem; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +/** Axiomatization for CEL's type() function. */ +final class TypeAxiom { + + private static final String TYPE_NAME_LIST = ListType.create().name(); + private static final String TYPE_NAME_MAP = MapType.create(SimpleType.DYN, SimpleType.DYN).name(); + + static final CelZ3FunctionAxiom INSTANCE = + CelZ3FunctionAxiom.newBuilder(StandardFunction.TYPE.functionDecl()) + .addOverloadTranslator( + StandardFunction.Overload.InternalOperator.TYPE.celOverloadDecl(), + new CelZ3OverloadTranslator() { + @Override + public Optional translate( + Context ctx, + CelZ3TypeSystem typeSystem, + Consumer constraintSink, + List> unwrappedArgs, + List argApproximations) { + Preconditions.checkArgument(unwrappedArgs.size() == 1); + Preconditions.checkArgument(argApproximations.size() == 1); + + Expr val = unwrappedArgs.get(0); + BoolExpr argApprox = argApproximations.get(0); + + Expr result = getTypeExpression(ctx, typeSystem, val); + + // Custom approximation logic for type(): it is only approximate if the argument + // is approximate AND the argument is an Error or Unknown. + BoolExpr isErrOrUnk = + ctx.mkOr(typeSystem.isError(val), typeSystem.isUnknown(val)); + BoolExpr typeApprox = ctx.mkAnd(argApprox, isErrOrUnk); + + return Optional.of(CelZ3OverloadResult.create(result, typeApprox)); + } + }) + .build(); + + private static Expr getTypeExpression(Context ctx, CelZ3TypeSystem typeSystem, Expr val) { + return CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase( + typeSystem.isStruct(val), + typeSystem.wrapString(typeSystem.getMsgTypeName(typeSystem.getMessageRef(val)))) + .addCase(typeSystem.isOptional(val), typeSystem.mkString(OptionalType.NAME)) + .addCase(typeSystem.isNull(val), typeSystem.mkString(SimpleType.NULL_TYPE.name())) + .addCase(typeSystem.isMap(val), typeSystem.mkString(TYPE_NAME_MAP)) + .addCase(typeSystem.isList(val), typeSystem.mkString(TYPE_NAME_LIST)) + .addCase(typeSystem.isBytes(val), typeSystem.mkString(SimpleType.BYTES.name())) + .addCase(typeSystem.isString(val), typeSystem.mkString(SimpleType.STRING.name())) + .addCase(typeSystem.isDouble(val), typeSystem.mkString(SimpleType.DOUBLE.name())) + .addCase(typeSystem.isUint(val), typeSystem.mkString(SimpleType.UINT.name())) + .addCase(typeSystem.isInt(val), typeSystem.mkString(SimpleType.INT.name())) + .addCase(typeSystem.isBool(val), typeSystem.mkString(SimpleType.BOOL.name())) + .build(typeSystem.mkError()); + } + + private TypeAxiom() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java b/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java new file mode 100644 index 000000000..741513daf --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java @@ -0,0 +1,246 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.axioms; + +import static dev.cel.verifier.CelZ3TypeSystem.MAX_INT64; + +import com.google.common.collect.ImmutableList; +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Expr; +import com.microsoft.z3.FPExpr; +import com.microsoft.z3.FuncDecl; +import com.microsoft.z3.IntExpr; +import com.microsoft.z3.Sort; +import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import dev.cel.checker.CelStandardDeclarations.StandardFunction.Overload.Conversions; +import java.util.Optional; + +/** Axiomatization for CEL's type conversion functions. */ +final class TypeConversionAxioms { + + private static final CelZ3FunctionAxiom INT_AXIOM = + CelZ3FunctionAxiom.newBuilder(StandardFunction.INT.functionDecl()) + .addUnaryOverloadTranslator( + Conversions.INT64_TO_INT64.celOverloadDecl(), + (ctx, typeSystem, sink, arg) -> Optional.of(arg)) + .addUnaryOverloadTranslator( + Conversions.UINT64_TO_INT64.celOverloadDecl(), + (ctx, typeSystem, sink, arg) -> { + IntExpr uintVal = typeSystem.getUint(arg); + BoolExpr outOfBounds = ctx.mkGt(uintVal, ctx.mkInt(MAX_INT64)); + return Optional.of( + typeSystem.withRuntimeError(typeSystem.wrapInt(uintVal), outOfBounds)); + }) + .addUnaryOverloadTranslator( + Conversions.DOUBLE_TO_INT64.celOverloadDecl(), + createUninterpretedConversion(Conversions.DOUBLE_TO_INT64), + true) + .addUnaryOverloadTranslator( + Conversions.STRING_TO_INT64.celOverloadDecl(), + createUninterpretedConversion(Conversions.STRING_TO_INT64), + true) + .addUnaryOverloadTranslator( + Conversions.TIMESTAMP_TO_INT64.celOverloadDecl(), + createUninterpretedConversion(Conversions.TIMESTAMP_TO_INT64), + true) + .build(); + + private static final CelZ3FunctionAxiom UINT_AXIOM = + CelZ3FunctionAxiom.newBuilder(StandardFunction.UINT.functionDecl()) + .addUnaryOverloadTranslator( + Conversions.UINT64_TO_UINT64.celOverloadDecl(), + (ctx, typeSystem, sink, arg) -> Optional.of(arg)) + .addUnaryOverloadTranslator( + Conversions.INT64_TO_UINT64.celOverloadDecl(), + (ctx, typeSystem, sink, arg) -> { + IntExpr intVal = typeSystem.getInt(arg); + BoolExpr outOfBounds = ctx.mkLt(intVal, ctx.mkInt(0)); + return Optional.of( + typeSystem.withRuntimeError(typeSystem.wrapUint(intVal), outOfBounds)); + }) + .addUnaryOverloadTranslator( + Conversions.DOUBLE_TO_UINT64.celOverloadDecl(), + createUninterpretedConversion(Conversions.DOUBLE_TO_UINT64), + true) + .addUnaryOverloadTranslator( + Conversions.STRING_TO_UINT64.celOverloadDecl(), + createUninterpretedConversion(Conversions.STRING_TO_UINT64), + true) + .build(); + + private static final CelZ3FunctionAxiom DOUBLE_AXIOM = + CelZ3FunctionAxiom.newBuilder(StandardFunction.DOUBLE.functionDecl()) + .addUnaryOverloadTranslator( + Conversions.DOUBLE_TO_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, sink, arg) -> Optional.of(arg)) + .addUnaryOverloadTranslator( + Conversions.INT64_TO_DOUBLE.celOverloadDecl(), + createUninterpretedConversion(Conversions.INT64_TO_DOUBLE), + true) + .addUnaryOverloadTranslator( + Conversions.UINT64_TO_DOUBLE.celOverloadDecl(), + createUninterpretedConversion(Conversions.UINT64_TO_DOUBLE), + true) + .addUnaryOverloadTranslator( + Conversions.STRING_TO_DOUBLE.celOverloadDecl(), + createUninterpretedConversion(Conversions.STRING_TO_DOUBLE), + true) + .build(); + + private static final CelZ3FunctionAxiom STRING_AXIOM = + CelZ3FunctionAxiom.newBuilder(StandardFunction.STRING.functionDecl()) + .addUnaryOverloadTranslator( + Conversions.STRING_TO_STRING.celOverloadDecl(), + (ctx, typeSystem, sink, arg) -> Optional.of(arg)) + .addUnaryOverloadTranslator( + Conversions.INT64_TO_STRING.celOverloadDecl(), + createUninterpretedConversion(Conversions.INT64_TO_STRING), + true) + .addUnaryOverloadTranslator( + Conversions.UINT64_TO_STRING.celOverloadDecl(), + createUninterpretedConversion(Conversions.UINT64_TO_STRING), + true) + .addUnaryOverloadTranslator( + Conversions.DOUBLE_TO_STRING.celOverloadDecl(), + createUninterpretedConversion(Conversions.DOUBLE_TO_STRING), + true) + .addUnaryOverloadTranslator( + Conversions.BOOL_TO_STRING.celOverloadDecl(), + createUninterpretedConversion(Conversions.BOOL_TO_STRING), + true) + .addUnaryOverloadTranslator( + Conversions.BYTES_TO_STRING.celOverloadDecl(), + createUninterpretedConversion(Conversions.BYTES_TO_STRING), + true) + .addUnaryOverloadTranslator( + Conversions.TIMESTAMP_TO_STRING.celOverloadDecl(), + createUninterpretedConversion(Conversions.TIMESTAMP_TO_STRING), + true) + .addUnaryOverloadTranslator( + Conversions.DURATION_TO_STRING.celOverloadDecl(), + createUninterpretedConversion(Conversions.DURATION_TO_STRING), + true) + .build(); + + private static final CelZ3FunctionAxiom BYTES_AXIOM = + CelZ3FunctionAxiom.newBuilder(StandardFunction.BYTES.functionDecl()) + .addUnaryOverloadTranslator( + Conversions.BYTES_TO_BYTES.celOverloadDecl(), + (ctx, typeSystem, sink, arg) -> Optional.of(arg)) + .addUnaryOverloadTranslator( + Conversions.STRING_TO_BYTES.celOverloadDecl(), + createUninterpretedConversion(Conversions.STRING_TO_BYTES), + true) + .build(); + + private static final CelZ3FunctionAxiom DYN_AXIOM = + CelZ3FunctionAxiom.newBuilder(StandardFunction.DYN.functionDecl()) + .addUnaryOverloadTranslator( + Conversions.TO_DYN.celOverloadDecl(), + (ctx, typeSystem, sink, arg) -> Optional.of(arg)) + .build(); + + private static final CelZ3FunctionAxiom DURATION_AXIOM = + CelZ3FunctionAxiom.newBuilder(StandardFunction.DURATION.functionDecl()) + .addUnaryOverloadTranslator( + Conversions.DURATION_TO_DURATION.celOverloadDecl(), + (ctx, typeSystem, sink, arg) -> Optional.of(arg)) + .addUnaryOverloadTranslator( + Conversions.STRING_TO_DURATION.celOverloadDecl(), + createUninterpretedConversion(Conversions.STRING_TO_DURATION), + true) + .build(); + + private static final CelZ3FunctionAxiom TIMESTAMP_AXIOM = + CelZ3FunctionAxiom.newBuilder(StandardFunction.TIMESTAMP.functionDecl()) + .addUnaryOverloadTranslator( + Conversions.TIMESTAMP_TO_TIMESTAMP.celOverloadDecl(), + (ctx, typeSystem, sink, arg) -> Optional.of(arg)) + .addUnaryOverloadTranslator( + Conversions.STRING_TO_TIMESTAMP.celOverloadDecl(), + createUninterpretedConversion(Conversions.STRING_TO_TIMESTAMP), + true) + .addUnaryOverloadTranslator( + Conversions.INT64_TO_TIMESTAMP.celOverloadDecl(), + createUninterpretedConversion(Conversions.INT64_TO_TIMESTAMP), + true) + .build(); + + private static final CelZ3FunctionAxiom BOOL_AXIOM = + CelZ3FunctionAxiom.newBuilder(StandardFunction.BOOL.functionDecl()) + .addUnaryOverloadTranslator( + Conversions.BOOL_TO_BOOL.celOverloadDecl(), + (ctx, typeSystem, sink, arg) -> Optional.of(arg)) + .addUnaryOverloadTranslator( + Conversions.STRING_TO_BOOL.celOverloadDecl(), + createUninterpretedConversion(Conversions.STRING_TO_BOOL), + true) + .build(); + + static final ImmutableList ALL_AXIOMS = + ImmutableList.of( + INT_AXIOM, + UINT_AXIOM, + DOUBLE_AXIOM, + STRING_AXIOM, + BYTES_AXIOM, + DYN_AXIOM, + DURATION_AXIOM, + TIMESTAMP_AXIOM, + BOOL_AXIOM); + + private static CelZ3FunctionAxiom.UnaryTranslator createUninterpretedConversion( + Conversions conversion) { + return (ctx, typeSystem, sink, arg) -> { + FuncDecl funcDecl = + typeSystem.internFuncDecl( + conversion.celOverloadDecl().overloadId(), + new Sort[] {typeSystem.celValueSort()}, + typeSystem.celValueSort()); + Expr res = ctx.mkApp(funcDecl, arg); + + switch (conversion.celOverloadDecl().resultType().kind()) { + case INT: + case TIMESTAMP: + case DURATION: + sink.accept(typeSystem.isInt(res)); + sink.accept(ctx.mkNot(typeSystem.checkIntOverflow(typeSystem.getInt(res)))); + break; + case UINT: + sink.accept(typeSystem.isUint(res)); + sink.accept(ctx.mkNot(typeSystem.checkUintOverflow(typeSystem.getUint(res)))); + break; + case DOUBLE: + sink.accept(typeSystem.isDouble(res)); + sink.accept(ctx.mkNot(ctx.mkFPIsNaN((FPExpr) typeSystem.getDouble(res)))); + break; + case STRING: + sink.accept(typeSystem.isString(res)); + break; + case BYTES: + sink.accept(typeSystem.isBytes(res)); + break; + case BOOL: + sink.accept(typeSystem.isBool(res)); + break; + default: + break; + } + return Optional.of(res); + }; + } + + private TypeConversionAxioms() {} +} diff --git a/verifier/src/test/java/dev/cel/verifier/BUILD.bazel b/verifier/src/test/java/dev/cel/verifier/BUILD.bazel new file mode 100644 index 000000000..03676eca6 --- /dev/null +++ b/verifier/src/test/java/dev/cel/verifier/BUILD.bazel @@ -0,0 +1,65 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:testing.bzl", "junit4_test_suites") + +package( + default_applicable_licenses = ["//:license"], +) + +java_library( + name = "tests", + testonly = True, + srcs = glob( + ["**/*.java"], + ), + compatible_with = [], + deps = [ + "//bundle:cel", + "//common:cel_ast", + "//common:compiler_common", + "//common:container", + "//common:operator", + "//common:options", + "//common/ast", + "//common/types", + "//common/types:message_type_provider", + "//compiler:compiler_builder", + "//extensions", + "//extensions:optional_library", + # "//java/com/google/testing/testsize:annotations", + "//optimizer", + "//optimizer:optimizer_builder", + "//optimizer/optimizers:common_subexpression_elimination", + "//optimizer/optimizers:constant_folding", + "//parser:macro", + "//parser:unparser", + "//policy", + "//policy:compiler", + "//policy:compiler_factory", + "//policy:parser", + "//policy:parser_factory", + "@maven//:junit_junit", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "//:java_truth", + "@maven//:tools_aqua_z3_turnkey", + "//verifier", + "//verifier:policy_verifier", + "//verifier:policy_verifier_factory", + "//verifier:type_system", + "//verifier:verifier_factory", + "//verifier:z3_impl", + "//verifier/axioms", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@maven//:com_google_guava_guava", + ], +) + +junit4_test_suites( + name = "test_suites", + shard_count = 4, + sizes = [ + "small", + "medium", + ], + src_dir = "src/test/java", + deps = [":tests"], +) diff --git a/verifier/src/test/java/dev/cel/verifier/CelPolicyVerifierImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelPolicyVerifierImplTest.java new file mode 100644 index 000000000..a5567c1f1 --- /dev/null +++ b/verifier/src/test/java/dev/cel/verifier/CelPolicyVerifierImplTest.java @@ -0,0 +1,311 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelFactory; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelOptions; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.extensions.CelExtensions; +import dev.cel.optimizer.CelOptimizer; +import dev.cel.optimizer.CelOptimizerFactory; +import dev.cel.optimizer.optimizers.SubexpressionOptimizer; +import dev.cel.optimizer.optimizers.SubexpressionOptimizer.SubexpressionOptimizerOptions; +import dev.cel.parser.CelStandardMacro; +import dev.cel.parser.CelUnparserFactory; +import dev.cel.policy.CelPolicy; +import dev.cel.policy.CelPolicyCompiler; +import dev.cel.policy.CelPolicyCompilerFactory; +import dev.cel.policy.CelPolicyParser; +import dev.cel.policy.CelPolicyParserFactory; +import dev.cel.verifier.CelVerificationResult.VerificationStatus; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class CelPolicyVerifierImplTest { + + private static final CelPolicyParser PARSER = + CelPolicyParserFactory.newYamlParserBuilder().enableSimpleVariables(true).build(); + + private static final Cel CEL = + CelFactory.standardCelBuilder() + .setOptions(CelOptions.current().populateMacroCalls(true).build()) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addCompilerLibraries(CelExtensions.bindings()) + .addMessageTypes(TestAllTypes.getDescriptor()) + .addVar("x", SimpleType.INT) + .addVar("y", SimpleType.INT) + .addVar("a", SimpleType.BOOL) + .addVar("b", SimpleType.BOOL) + .addVar("role", SimpleType.STRING) + .addVar("country", SimpleType.STRING) + .addVar("port", SimpleType.INT) + .addVar("request", SimpleType.DYN) + .addVar( + "test_all_types", + StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")) + .build(); + + private static final CelPolicyCompiler POLICY_COMPILER = + CelPolicyCompilerFactory.newPolicyCompiler(CEL).build(); + + private static final CelVerifier AST_VERIFIER = CelVerifierFactory.newVerifier().build(); + private static final CelPolicyVerifier VERIFIER = + CelPolicyVerifierFactory.newVerifier(POLICY_COMPILER, AST_VERIFIER).build(); + + @Before + public void setUp() { + System.setProperty("z3.skipLibraryLoad", "true"); + } + + private enum EquivalenceTestCase { + DE_MORGANS_LAW( + "name: pA\n" // + + "rule:\n" // + + " match:\n" // + + " - output: '!(a && b)'", + "name: pB\n" // + + "rule:\n" // + + " match:\n" // + + " - output: '!a || !b'"), + CONSTANT_FOLDING( + "name: pA\n" // + + "rule:\n" // + + " match:\n" // + + " - output: 'x > 5 + 5'", + "name: pB\n" // + + "rule:\n" // + + " match:\n" // + + " - output: 'x > 10'"), + STRING_COMPARISON( + "name: pA\n" // + + "rule:\n" // + + " match:\n" // + + " - output: 'role == \"admin\"'", + "name: pB\n" // + + "rule:\n" // + + " match:\n" // + + " - output: '\"admin\" == role'"), + FLATTENED_SELECT( + "name: pA\n" // + + "rule:\n" // + + " match:\n" // + + " - output: 'test_all_types.single_string == \"admin\"'", + "name: pB\n" // + + "rule:\n" // + + " match:\n" // + + " - output: '\"admin\" == test_all_types.single_string'"), + REALISTIC_REFACTORING( + "name: legacy_authz\n" + + "rule:\n" + + " match:\n" + + " - output: '(role == \"admin\" || (role == \"editor\" && country == \"US\")) &&" + + " port == 443'", + "name: refactored_authz\n" + + "rule:\n" + + " variables:\n" + + " - is_admin: 'role == \"admin\"'\n" + + " - is_us_editor: 'role == \"editor\" && country == \"US\"'\n" + + " - is_secure: 'port == 443'\n" + + " match:\n" + + " - output: '(variables.is_admin || variables.is_us_editor) &&" + + " variables.is_secure'"), + MACRO_EXISTS_EQUIVALENT( + "name: pA\n" // + + "rule:\n" // + + " match:\n" // + + " - output: '[1, 2, 3].exists(x, x > 0)'", + "name: pB\n" // + + "rule:\n" // + + " match:\n" // + + " - output: '[1, 2, 3].exists(y, y > 0)'"), + IN_LIST_EQUIVALENT( + "name: pA\n" // + + "rule:\n" // + + " match:\n" // + + " - output: '2 in [1, 2, 3]'", + "name: pB\n" // + + "rule:\n" // + + " match:\n" // + + " - output: '2 in [1, 2, 3] || false'"), + MACRO_ALL_EQUIVALENT( + "name: pA\n" // + + "rule:\n" // + + " match:\n" // + + " - output: '[1, 2, 3].all(x, x > 0)'", + "name: pB\n" // + + "rule:\n" // + + " match:\n" // + + " - output: '1 > 0 && 2 > 0 && 3 > 0'"), + MACRO_EXISTS_ONE_EQUIVALENT( + "name: pA\n" // + + "rule:\n" // + + " match:\n" // + + " - output: '[1, 2, 3].exists_one(x, x == 2)'", + "name: pB\n" // + + "rule:\n" // + + " match:\n" // + + " - output: '(1 == 2 ? 1 : 0) + (2 == 2 ? 1 : 0) + (3 == 2 ? 1 : 0) == 1'"), + MACRO_MAP_EQUIVALENT( + "name: pA\n" // + + "rule:\n" // + + " match:\n" // + + " - output: '{1: true, 2: true, 3: true}.all(k, k > 0)'", + "name: pB\n" // + + "rule:\n" // + + " match:\n" // + + " - output: '1 > 0 && 2 > 0 && 3 > 0'"), + MACRO_SHADOWING_EQUIVALENT( + "name: pA\n" // + + "rule:\n" // + + " variables:\n" // + + " - x: '10'\n" // + + " match:\n" // + + " - output: '[1, 2, 3].all(x, x > 0) && variables.x == 10'", + "name: pB\n" // + + "rule:\n" // + + " match:\n" // + + " - output: '(1 > 0 && 2 > 0 && 3 > 0) && 10 == 10'"), + MACRO_BIND_EQUIVALENT( + "name: pA\n" // + + "rule:\n" // + + " match:\n" // + + " - output: 'cel.bind(x, 10, x > 0)'", + "name: pB\n" // + + "rule:\n" // + + " match:\n" // + + " - output: '10 > 0'"), + COMPREHENSION_SHADOWING_DOES_NOT_LEAK( + "name: pA\n" // + + "rule:\n" // + + " match:\n" // + + " - output: '[1, 2].all(x, x > 0) && x == 0'", + "name: pB\n" // + + "rule:\n" // + + " match:\n" // + + " - output: 'x == 0'"), + MODULO_BY_ONE_EQUIVALENT( + "name: pA\n" // + + "rule:\n" // + + " match:\n" // + + " - output: '5 % 1 == 0'\n", + "name: pB\n" // + + "rule:\n" // + + " match:\n" // + + " - output: 'true'\n"), + EMPTY_VARIABLES( + "name: pA\n" // + + "rule:\n" // + + " variables: []\n" // + + " match:\n" // + + " - output: 'true'", + "name: pB\n" // + + "rule:\n" // + + " match:\n" // + + " - output: 'true'"), + SHADOWED_VARIABLES( + "name: pA\n" // + + "rule:\n" // + + " variables:\n" // + + " - a: '1'\n" // + + " - a: '2'\n" // + + " match:\n" // + + " - output: 'variables.a == 2'", + "name: pB\n" // + + "rule:\n" // + + " match:\n" // + + " - output: 'true'"); + + private final String policySourceA; + private final String policySourceB; + + EquivalenceTestCase(String policySourceA, String policySourceB) { + this.policySourceA = policySourceA; + this.policySourceB = policySourceB; + } + } + + @Test + public void verifyEquivalence_success(@TestParameter EquivalenceTestCase testCase) + throws Exception { + CelPolicy policyA = PARSER.parse(testCase.policySourceA); + CelPolicy policyB = PARSER.parse(testCase.policySourceB); + + CelVerificationResult result = VERIFIER.verifyEquivalence(policyA, policyB); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void verifyEquivalence_violation_throws() throws Exception { + String policySourceA = + "name: pA\n" // + + "rule:\n" // + + " match:\n" // + + " - output: 'x > 10'"; + String policySourceB = + "name: pB\n" // + + "rule:\n" // + + " match:\n" // + + " - output: 'x > 5'"; + CelPolicy policyA = PARSER.parse(policySourceA); + CelPolicy policyB = PARSER.parse(policySourceB); + + CelVerificationResult result = VERIFIER.verifyEquivalence(policyA, policyB); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()) + .containsMatch( + "Equivalence violation detected\\. Counterexample input:\\n x = (6|7|8|9|10)"); + } + + @Test + public void verifyEquivalence_celBlockSupport_cseOptimizer( + @TestParameter({ + "request.a + request.b == request.a + request.b", + "size([1, 2, 3]) + size([1, 2, 3]) == 6", + "size('hello') > 0 && size('hello') > 0", + "test_all_types.single_int32 + test_all_types.single_int32 == 10", + "test_all_types.single_int32 + test_all_types.single_int64 ==" + + " test_all_types.single_int32 + test_all_types.single_int64", + "true || ((1/0 == 1) || (1/0 == 1))", + "(size('hello') + 1) + (size('hello') + 1) * 2" + }) + String expression) + throws Exception { + CelOptimizer celOptimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + .addAstOptimizers( + SubexpressionOptimizer.newInstance( + SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build())) + .build(); + CelAbstractSyntaxTree unoptimizedAst = CEL.compile(expression).getAst(); + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(unoptimizedAst); + CelVerificationResult result = AST_VERIFIER.verifyEquivalence(unoptimizedAst, optimizedAst); + + String unparsed = CelUnparserFactory.newUnparser().unparse(optimizedAst); + assertThat(unparsed).startsWith("cel.@block"); + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } +} diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java new file mode 100644 index 000000000..c16114dd2 --- /dev/null +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -0,0 +1,2188 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +// import com.google.testing.testsize.MediumTest; +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Context; +import com.microsoft.z3.Expr; +import com.microsoft.z3.IntExpr; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelFactory; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOptions; +import dev.cel.common.CelOverloadDecl; +import dev.cel.common.Operator; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.ast.CelExpr.CelCall; +import dev.cel.common.types.ListType; +import dev.cel.common.types.MapType; +import dev.cel.common.types.ProtoMessageTypeProvider; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.compiler.CelCompiler; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.extensions.CelExtensions; +import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.optimizer.CelOptimizer; +import dev.cel.optimizer.CelOptimizerFactory; +import dev.cel.optimizer.optimizers.ConstantFoldingOptimizer; +import dev.cel.parser.CelMacro; +import dev.cel.parser.CelStandardMacro; +import dev.cel.verifier.CelVerificationResult.VerificationStatus; +import dev.cel.verifier.axioms.CelZ3FunctionAxiom; +import dev.cel.verifier.axioms.CelZ3OverloadResult; +import dev.cel.verifier.axioms.CelZ3OverloadTranslator; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +// @MediumTest +@RunWith(TestParameterInjector.class) +public final class CelVerifierZ3ImplTest { + + private static final Cel CEL = + CelFactory.plannerCelBuilder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .addCompilerLibraries( + CelExtensions.bindings(), CelOptionalLibrary.INSTANCE, CelExtensions.comprehensions()) + .addMessageTypes(TestAllTypes.getDescriptor(), TestAllTypes.NestedMessage.getDescriptor()) + .addVar("x", SimpleType.INT) + .addVar("u", SimpleType.UINT) + .addVar("d", SimpleType.DOUBLE) + .addVar("by", SimpleType.BYTES) + .addVar("y", SimpleType.INT) + .addVar("a", SimpleType.BOOL) + .addVar("b", SimpleType.BOOL) + .addVar("role", SimpleType.STRING) + .addVar("country", SimpleType.STRING) + .addVar("port", SimpleType.INT) + .addVar("request", SimpleType.DYN) + .addVar("unknown_var", SimpleType.DYN) + .addVar("int_list", ListType.create(SimpleType.INT)) + .addVar("int_list_2", ListType.create(SimpleType.INT)) + .addVar("nested_list", ListType.create(ListType.create(SimpleType.INT))) + .addVar("nested_list_2", ListType.create(ListType.create(SimpleType.INT))) + .addVar("dyn_list", ListType.create(SimpleType.DYN)) + .addVar("dyn_map", MapType.create(SimpleType.DYN, SimpleType.DYN)) + .addVar("dyn_var", SimpleType.DYN) + .addVar("dyn_var2", SimpleType.DYN) + .addVar("string_int_map", MapType.create(SimpleType.STRING, SimpleType.INT)) + .addVar("bytes_val", SimpleType.BYTES) + .addVar( + "string_int_list_map", + MapType.create(SimpleType.STRING, ListType.create(SimpleType.INT))) + .addVar( + "test_all_types", + StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")) + .build(); + + private static final CelVerifier VERIFIER = + CelVerifierFactory.newVerifier() + .setTypeProvider( + ProtoMessageTypeProvider.newBuilder() + .addDescriptors( + ImmutableList.of( + TestAllTypes.getDescriptor(), TestAllTypes.NestedMessage.getDescriptor())) + .build()) + .build(); + + @Before + public void setUp() { + System.setProperty("z3.skipLibraryLoad", "true"); + } + + private enum IsSatisfiableTestCase { + SATISFIABLE("x > 5"), + DYNAMIC_ARITHMETIC("request == 1 && request + 2 == 3"), + DYNAMIC_ARITHMETIC_UNARY("request == 1 && -request == -1"), + GREATER_DOUBLE("d > 1.5"), + LESS_EQUALS_UINT64("u <= 5u"), + LESS_EQUALS_DOUBLE("d <= 5.5"), + LESS_EQUALS_STRING("role <= 'admin'"), + LESS_EQUALS_BYTES("by <= b'bytes'"), + GREATER_STRING("role > 'admin'"), + GREATER_BYTES("by > b'bytes'"), + DYNAMIC_LIST_COMPREHENSION_EXISTS("int_list.exists(x, x > 5)"), + DYNAMIC_MAP_COMPREHENSION_EXISTS("string_int_map.exists(k, k == 'test')"), + NULL_SATISFIABLE("unknown_var == null"), + DYNAMIC_VAR_NUMERIC_EQUALITY("dyn_var == 1 && dyn_var == 1.0"), + DYNAMIC_VAR_NOT_IN_LIST("dyn_var == 1.5 && !(dyn_var in dyn_list) && size(dyn_list) > 5"), + TIMESTAMP_EQUALITY_TAUTOLOGY( + "timestamp('2023-01-01T00:00:00Z') == timestamp('2023-01-01T00:00:00Z')"), + CROSS_NUMERIC_EQUALITY_INT_DYN_EXACT("1 == request"), + MACRO_LIMIT("dyn_list.all(x, x == 1)"), + STRUCT_FIELD_MISSING_APPROXIMATE_SATISFIABLE("dyn_var.unknown_field"); + + final String expr; + + IsSatisfiableTestCase(String expr) { + this.expr = expr; + } + } + + @Test + public void isSatisfiable_typeConversionApproximation() throws Exception { + CelAbstractSyntaxTree astInt = CEL.compile("type(int('1')) == int").getAst(); + assertThat(VERIFIER.isSatisfiable(astInt).status()).isEqualTo(VerificationStatus.VERIFIED); + + CelAbstractSyntaxTree astDouble = CEL.compile("type(double('1.5')) == double").getAst(); + assertThat(VERIFIER.isSatisfiable(astDouble).status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void isSatisfiable_success(@TestParameter IsSatisfiableTestCase testCase) + throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(testCase.expr).getAst(); + + CelVerificationResult result = VERIFIER.isSatisfiable(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } + + private enum IsSatisfiableInconclusiveTestCase { + MASKED_BY_BMC("int_list == [1, 2, 3, 4, 5, 6] ? int_list.exists(x, x == 42) : false"), + MASKED_BY_BMC_ALL("int_list == [1, 2, 3, 4, 5, 6] ? int_list.all(x, x > 0) : false"), + MASKED_BY_BMC_FILTER( + "int_list == [1, 2, 3, 4, 5, 6] ? size(int_list.filter(x, x > 2)) == 3 : false"), + MASKED_BY_BMC_MAP( + "string_int_map == {'a':1, 'b':2, 'c':3, 'd':4, 'e':5, 'f':6} ? string_int_map.exists(k," + + " k == 'g') : false"), + MASKED_BY_BMC_NESTED( + "nested_list == [[1, 2, 3, 4, 5, 6]] ? nested_list.exists(row, row.exists(x, x == 42)) :" + + " false"), + APPROXIMATED_STRING_TO_INT("int('123') == 123"), + APPROXIMATED_DOUBLE_TO_INT("int(1.5) == 1"), + APPROXIMATED_INT_TO_STRING("string(123) == '123'"), + APPROXIMATED_RANGE("int('123') > 100 && int('123') < 200"), + APPROXIMATED_BRANCHING("int('123') == 123 ? x > 5 : false"); + + final String expr; + + IsSatisfiableInconclusiveTestCase(String expr) { + this.expr = expr; + } + } + + @Test + public void isSatisfiable_inconclusive(@TestParameter IsSatisfiableInconclusiveTestCase testCase) + throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(testCase.expr).getAst(); + + CelVerificationResult result = VERIFIER.isSatisfiable(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + } + + @Test + public void isSatisfiable_comprehensionZeroUnrollLimit_inconclusive() throws Exception { + String expr = "int_list == [1] ? int_list.exists(x, x == 1) : false"; + CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); + + CelVerifier verifier = CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(0).build(); + CelVerificationResult result = verifier.isSatisfiable(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + } + + private enum IsUnsatisfiableTestCase { + UNSATISFIABLE("x > 5 && x < 3"), + CONTRADICTORY_TYPES_FOR_DYNAMIC_VARIABLE("unknown_var + 1 == 2 && unknown_var[0] == 1"), + TYPE_CONVERSION_UNSATISFIABLE_STRING_TO_INT("type(int('1')) == string"), + TYPE_CONVERSION_UNSATISFIABLE_DOUBLE_TO_STRING("type(string(1.5)) == int"), + EMPTY_MAP_SIZE_NOT_ZERO("size({}) != 0"), + TIMESTAMP_INEQUALITY_CONTRADICTION( + "timestamp('2023-01-01T00:00:00Z') != timestamp('2023-01-01T00:00:00Z')"); + + final String expr; + + IsUnsatisfiableTestCase(String expr) { + this.expr = expr; + } + } + + @Test + public void isSatisfiable_failure(@TestParameter IsUnsatisfiableTestCase testCase) + throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(testCase.expr).getAst(); + + CelVerificationResult result = VERIFIER.isSatisfiable(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()).isEqualTo("Condition is not satisfiable."); + } + + @Test + public void isSatisfiable_timeout_throwsException() throws Exception { + CelVerifier verifier = CelVerifierZ3Impl.newBuilder().setTimeout(Duration.ofMillis(1)).build(); + String expr = "int_list.all(x, int_list.all(y, int_list.all(z, x + y + z > 0)))"; + CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); + + CelVerificationException e = + assertThrows(CelVerificationException.class, () -> verifier.isSatisfiable(ast)); + assertThat(e).hasMessageThat().contains("timed out"); + } + + private enum IsAlwaysTrueTestCase { + LOGICAL_OR_CONSTANTS("true || false"), + CYCLIC_MACRO_SHADOWING_SAFETY("[1].all(x, [x].all(x, x == 1))"), + TAUTOLOGY("x > 5 || x <= 5"), + LIST_VARIABLE_CONSTRAINED("1 in int_list || !(1 in int_list)"), + LIST_CONCATENATION("[1, 2, 3] == [1] + [2] + [3]"), + LIST_CONTAINS_LIST("[1] in [[1]]"), + UINT_MIN_RANGE("u >= 0u"), + UINT_ARITHMETIC_ZERO("0u + 0u == 0u"), + MAP_COMPREHENSION("{1: 2, 3: 4}.all(k, k > 0)"), + NESTED_COMPREHENSIONS("[1, 2].all(x, [3, 4].all(y, x < y || y <= x))"), + CEL_BIND_SHADOWING("cel.bind(x, 1, cel.bind(x, 2, x) + x) == 3"), + CEL_BIND_TO_TRUE("cel.bind(x, true, !x) == false"), + CEL_BIND_TO_FALSE("cel.bind(x, false, !x) == true"), + STRING_TAUTOLOGY("role == role"), + BYTES_TAUTOLOGY("by == by"), + STRING_CONCATENATION("'a' + 'b' == 'ab'"), + BYTES_CONCATENATION("b'a' + b'b' == b'ab'"), + STRING_COMPARISON("'a' < 'b' || 'a' >= 'b'"), + LIST_SIZE("size([1, 2]) == 2"), + STRING_SIZE("size('abc') == 3"), + BYTES_SIZE("size(b'abc') == 3"), + CROSS_NUMERIC_EQUALITY_INT_DYN_TAUTOLOGY("1 == request || true"), + MAP_SIZE("size({'a': 1, 'b': 2}) == 2"), + EMPTY_LIST_SIZE("size([]) == 0"), + EMPTY_MAP_SIZE("size({}) == 0"), + MAP_LITERAL_DUPLICATE_KEYS("size({'a': 1, 'a': 2}) == 1"), + LIST_CONCATENATION_SIZE("size(int_list + [1]) == size(int_list) + 1"), + STRING_CONCATENATION_SIZE("size(role + 'a') == size(role) + 1"), + BYTES_CONCATENATION_SIZE("size(by + b'a') == size(by) + 1"), + LIST_SIZE_NON_NEGATIVE("size(int_list) >= 0"), + STRING_SIZE_NON_NEGATIVE("size(role) >= 0"), + MAP_SIZE_NON_NEGATIVE("size(string_int_map) >= 0"), + DYNAMIC_MAP_DUPLICATE_KEYS("!(x == y) || size({x: 1, y: 2}) == 1"), + MEMBER_LIST_SIZE("[1, 2].size() == 2"), + MEMBER_STRING_SIZE("'abc'.size() == 3"), + MEMBER_BYTES_SIZE("b'abc'.size() == 3"), + MEMBER_MAP_SIZE("{'a': 1, 'b': 2}.size() == 2"), + MAP_LOOKUP_EQUALITY("!(x == y) || {x: 1, 'b': 2}[x] == {y: 1, 'b': 2}[y]"), + MAP_INSERT_SIZE( + "dyn_map == {'a': 1, 'b': 2} ? size(dyn_map.transformMap(k, v, v > 1, v * 2)) == 1 : true"), + UNSET_MAP_FIELD_SIZE("TestAllTypes{}.map_int64_int64.size() == 0"), + STRING_CONTAINS_SELF("role.contains(role)"), + STRING_STARTS_WITH_SELF("role.startsWith(role)"), + STRING_ENDS_WITH_SELF("role.endsWith(role)"), + STRING_CONTAINS_SUBSTRING("'abcdef'.contains('bcd')"), + STRING_STARTS_WITH_PREFIX("'abcdef'.startsWith('abc')"), + STRING_ENDS_WITH_SUFFIX("'abcdef'.endsWith('def')"), + STRING_CONTAINS_FALSE("!'abcdef'.contains('xyz')"), + STRING_STARTS_WITH_FALSE("!'abcdef'.startsWith('xyz')"), + STRING_ENDS_WITH_FALSE("!'abcdef'.endsWith('xyz')"), + BYTES_COMPARISON("b'a' < b'b' || b'a' >= b'b'"), + UINT_MUL_ZERO("0u * 0u == 0u"), + UINT_DIV("1u / 1u == 1u"), + UINT_MOD("1u % 1u == 0u"), + INT_MOD("1 % 1 == 0"), + EXISTS_TRUE_COMPREHENSION("[1].exists(x, true) == true"), + COMPREHENSION_EXACT_LIMIT("[1, 2, 3].all(x, x > 0) == true"), + MAP_COMPREHENSION_EXACT_LIMIT("{1: 1, 2: 2, 3: 3}.all(k, k > 0) == true"), + EMPTY_LIST_ALL("[].all(x, false)"), + EMPTY_LIST_EXISTS("[].exists(x, true) == false"), + EMPTY_LIST_MAP("[].map(x, true) == []"), + EMPTY_LIST_FILTER("[].filter(x, true) == []"), + COMPREHENSION_NON_STRICT_SHORT_CIRCUIT_ALL("[1, 0].all(x, 1 / x < 0) == false"), + COMPREHENSION_NON_STRICT_SHORT_CIRCUIT_EXISTS("[1, 0].exists(x, 1 / x > 0) == true"), + COMPREHENSION_SHADOWING_NON_STRICT_EXISTS( + "cel.bind(x, 5, [2, 0].exists(x, x == 2 || 1 / x == 10) && x == 5)"), + COMPREHENSION_SHADOWING_NON_STRICT_ALL( + "cel.bind(x, 5, [0, 2].all(x, x != 0 && 1 / x == 0) == false && x == 5)"), + MAP_NESTED_MACROS("{'a': x, 'b': y}.all(k, {'a': x, 'b': y}.exists(k2, k == k2))"), + MAP_MACRO_ITER_VAR_NOT_REFERENCED( + "{'a': x, 'b': y}.all(i, {'a': x, 'b': y}.exists(i, i == 'a' || i == 'b'))"), + MAP_MACRO_SHADOWED_VARIABLE( + "{'a': x, 'b': y}.all(z, {'a': x, 'b': y}.exists(z, z == 'a' || z == 'b'))"), + MAP_LITERAL_VARIABLE_VALUE("{'a': x}['a'] == x"), + HETEROGENEOUS_LARGE_UINT_INT_VARIABLE_NEQ( + "unknown_var == " + CelZ3TypeSystem.MAX_UINT64 + "u ? unknown_var != -1 : true"), + MAP_LITERAL_VARIABLE_KEY("x != y ? {x: 1, y: 2}[x] == 1 : true"), + MAP_MACRO_LIST_RETURN("{'a': 1, 'b': 2}.map(x, x + 'a') == ['aa', 'ba']"), + MAP_LITERAL_NESTED_LIST("{'a': [1, 2]} == {'a': [1, 2]}"), + LIST_NESTED_LIST("[[1]] == [[1]]"), + LIST_DEEPLY_NESTED_LIST("[[[1]]] == [[[1]]]"), + MAP_DEEPLY_NESTED_MAP("{'a': {'b': {'c': 1}}} == {'a': {'b': {'c': 1}}}"), + MAP_CONTAINS_LIST_OF_MAPS("{'a': [{'b': 1}]} == {'a': [{'b': 1}]}"), + LIST_CONTAINS_MAP_OF_LISTS("[{'a': [1]}] == [{'a': [1]}]"), + MAP_COMPREHENSION_NESTED_STRUCTURE("[1, 2].map(x, {'a': x}) == [{'a': 1}, {'a': 2}]"), + HETEROGENEOUS_LIST_LITERAL("['string', 1, true] == ['string', 1, true]"), + HETEROGENEOUS_NUMERIC_LIST_LITERAL("[1, 1.5] == [1.0, 1.5]"), + HETEROGENEOUS_NUMERIC_LIST_LITERAL_NEQ("[1, 1.5] != [2.0, 1.5]"), + CROSS_TYPE_NUMERIC_EQUALITY_INT_UINT("[x, u] == [u, x] || [x, u] != [u, x]"), + CROSS_TYPE_NUMERIC_EQUALITY_INT_DOUBLE("[x, d] == [d, x] || [x, d] != [d, x]"), + DYNAMIC_LIST_EQUALITY_CROSS_TYPE("dyn_var == [1] ? dyn_var == [1u] : true"), + HETEROGENEOUS_MAP_LITERAL("{'key': 1, 'key2': 'string'} == {'key': 1, 'key2': 'string'}"), + DYNAMIC_ELEMENT_LIST_EQUALITY("[port] == [port]"), + DYNAMIC_ELEMENT_NESTED_LIST_EQUALITY("[[port]] == [[port]]"), + STATIC_LIST_STATIC_INDEX("[1, 2][0] == 1"), + ALL_MACRO_SHORT_CIRCUIT_EARLY("[1, 0].all(x, 1 / x == 0) == false"), + TYPED_LIST_HOMOGENEOUS("(int_list + [1])[0] >= 0 || (int_list + [1])[0] < 0"), + STATIC_NUMERIC_EQUALITY_INT("x == x"), + STATIC_NUMERIC_EQUALITY_UINT("u == u"), + DYNAMIC_MAP_EQUALITY("string_int_map == {'a': 1} || string_int_map != {'a': 1}"), + DYNAMIC_LIST_VARIABLE_EQUALITY("dyn_list == dyn_list"), + DYNAMIC_LIST_COMPREHENSION_ALL_SHORT_CIRCUITS_ERROR( + "dyn_list == [false, 1] ? (dyn_list.all(x, x) == false) : true"), + DYNAMIC_LIST_COMPREHENSION_EXISTS_SHORT_CIRCUITS_ERROR( + "dyn_list == [true, 1] ? (dyn_list.exists(x, x) == true) : true"), + DYNAMIC_LIST_COMPREHENSION_EMPTY_ALL_IS_TRUE( + "dyn_list == [] ? dyn_list.all(x, false) == true : true"), + DYNAMIC_LIST_COMPREHENSION_EMPTY_EXISTS_IS_FALSE( + "dyn_list == [] ? dyn_list.exists(x, true) == false : true"), + DYNAMIC_MAP_COMPREHENSION_HETEROGENEOUS_KEYS( + "dyn_map == {'a': 1, 'b': 2} ? dyn_map.all(k, k != 1) : true"), + DYNAMIC_LIST_VACUOUS_TRUTH_SWALLOWS_ERROR("dyn_list == [] ? dyn_list.all(x, 1/0 == 1) : true"), + LARGE_NUMBER_OF_LIST_EQUALITIES( + IntStream.range(0, 1000) + .mapToObj(i -> String.format("[%d] == [%d]", i, i)) + .collect(Collectors.joining(" && "))), + DYNAMIC_MAP_KEY_EXTENSIONALITY( + "dyn_map == {'a': 1, 'b': 2} ? dyn_map.all(k, k in {'a': 1, 'b': 2}) : true"), + DYNAMIC_LIST_MAP_STANDARD("int_list == [1, 2, 3] ? int_list.map(x, x * 2) == [2, 4, 6] : true"), + DYNAMIC_LIST_FILTER_STANDARD( + "int_list == [1, 2, 3] ? int_list.filter(x, x % 2 != 0) == [1, 3] : true"), + DYNAMIC_LIST_MACRO_CHAINING( + "int_list == [1, 2, 3] ? int_list.filter(x, x > 1).map(y, y * 10) == [20, 30] : true"), + DYNAMIC_LIST_MAP_NESTED_LISTS( + "int_list == [1, 2] ? int_list.map(x, [x, x]) == [[1, 1], [2, 2]] : true"), + DYNAMIC_LIST_MACRO_EMPTY_IDENTITY( + "int_list == [] ? int_list.map(x, x * 2) == [] && int_list.filter(x, true) == [] : true"), + DYNAMIC_LIST_NESTED_SHADOWING_COLLISION( + "int_list == [1, 2] ? int_list.map(x, int_list.filter(x, x > 1)) == [[2], [2]] : true"), + DYNAMIC_LIST_FILTER_TO_EMPTY("int_list == [1, 2, 3] ? int_list.filter(x, x > 10) == [] : true"), + DYNAMIC_LIST_MAP_PRESERVES_ACCUMULATOR_OUT_OF_BOUNDS( + "int_list == [99] ? int_list.map(x, x + 1) == [100] : true"), + DYNAMIC_LIST_MATRIX_GENERATION( + "int_list == [1, 2] ? int_list.map(x, [x * 2]) == [[2], [4]] : true"), + DIVIDE_TRUNCATED("-5 / 3 == -1"), + MODULO_TRUNCATED("-5 % 3 == -2"), + EXPLICIT_DEFAULT_READ("TestAllTypes{single_int32: 0}.single_int32 == 0"), + STRUCT_EMPTY_MESSAGE_FIELD_INEQUALITY( + "TestAllTypes{standalone_message: TestAllTypes.NestedMessage{}} != TestAllTypes{}"), + IEEE_754_NAN_NEQ_ITSELF("(0.0 / 0.0) != (0.0 / 0.0)"), + IEEE_754_NAN_RELATIONAL_OPS_FALSE("!((0.0 / 0.0) < 1.0 || (0.0 / 0.0) >= 1.0)"), + IEEE_754_INFINITY_EQ("(1.0 / 0.0) == (2.0 / 0.0)"), + IEEE_754_INFINITY_ADD_FINITE_EQ_INFINITY("(1.0 / 0.0) + 9999999.0 == (1.0 / 0.0)"), + IEEE_754_INFINITY_GT_NEG_INFINITY("(1.0 / 0.0) > (-1.0 / 0.0)"), + IEEE_754_INFINITY_NEQ_NEG_INFINITY("(1.0 / 0.0) != (1.0 / -0.0)"), + IEEE_754_NEG_ZERO_EQ("-0.0 == 0.0"), + OPTIONAL_NONE_EQ_NONE("optional.none() == optional.none()"), + OPTIONAL_OF_EQ_OF("optional.of(1) == optional.of(1)"), + OPTIONAL_OF_NEQ_NONE("optional.of(1) != optional.none()"), + OPTIONAL_VALUE("optional.of('test').value() == 'test'"), + OPTIONAL_HAS_VALUE_TRUE("optional.of(true).hasValue()"), + OPTIONAL_HAS_VALUE_FALSE("!optional.none().hasValue()"), + OPTIONAL_OR_VALUE_FALLBACK("optional.none().orValue(5) == 5"), + OPTIONAL_OR_VALUE_PRIMARY("optional.of(5).orValue(10) == 5"), + OPTIONAL_OR_FALLBACK("optional.none().or(optional.of(5)) == optional.of(5)"), + OPTIONAL_OR_PRIMARY("optional.of(5).or(optional.none()) == optional.of(5)"), + OPTIONAL_OF_EQ_OF_VAR("optional.of(x) == optional.of(x)"), + OPTIONAL_OF_NEQ_OF_VAR("x != y ? optional.of(x) != optional.of(y) : true"), + OPTIONAL_OR_VALUE_VAR("optional.of(x).orValue(y) == x"), + OPTIONAL_OR_VALUE_FALLBACK_VAR("optional.none().orValue(x) == x"), + OPTIONAL_OR_VAR("optional.of(x).or(optional.of(y)) == optional.of(x)"), + OPTIONAL_OR_FALLBACK_VAR("optional.none().or(optional.of(x)) == optional.of(x)"), + OPTIONAL_OR_NONE_IS_NONE("optional.none().or(optional.none()) == optional.none()"), + OPTIONAL_VALUE_VAR("optional.of(x).value() == x"), + OPTIONAL_HAS_VALUE_VAR("optional.of(x).hasValue()"), + IEEE_754_PROTO_NEG_ZERO_NEQ( + "TestAllTypes{single_double: -0.0} != TestAllTypes{single_double: 0.0}"), + IEEE_754_ROUND_NEAREST_TIES_TO_EVEN_DOWN("1.0 + 1.1102230246251565e-16 == 1.0"), + IEEE_754_ROUND_NEAREST_TIES_TO_EVEN_UP( + "1.0 + 2.220446049250313e-16 + 1.1102230246251565e-16 == 1.0 + 2.0 *" + + " 2.220446049250313e-16"), + IEEE_754_NEG_ZERO_IN_LIST("-0.0 in [0.0]"), + IEEE_754_POS_ZERO_IN_LIST("0.0 in [-0.0]"), + IEEE_754_NAN_IN_LIST_FALSE("!((0.0/0.0) in [1.0, (0.0/0.0)])"), + STRING_IN_LIST("'b' in ['a', 'b', 'c']"), + INT_IN_MAP("1 in {1: 2}"), + MAP_MISSING_KEY("!(3 in {1: 'a', 2: 'b'})"), + MAP_VARIABLE_CONSTRAINED("'a' in string_int_map || !('a' in string_int_map)"), + NESTED_MAP_CONTAINS("1 in {'a': {1: 'b'}}['a']"), + HETEROGENEOUS_INT_DOUBLE_LT("1 < 2.0"), + HETEROGENEOUS_DOUBLE_INT_LT("1.0 < 2"), + HETEROGENEOUS_UINT_DOUBLE_LT("1u < 2.0"), + HETEROGENEOUS_DOUBLE_UINT_LT("1.0 < 2u"), + HETEROGENEOUS_INT_UINT_LT("1 < 2u"), + HETEROGENEOUS_UINT_INT_LT("1u < 2"), + HETEROGENEOUS_INT_DOUBLE_GT("2 > 1.0"), + HETEROGENEOUS_DOUBLE_INT_GT("2.0 > 1"), + HETEROGENEOUS_UINT_DOUBLE_GT("2u > 1.0"), + HETEROGENEOUS_DOUBLE_UINT_GT("2.0 > 1u"), + HETEROGENEOUS_INT_UINT_GT("2 > 1u"), + HETEROGENEOUS_UINT_INT_GT("2u > 1"), + HETEROGENEOUS_INT_DOUBLE_LE("1 <= 2.0"), + HETEROGENEOUS_DOUBLE_INT_LE("1.0 <= 2"), + HETEROGENEOUS_UINT_DOUBLE_LE("1u <= 2.0"), + HETEROGENEOUS_DOUBLE_UINT_LE("1.0 <= 2u"), + HETEROGENEOUS_INT_UINT_LE("1 <= 2u"), + HETEROGENEOUS_UINT_INT_LE("1u <= 2"), + HETEROGENEOUS_INT_DOUBLE_GE("2 >= 1.0"), + HETEROGENEOUS_DOUBLE_INT_GE("2.0 >= 1"), + HETEROGENEOUS_UINT_DOUBLE_GE("2u >= 1.0"), + HETEROGENEOUS_DOUBLE_UINT_GE("2.0 >= 1u"), + HETEROGENEOUS_INT_UINT_GE("2 >= 1u"), + HETEROGENEOUS_UINT_INT_GE("2u >= 1"), + HETEROGENEOUS_INT_DOUBLE_PRECISION("9007199254740993 > 9007199254740992.0"), + CYCLIC_BIND_DOES_NOT_HANG("cel.bind(x, x, x) == x"), + HETEROGENEOUS_INT_UINT_VARIABLE_EQ("unknown_var == 1u ? unknown_var == 1 : true"), + HETEROGENEOUS_INT_UINT_VARIABLE_VARIABLE_EQ( + "unknown_var == u && x == 1 && u == 1u ? unknown_var == x : true"), + HETEROGENEOUS_UINT_INT_VARIABLE_VARIABLE_EQ( + "unknown_var == x && u == 1u && x == 1 ? unknown_var == u : true"), + HETEROGENEOUS_INT_UINT_VARIABLE_NEQ("unknown_var == 2u ? unknown_var != 1 : true"), + HETEROGENEOUS_DOUBLE_INT_OVERFLOW( + "unknown_var == 9223372036854775807 ? unknown_var != 1e100 : true"), + DYNAMIC_LIST_RESOLVES_QUANTIFIER_LOOPS( + "int_list == [1] ? !(int_list.all(x, int_list.exists(y, y == x + 1))) : true"), + DYNAMIC_LIST_RESOLVES_PIGEONHOLE( + "int_list == [x, y, port] && int_list.all(v, v == 1) ? (x == 1 && y == 1 && port == 1)" + + " : true"), + DYNAMIC_LIST_RESOLVES_CORRELATED_NESTING( + "int_list == [1, 2] && int_list_2 == [2, 3] ? int_list.exists(x, int_list_2.exists(y, x" + + " == y)) : true"), + DYNAMIC_MAP_EXISTS( + "string_int_map == {'a': 1, 'b': 2} ? string_int_map.exists(k, string_int_map[k] == 2)" + + " : true"), + DYNAMIC_MAP_ALL("string_int_map == {'a': 1, 'b': 2} ? string_int_map.all(k, k != 'c') : true"), + DYNAMIC_MAP_MAP("string_int_map == {'a': 1} ? string_int_map.map(k, k) == ['a'] : true"), + DYNAMIC_MAP_FILTER( + "string_int_map == {'a': 1, 'b': 2} ? string_int_map.filter(k, k == 'a') == ['a'] : true"), + DYNAMIC_LIST_EXISTS_ONE("int_list == [1, 2, 3] ? int_list.exists_one(x, x == 2) : true"), + DYNAMIC_MAP_EXISTS_ONE( + "string_int_map == {'a': 1, 'b': 2} ? string_int_map.exists_one(k, k == 'a') : true"), + DYNAMIC_LIST_COMPREHENSION_ITE_SHORT_CIRCUIT_ERROR( + "dyn_list == [1, 2] ? !(dyn_list.all(x, x == 1 ? (1/0 == 0) : false)) : true"), + DYNAMIC_MAP_KEYS_EQUIVALENCE( + "string_int_map == {'a': 1} ? string_int_map.map(k, k) == ['a'] : true"), + UINT_SUBTRACT("3u - 2u == 1u"), + DOUBLE_SUBTRACT("3.0 - 2.0 == 1.0"), + // TODO: Cross-type numeric equality evaluates to strictly false, causing this + // test's condition to become a tautology. + // CROSS_TYPE_DYNAMIC_EQUALITY_NOT_ALWAYS_UNEQUAL_INT_DOUBLE( + // "!(request == unknown_var && type(request) == int && type(unknown_var) == double)"), + DYNAMIC_LIST_V2_ALL("int_list == [1, 2] ? int_list.all(i, v, v > 0 && i >= 0) : true"), + DYNAMIC_LIST_V2_EXISTS("int_list == [1, 2] ? int_list.exists(i, v, i == 0 && v == 1) : true"), + DYNAMIC_MAP_V2_ALL( + "string_int_map == {'a': 1} ? string_int_map.all(k, v, k == 'a' && v == 1) : true"), + DYNAMIC_MAP_V2_EXISTS( + "string_int_map == {'a': 1, 'b': 2} ? string_int_map.exists(k, v, k == 'b' && v == 2) :" + + " true"), + LITERAL_LIST_V2_ALL("[1, 2].all(i, v, i >= 0 && v > 0)"), + LITERAL_MAP_V2_EXISTS("{'a': 1, 'b': 2}.exists(k, v, k == 'b' && v == 2)"), + LITERAL_LIST_TRANSFORM_LIST_V2_FILTER("[1, 2, 3].transformList(i, v, i < 2, v * 2) == [2, 4]"), + LITERAL_MAP_TRANSFORM_MAP_V2_FILTER( + "{'a': 1, 'b': 2}.transformMap(k, v, v > 1, v * 2) == {'b': 4}"), + TWO_VAR_HETEROGENEOUS_MAP( + "dyn_map == {'a': 1, 1: 'b'} ? dyn_map.exists(k, v, type(k) == string && type(v) == int &&" + + " k == 'a' && v == 1) : true"), + TYPE_AXIOM_BOOL("type(true) == bool"), + TYPE_AXIOM_INT("type(1) == int"), + TYPE_AXIOM_UINT("type(1u) == uint"), + TYPE_AXIOM_DOUBLE("type(1.0) == double"), + TYPE_AXIOM_STRING("type('a') == string"), + TYPE_AXIOM_BYTES("type(b'a') == bytes"), + TYPE_AXIOM_LIST("type([1]) == list"), + TYPE_AXIOM_MAP("type({'a': 1}) == map"), + TYPE_CONVERSION_INT_IDENTITY("int(1) == 1"), + TYPE_CONVERSION_UINT_IDENTITY("uint(1u) == 1u"), + TYPE_CONVERSION_DOUBLE_IDENTITY("double(1.0) == 1.0"), + TYPE_CONVERSION_STRING_IDENTITY("string('a') == 'a'"), + TYPE_CONVERSION_BYTES_IDENTITY("bytes(b'a') == b'a'"), + TYPE_CONVERSION_BOOL_IDENTITY("bool(true) == true"), + TYPE_CONVERSION_DYN_IDENTITY("dyn(1) == 1"), + TYPE_CONVERSION_UINT_TO_INT("int(1u) == 1"), + TYPE_CONVERSION_INT_TO_UINT("uint(1) == 1u"), + TYPE_CONVERSION_INT_FROM_DOUBLE("int(1.0) == int(1.0)"), + TYPE_CONVERSION_INT_FROM_STRING("int('1') == int('1')"), + TYPE_CONVERSION_INT_FROM_TIMESTAMP( + "int(timestamp('1970-01-01T00:00:00Z')) == int(timestamp('1970-01-01T00:00:00Z'))"), + TYPE_CONVERSION_UINT_FROM_DOUBLE("uint(1.0) == uint(1.0)"), + TYPE_CONVERSION_UINT_FROM_STRING("uint('1') == uint('1')"), + TYPE_CONVERSION_DOUBLE_FROM_INT("double(1) == double(1)"), + TYPE_CONVERSION_DOUBLE_FROM_UINT("double(1u) == double(1u)"), + TYPE_CONVERSION_DOUBLE_FROM_STRING("double('1.0') == double('1.0')"), + TYPE_CONVERSION_STRING_FROM_INT("string(1) == string(1)"), + TYPE_CONVERSION_STRING_FROM_UINT("string(1u) == string(1u)"), + TYPE_CONVERSION_STRING_FROM_DOUBLE("string(1.0) == string(1.0)"), + TYPE_CONVERSION_STRING_FROM_BOOL("string(true) == string(true)"), + TYPE_CONVERSION_STRING_FROM_BYTES("string(b'foo') == string(b'foo')"), + TYPE_CONVERSION_STRING_FROM_TIMESTAMP( + "string(timestamp('1970-01-01T00:00:00Z')) == string(timestamp('1970-01-01T00:00:00Z'))"), + TYPE_CONVERSION_STRING_FROM_DURATION("string(duration('1s')) == string(duration('1s'))"), + TYPE_CONVERSION_BYTES_FROM_STRING("bytes('foo') == bytes('foo')"), + TYPE_CONVERSION_DURATION_FROM_STRING("duration('1s') == duration('1s')"), + TYPE_CONVERSION_TIMESTAMP_FROM_STRING( + "timestamp('1970-01-01T00:00:00Z') == timestamp('1970-01-01T00:00:00Z')"), + TYPE_CONVERSION_TIMESTAMP_FROM_INT("timestamp(1) == timestamp(1)"), + TYPE_CONVERSION_BOOL_FROM_STRING("bool('true') == bool('true')"), + TYPE_CONVERSION_INT_TO_UINT_ZERO("uint(0) == 0u"), + + TYPE_AXIOM_OPTIONAL("type(optional.of(1)) == optional_type"), + TYPE_AXIOM_STRUCT("type(TestAllTypes{}) == type(TestAllTypes{})"), + TWO_VAR_TRANSFORM_MAP_DYNAMIC( + "dyn_map == {'a': 1, 'b': 2} ? dyn_map.transformMap(k, v, v > 1, v * 2) == {'b': 4} :" + + " true"), + NULL_EQ_NULL("null == null"), + NULL_NEQ_DYN("request == 1 ? request != null : true"), + TYPE_AXIOM_NULL("type(null) == null_type"), + WRAPPER_UNSET_IS_NULL("TestAllTypes{}.single_int64_wrapper == null"), + WRAPPER_SET_NULL_IS_NULL( + "TestAllTypes{single_int64_wrapper: null}.single_int64_wrapper == null"), + WRAPPER_SET_NULL_EQ_UNSET("TestAllTypes{single_int64_wrapper: null} == TestAllTypes{}"), + STRING_CONTAINS_EMPTY("role.contains('')"), + STRING_STARTS_WITH_EMPTY("role.startsWith('')"), + STRING_ENDS_WITH_EMPTY("role.endsWith('')"), + STRING_CONTAINS_CONCAT_LEFT("('prefix_' + role).contains(role)"), + STRING_CONTAINS_CONCAT_RIGHT("(role + '_suffix').contains(role)"), + STRING_CONTAINS_CONCAT_BOTH("('prefix_' + role + '_suffix').contains(role)"), + STRING_STARTS_WITH_CONCAT("(role + '_suffix').startsWith(role)"), + STRING_ENDS_WITH_CONCAT("('prefix_' + role).endsWith(role)"), + STRING_CONTAINS_IMPLIES_SIZE("role.contains('abc') ? size(role) >= 3 : true"), + STRING_STARTS_WITH_IMPLIES_SIZE("role.startsWith('abc') ? size(role) >= 3 : true"), + STRING_ENDS_WITH_IMPLIES_SIZE("role.endsWith('abc') ? size(role) >= 3 : true"), + STRING_CANNOT_CONTAIN_LONGER("!role.contains(role + 'a')"), + STRING_CANNOT_START_WITH_LONGER("!role.startsWith(role + 'a')"), + STRING_CANNOT_END_WITH_LONGER("!role.endsWith('a' + role)"), + STRING_PREFIX_TRANSITIVITY( + "role.startsWith(country) && country.startsWith('US') ? role.startsWith('US') : true"), + IN_LIST_IEEE_754_POS_NEG_ZERO("dyn_list == [-0.0] ? 0.0 in dyn_list : true"), + IN_LIST_IEEE_754_NEG_POS_ZERO("dyn_list == [0.0] ? -0.0 in dyn_list : true"), + IN_LIST_IEEE_754_NAN_NEQ("dyn_list == [0.0/0.0] ? !(0.0/0.0 in dyn_list) : true"), + DYNAMIC_MAP_LOOKUP_IDENTITY( + "1 in dyn_map && type(dyn_map[1]) == int ? dyn_map[1] == dyn_map[1] : true"), + DYNAMIC_LIST_LOOKUP_IDENTITY( + "size(dyn_list) > 1 && type(dyn_list[1]) == int ? dyn_list[1] == dyn_list[1] : true"), + DYNAMIC_NESTED_LIST_EQUALITY("dyn_var == [1, 2] ? [dyn_var] == [[1, 2]] : true"), + DYNAMIC_NESTED_MAP_EQUALITY("dyn_var == {'a': 1} ? [dyn_var] == [{'a': 1}] : true"), + DYNAMIC_INCLUSION_ENFORCES_EXTENSIONALITY( + "type(dyn_var) == list && type(dyn_var2) == list && dyn_var == dyn_var2 " + + "? dyn_var in [dyn_var2] " + + ": true"), + DYNAMIC_NESTED_LIST_EQUALITY_NEEDS_EXTENSIONALITY( + "int_list == [x] && int_list_2 == [y] && x == y ? int_list == int_list_2 : true"), + LIST_INDEX_TYPE_CONSTRAINT("size(int_list) > 15 ? type(int_list[15]) == int : true"), + MAP_INDEX_TYPE_CONSTRAINT( + "'key' in string_int_map ? type(string_int_map['key']) == int : true"), + LITERAL_LIST_INDEX("[1, 2][0] == 1"), + NESTED_LIST_VARIABLES_EQUALITY( + "nested_list == [[1]] && nested_list_2 == [[1]] ? nested_list == nested_list_2 : true"); + + final String expr; + + IsAlwaysTrueTestCase(String expr) { + this.expr = expr; + } + } + + @Test + public void isAlwaysTrue_success(@TestParameter IsAlwaysTrueTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(testCase.expr).getAst(); + CelVerificationResult result = VERIFIER.isAlwaysTrue(ast); + + assertWithMessage(result.message()) + .that(result.status()) + .isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void isAlwaysTrue_withUnknownIdentifier_evaluatesToUnknown( + @TestParameter({ + "x == x", + "[x] == [x]", + "{1: x} == {1: x}", + }) + String expression) + throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(expression).getAst(); + + CelVerifier verifierWithUnknown = + CelVerifierFactory.newVerifier().addUnknownIdentifier("x").build(); + + // 'x == x' is not a tautology if it can be unknown + // i.e: CelUnknown == CelUnknown is unknown. + CelVerificationResult result = verifierWithUnknown.isAlwaysTrue(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()).contains("x = Unknown"); + } + + @Test + public void isAlwaysTrue_dynamicComprehensionNonBoolYieldsError() throws Exception { + // Testing the path where a comprehension step successfully evaluates but yields a + // non-boolean result. To bypass standard field access CEL errors, we constrain dyn_list to a + // map containing an integer value. + // 'x.not_a_bool' successfully evaluates to 123 (int). Since 'all' strictly expects a bool, + // this yields a CEL Error. The entire ternary evaluates to Error, so isAlwaysTrue is false. + String expr = + "dyn_list == [{'not_a_bool': 123}] ? !(dyn_list.all(x, x.not_a_bool) == true ||" + + " dyn_list.all(x, x.not_a_bool) == false) : true"; + CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); + CelVerificationResult result = + CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(1).build().isAlwaysTrue(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()) + .isEqualTo( + "Condition is not always true. Counterexample input:\n" + + " dyn_list = [{\"not_a_bool\": 123}]"); + } + + @Test + public void isAlwaysTrue_comprehensionExceedsMaxIterations_returnsUnknown() throws Exception { + String expr = "int_list == [1, 2, 3] ? int_list.all(x, x > 0) : true"; + CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); + + CelVerifier verifier = CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(2).build(); + CelVerificationResult verifiedValue = verifier.isAlwaysTrue(ast); + + // Truncated loops return Unknown, which negates to Unknown. + // Since it's not strictly TRUE, isAlwaysTrue returns false. + assertThat(verifiedValue.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + } + + @Test + public void isAlwaysTrue_comprehensionZeroUnrollLimit_emptyList() throws Exception { + // Tests that mkOrFlattened and mkAndFlattened gracefully handle empty iteration loops. + // A limit of 0 means the loop body is never built. + // For an empty list, length is 0, so isTruncated (0 > 0) is false. + // Thus an empty list correctly evaluates to true for 'all' even with a 0 unroll limit. + String expr = "int_list == [] ? int_list.all(x, x > 0) : true"; + CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); + + CelVerifier verifier = CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(0).build(); + CelVerificationResult verifiedValue = verifier.isAlwaysTrue(ast); + + assertThat(verifiedValue.status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void isAlwaysTrue_unpopulatedTypes_throwsIllegalArgumentException() throws Exception { + CelAbstractSyntaxTree parsedAst = CEL.parse("1 == 1").getAst(); + + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> VERIFIER.isAlwaysTrue(parsedAst)); + assertThat(e).hasMessageThat().contains("AST must be type-checked"); + } + + @Test + public void isAlwaysTrue_strictCustomFunction_errorPropagates() throws Exception { + Cel celWithCustomFunc = + CelFactory.plannerCelBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "custom_func", + CelOverloadDecl.newGlobalOverload( + "custom_func_overload", SimpleType.INT, SimpleType.INT))) + .build(); + CelAbstractSyntaxTree ast = + celWithCustomFunc.compile("custom_func(1 / 0) == 5 || !(custom_func(1 / 0) == 5)").getAst(); + + CelVerificationResult result = VERIFIER.isAlwaysTrue(ast); + + // Should fail verification because the error propagates through custom_func, + // so the whole expression evaluates to an error, not true. + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()) + .isEqualTo( + "Condition is not always true. (The expression fails unconditionally, regardless of" + + " input state)"); + } + + @Test + public void verifyEquivalence_unknownPrecedenceOverError() throws Exception { + Cel celWithCustomFunc = + CelFactory.plannerCelBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addVar("unknown_var", SimpleType.DYN) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "custom_func", + CelOverloadDecl.newGlobalOverload( + "custom_func_overload", SimpleType.INT, SimpleType.INT, SimpleType.DYN))) + .build(); + CelAbstractSyntaxTree astA = + celWithCustomFunc.compile("custom_func(1 / 0, unknown_var)").getAst(); + CelAbstractSyntaxTree astB = celWithCustomFunc.compile("1 / 0").getAst(); + + CelVerifier verifier = + CelVerifierFactory.newVerifier().addUnknownIdentifier("unknown_var").build(); + + CelVerificationResult result = verifier.verifyEquivalence(astA, astB); + + // Because Unknown has higher precedence than Error, custom_func evaluates to Unknown + // when unknown_var is Unknown, whereas `1 / 0` evaluates to Error. Thus they are not + // equivalent. + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + } + + @Test + public void isSatisfiable_approximateIterRangeInMap_inconclusive() throws Exception { + Cel celWithCustomFunc = + CelFactory.plannerCelBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "approx_list", + CelOverloadDecl.newGlobalOverload( + "approx_list_overload", ListType.create(SimpleType.INT), SimpleType.INT))) + .build(); + CelAbstractSyntaxTree ast = + celWithCustomFunc.compile("approx_list(1).map(x, x * 2) == []").getAst(); + + CelVerificationResult result = VERIFIER.isSatisfiable(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + } + + private enum UnconditionalErrorTestCase { + MAP_MISSING_KEY("{'a': 1}['b'] == 0 || !({'a': 1}['b'] == 0)"), + COLLECTION_ERROR("{'a': 1 / 0} == {'a': 1 / 0}"), + LIST_ERROR("[1 / 0] == [1 / 0]"), + STRICT_LITERAL_ERROR("{'a': 1/0}.exists(k, k == 'a') == {'a': 1/0}.exists(k, k == 'a')"), + STRICT_LITERAL_ERROR_KEY("{1/0: 'a'}.exists(k, k == 1) == {1/0: 'a'}.exists(k, k == 1)"); + + final String expr; + + UnconditionalErrorTestCase(String expr) { + this.expr = expr; + } + } + + @Test + public void isAlwaysTrue_unconditionalError_failsVerification( + @TestParameter UnconditionalErrorTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(testCase.expr).getAst(); + + CelVerificationResult result = VERIFIER.isAlwaysTrue(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()) + .isEqualTo( + "Condition is not always true. (The expression fails unconditionally, regardless of" + + " input state)"); + } + + @Test + public void verifyEquivalence_unconditionalError_failsVerification( + @TestParameter UnconditionalErrorTestCase testCase) throws Exception { + CelAbstractSyntaxTree astA = CEL.compile(testCase.expr).getAst(); + CelAbstractSyntaxTree astB = CEL.compile("true").getAst(); + + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()) + .isEqualTo( + "Equivalence violation detected. (The expression fails unconditionally, regardless of" + + " input state)"); + } + + + + @Test + public void verifyEquivalence_dynamicMapEquality_enforcesExtensionalityOnPresentKeys() + throws Exception { + CelAbstractSyntaxTree astA = CEL.compile("string_int_map == {'a': 1}").getAst(); + CelAbstractSyntaxTree astB = CEL.compile("true").getAst(); + + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()).contains("string_int_map ="); + assertThat(result.message()).doesNotContain("string_int_map = {'a': 1}"); + } + + private enum MalformedAstTestCase { + LOGICAL_AND( + Operator.LOGICAL_AND, + ImmutableList.of( + CelExpr.newBuilder().setId(2).setConstant(CelConstant.ofValue(true)).build()), + "expects at least 2 arguments, got 1"), + NEGATE(Operator.NEGATE, ImmutableList.of(), "expects 1 argument, got 0"), + CONDITIONAL( + Operator.CONDITIONAL, + ImmutableList.of( + CelExpr.newBuilder().setId(2).setConstant(CelConstant.ofValue(true)).build(), + CelExpr.newBuilder().setId(3).setConstant(CelConstant.ofValue(1L)).build()), + "expects 3 arguments, got 2"), + LESS( + Operator.LESS, + ImmutableList.of( + CelExpr.newBuilder().setId(2).setConstant(CelConstant.ofValue(1L)).build()), + "expects 2 arguments, got 1"); + + final Operator operator; + final ImmutableList args; + final String expectedError; + + MalformedAstTestCase(Operator operator, ImmutableList args, String expectedError) { + this.operator = operator; + this.args = args; + this.expectedError = expectedError; + } + } + + + + @Test + public void isAlwaysTrue_malformedAst_throwsIllegalArgumentException( + @TestParameter MalformedAstTestCase testCase) throws Exception { + CelAbstractSyntaxTree validAst = CEL.compile("1 < 2").getAst(); + + CelCall.Builder callBuilder = CelCall.newBuilder().setFunction(testCase.operator.getFunction()); + for (CelExpr arg : testCase.args) { + callBuilder.addArgs(arg); + } + + CelExpr malformedExpr = CelExpr.newBuilder().setId(1).setCall(callBuilder.build()).build(); + + CelAbstractSyntaxTree malformedAst = + CelAbstractSyntaxTree.newCheckedAst( + malformedExpr, validAst.getSource(), validAst.getReferenceMap(), validAst.getTypeMap()); + + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> VERIFIER.isAlwaysTrue(malformedAst)); + assertThat(e).hasMessageThat().contains(testCase.expectedError); + } + + @Test + public void verifyEquivalence_infinityConstants_notEquivalent() throws Exception { + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) + .build(); + CelAbstractSyntaxTree astA = + optimizer.optimize(CEL.compile("d == double('Infinity')").getAst()); + CelAbstractSyntaxTree astB = + optimizer.optimize(CEL.compile("d == double('-Infinity')").getAst()); + + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + } + + private enum IsAlwaysTrueViolationTestCase { + NOT_ALWAYS_TRUE("x > 5", "Condition is not always true.", "Counterexample input:", "x ="), + LAW_OF_EXCLUDED_MIDDLE_FAILS_WITH_ERRORS( + "(1 / 0 == 5) || !(1 / 0 == 5)", "Condition is not always true."), + INTEGER_OVERFLOW_FAILS_WITH_ERRORS( + "(x + 1) - 1 == x", "Condition is not always true.", "Counterexample input:", "x ="), + UINT_SUBTRACT_UNDERFLOW_FAILS_WITH_ERRORS( + "(u - 1u) + 1u == u", "Condition is not always true.", "Counterexample input:", "u ="), + NEGATE_MIN_INT_FAILS_WITH_ERRORS( + "-(-x) == x", "Condition is not always true.", "Counterexample input:", "x ="), + HETEROGENEOUS_ARITHMETIC_FAILS("dyn(1) + 1u == 2u", "Condition is not always true."), + CROSS_TYPE_SYMBOLIC_EQUALITY_NOT_ALWAYS_UNEQUAL_INT_UINT( + "dyn(x) != dyn(u)", "Condition is not always true."), + CROSS_TYPE_SYMBOLIC_EQUALITY_NOT_ALWAYS_UNEQUAL_UINT_INT( + "dyn(u) != dyn(x)", "Condition is not always true."), + DYNAMIC_MAP_ALL_VIOLATION( + "string_int_map == {'a': 1, 'b': 2} ? string_int_map.all(k, k == 'a') : true", + "Condition is not always true.", + "Counterexample input:", + "string_int_map = {", + "\"a\": 1", + "\"b\": 2"), + DYNAMIC_MAP_EXISTS_VIOLATION( + "string_int_map == {'a': 1, 'b': 2} ? string_int_map.exists(k, k == 'c') : true", + "Condition is not always true.", + "Counterexample input:", + "string_int_map = {", + "\"a\": 1", + "\"b\": 2"), + LITERAL_MAP_KEY_ERROR_PROPAGATES( + "{1/0: 1}.all(k, true) == true", + "Condition is not always true.", + "(The expression fails unconditionally, regardless of input state)"), + DYNAMIC_LIST_EXISTS_ONE_UNKNOWN_MATH( + "int_list == [1, 2] ? int_list.exists_one(x, x == 1 || unknown_var) : true", + "Condition is not always true.", + "Counterexample input:", + "unknown_var =", + "int_list = [1, 2]"), + DYNAMIC_LIST_EXISTS_ONE_UNKNOWN_POISONING( + "int_list == [1, 2] ? !(int_list.exists_one(x, x == 1 || unknown_var) == true ||" + + " int_list.exists_one(x, x == 1 || unknown_var) == false) : true", + "Condition is not always true.", + "Counterexample input:", + "unknown_var =", + "int_list = [1, 2]"), + DYNAMIC_ITERATION_OVER_SCALAR_RETURNS_UNKNOWN( + "unknown_var == 1 ? !(unknown_var.all(x, false) == true || unknown_var.all(x, false) ==" + + " false) : true", + "Condition is not always true.", + "Counterexample input:", + "unknown_var = 1"), + ERROR_UNKNOWN_PRECEDENCE( + "[1, 2].all(x, x == 1 ? unknown_var : 1/0 == 0) == true", + "Condition is not always true.", + "Counterexample input:", + "unknown_var ="), + LIST_LITERAL_ELEMENT_ERROR_PROPAGATES( + "[true, 1/0].exists(x, x) == true", + "Condition is not always true.", + "(The expression fails unconditionally, regardless of input state)"), + DYNAMIC_STRING_MACRO_TYPE_MISMATCH( + "unknown_var == 1 ? !(unknown_var.contains('a') == true || unknown_var.contains('a') ==" + + " false) : true", + "Condition is not always true.", + "Counterexample input:", + "unknown_var = 1"), + STRING_CONTAINS_IS_NOT_EQUALITY( + "role.contains('admin') ? role == 'admin' : true", + "Condition is not always true.", + "Counterexample input:", + "role ="), + STRING_OVERLAP_FALLACY( + "role.startsWith('A') && role.endsWith('B') ? role == 'AB' : true", + "Condition is not always true.", + "Counterexample input:", + "role ="), + TYPE_CONVERSION_INT_TO_UINT_UNDERFLOW_ERROR( + "uint(-1) == 1u", + "Condition is not always true.", + "(The expression fails unconditionally, regardless of input state)"), + TYPE_CONVERSION_UINT_TO_INT_OVERFLOW_ERROR( + "int(9223372036854775808u) == 1", + "Condition is not always true.", + "(The expression fails unconditionally, regardless of input state)"), + STRING_STARTS_VS_ENDS_WITH( + "role.startsWith('admin') == role.endsWith('admin')", + "Condition is not always true.", + "Counterexample input:", + "role ="), + STRING_CONTAINS_VS_STARTS_WITH( + "role.contains('admin') == role.startsWith('admin')", + "Condition is not always true.", + "Counterexample input:", + "role ="), + DYNAMIC_MAP_INDEX_COMPUTATION_VIOLATION( + "type(dyn_map[1 + 1]) == list && size(dyn_map[1 + 1]) == 0 " + + "? dyn_map[1 + 1] == [] : true", + "Condition is not always true.", + "Counterexample input:", + "dyn_map ="), + DYNAMIC_MAP_COMPREHENSION_NESTED_EQUALITY_VIOLATION( + "cel.bind(r, request, r.l == [[1], [2], [3], [4], [5]] && r.m == {1: [1], 2: [2]," + + " 3: [3]} ? r.l.all(x, r.m.exists(k, r.m[k] == x)) : true)", + "Condition is not always true.", + "Counterexample input:", + "request ="), + UNINTERPRETED_EQUALITY_VIOLATION( + "request == request", + "Condition is not always true.", + "Counterexample input:", + "request = NaN"), + CROSS_TYPE_NUMERIC_EQUALITY_APPROXIMATION_VIOLATION( + "dyn_var == 1.0", + "Condition is not always true.", + "Counterexample input:", + "dyn_var = false"), + DYNAMIC_NOT_TYPE_MISMATCH( + "!dyn_var", "Condition is not always true.", "Counterexample input:", "dyn_var ="), + DYNAMIC_CONDITIONAL_TYPE_MISMATCH( + "dyn_var ? true : false", + "Condition is not always true.", + "Counterexample input:", + "dyn_var ="), + DYNAMIC_NOT_TYPE_MISMATCH_SURVIVOR( + "type(dyn_var) == int ? (!dyn_var == !dyn_var) : true", + "Condition is not always true.", + "Counterexample input:", + "dyn_var ="), + DYNAMIC_CONDITIONAL_TYPE_MISMATCH_SURVIVOR( + "type(dyn_var) == int ? (dyn_var ? true : false) == (dyn_var ? true : false) : true", + "Condition is not always true.", + "Counterexample input:", + "dyn_var ="); + + final String expr; + final ImmutableList expectedFragments; + + IsAlwaysTrueViolationTestCase(String expr, String... expectedFragments) { + this.expr = expr; + this.expectedFragments = ImmutableList.copyOf(expectedFragments); + } + } + + @Test + public void isAlwaysTrue_violation_returnsFalse( + @TestParameter IsAlwaysTrueViolationTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(testCase.expr).getAst(); + + CelVerificationResult result = VERIFIER.isAlwaysTrue(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + for (String fragment : testCase.expectedFragments) { + assertThat(result.message()).contains(fragment); + } + } + + private enum IsInconclusiveTestCase { + UNINTERPRETED_FUNCTION("request.matches('^[a-z]+$')"), + INT_STRING_UNINTERPRETED("int('123') == 123"), + LIST_WITH_APPROXIMATE_ELEMENT("[request.matches('a')]"), + MAP_WITH_APPROXIMATE_KEY("{request.matches('a'): 1}"), + MAP_WITH_APPROXIMATE_VALUE("{1: request.matches('a')}"), + COMPREHENSION_APPROXIMATE_CONDITION( + "size([1, 2, 3].filter(x, request.matches(string(x)))) == 1"), + ABSTRACT_COMPREHENSION_APPROXIMATE_CONDITION( + "int_list.all(x, request.matches(string(x)) ? x > 0 : x < 0) == true"), + ABSTRACT_COMPREHENSION_APPROXIMATE_MAP( + "int_list.map(x, request.matches(string(x))).size() == 0"), + COMPREHENSION_APPROXIMATE_STEP("[1, 2, 3].all(x, request.matches(string(x))) == true"), + STRUCT_FIELD_MISSING_APPROXIMATE_SURVIVOR( + "TestAllTypes{single_bool: request.matches('a')} == TestAllTypes{}"), + COMPREHENSION_LITERAL_LIST_APPROXIMATE_SURVIVOR("[request.matches('a')].all(x, x == true)"), + MAP_INSERT_APPROXIMATE_SURVIVOR("{'a': request.matches('a')} == {'a': true}"), + MAP_COMPREHENSION_APPROXIMATE_KEY("{request.matches('a') ? 1 : 2 : 'val'}.all(x, x == 1)"), + MAP_COMPREHENSION_APPROXIMATE_VALUE( + "{\"key\": request.matches('a') ? 1 : 2}.all(k, v, v == 1)"), + BIND_APPROXIMATE_ACCU("cel.bind(x, request.matches('a') ? 1 : 2, x == 1)"), + BIND_APPROXIMATE_BODY("cel.bind(x, 1, x == 1 && request.matches('a'))"); + + final String expr; + + IsInconclusiveTestCase(String expr) { + this.expr = expr; + } + } + + @Test + public void isAlwaysTrue_inconclusive(@TestParameter IsInconclusiveTestCase testCase) + throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(testCase.expr).getAst(); + + CelVerificationResult result = VERIFIER.isAlwaysTrue(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + } + + private enum EquivalenceInconclusiveTestCase { + MASKED_BY_BMC( + "int_list == [1, 2, 3, 4, 5, 6] ? int_list.all(x, x > 0) : true", + "int_list == [1, 2, 3, 4, 5, 6] ? (int_list.all(x, x > 0) || size(int_list) == 6) : true"), + // TODO: Implement alpha equivalent unknowns to handle this case + TRUNCATION_EQUIVALENT( + "int_list == [1, 2, 3, 4, 5, 6] ? int_list.all(x, x > 0) : true", + "int_list == [1, 2, 3, 4, 5, 6] ? int_list.all(y, y > 0) : true"), + APPROXIMATION_DIVERGENCE("request.matches('a') == true", "request.matches('a') == false"); + + final String exprA; + final String exprB; + + EquivalenceInconclusiveTestCase(String exprA, String exprB) { + this.exprA = exprA; + this.exprB = exprB; + } + } + + @Test + public void verifyEquivalence_inconclusive( + @TestParameter EquivalenceInconclusiveTestCase testCase) throws Exception { + CelAbstractSyntaxTree astA = CEL.compile(testCase.exprA).getAst(); + CelAbstractSyntaxTree astB = CEL.compile(testCase.exprB).getAst(); + + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + } + + private enum EquivalenceTestCase { + DE_MORGANS_LAW("!(a && b)", "!a || !b"), + CONSTANT_FOLDING("x > 5 + 5", "x > 10"), + STRING_COMPARISON("role == \"admin\"", "\"admin\" == role"), + FLATTENED_SELECT( + "test_all_types.single_string == \"admin\"", "\"admin\" == test_all_types.single_string"), + MACRO_EXISTS_EQUIVALENT("[1, 2, 3].exists(x, x > 0)", "[1, 2, 3].exists(y, y > 0)"), + IN_LIST_EQUIVALENT("2 in [1, 2, 3]", "2 in [1, 2, 3] || false"), + MACRO_ALL_EQUIVALENT("[1, 2, 3].all(x, x > 0)", "1 > 0 && 2 > 0 && 3 > 0"), + MACRO_EXISTS_ONE_EQUIVALENT( + "[1, 2, 3].exists_one(x, x == 2)", + "(1 == 2 ? 1 : 0) + (2 == 2 ? 1 : 0) + (3 == 2 ? 1 : 0) == 1"), + MACRO_MAP_EQUIVALENT("{1: true, 2: true, 3: true}.all(k, k > 0)", "1 > 0 && 2 > 0 && 3 > 0"), + MACRO_BIND_EQUIVALENT("cel.bind(x, 10, x > 0)", "10 > 0"), + NESTED_MACRO( + "[1, 2].all(x, [3, 4].exists(y, x < y))", "[1, 2].all(a, [3, 4].exists(b, a < b))"), + NESTED_MACRO_SHADOWING( + "[1, 2].all(x, [3, 4].exists(x, x > 0))", "[1, 2].all(y, [3, 4].exists(z, z > 0))"), + CONSTANTS_UINT("u > 5u + 5u", "u > 10u"), + CONSTANTS_DOUBLE("d > 5.5 + 5.5", "d > 11.0"), + IEEE_754_ZERO_EQUALITY("0.0 == -0.0", "true"), + IEEE_754_NAN_INEQUALITY("0.0/0.0 == 0.0/0.0", "false"), + IEEE_754_INFINITY_EQUALITY("1.0/0.0 == 1.0/0.0", "true"), + IEEE_754_INFINITY_INEQUALITY("-1.0/0.0 == 1.0/0.0", "false"), + IEEE_754_OVERFLOW_INFINITY("1e300 * 1e300 == 1.0/0.0", "true"), + CROSS_TYPE_NUMERIC_EQUALITY_INT_DOUBLE("request == 1.0", "request == 1"), + CROSS_TYPE_NUMERIC_EQUALITY_UINT_DOUBLE("request == 1u", "request == 1.0"), + CROSS_TYPE_NUMERIC_EQUALITY_INT_UINT("request == 1", "request == 1u"), + STATIC_DOUBLE_EQUALITY("d + 1.0 == d + 1.0", "d == d"), + MAP_FIELD_SELECT("string_int_map.my_field > 0", "string_int_map['my_field'] > 0"), + HETEROGENEOUS_LIST_SIZES_SAFE_FALSE("!([1, 2] == [1, 2, 3])", "true"), + HETEROGENEOUS_LIST_SIZES_SAFE_FALSE_REVERSE("!([1, 2, 3] == [1, 2])", "true"), + TRANSITIVE_AST_BINDING_LIST_UNROLLING("cel.bind(x, [1], cel.bind(y, x, y == [1]))", "true"), + DYNAMIC_VS_STRING_SAFE_BYPASS("request == 'admin' || request != 'admin'", "true"), + DYNAMIC_MAP_VS_HETEROGENEOUS_LITERAL( + "request == {'a': 1.0, 'b': 'string'} || request != {'a': 1.0, 'b': 'string'}", "true"), + DEEP_TRANSITIVE_BIND_NUMERIC("cel.bind(x, 1, cel.bind(y, x, cel.bind(z, y, z))) == 1", "true"), + DEEP_TRANSITIVE_BIND_COLLECTION("cel.bind(x, [1], cel.bind(y, x, y == [1]))", "true"), + MACRO_ITER_VAR_SHADOWING_SAFETY("cel.bind(i, 1.0, [1, 2, 3].all(i, i > 0))", "true"), + MACRO_STRUCTURAL_EQUIVALENCE_PRESERVED( + "request.auth.claims.groups.all(g, g == 'admin')", + "request.auth.claims.groups.all(x, x == 'admin')"), + + CONSTANTS_BYTES("by == b\"abc\"", "b\"abc\" == by"), + STRING_CONCATENATION("\"abc\" + \"def\"", "\"abcdef\""), + COMPLEX_MESSAGE( + "test_all_types.single_int32 == 10 && test_all_types.single_int64 > 5", + "10 == test_all_types.single_int32 && 5 < test_all_types.single_int64"), + OPTIONAL("optional.of(x).hasValue()", "optional.of(x).hasValue() && true"), + OPTIONAL_OR_VALUE_EQUIVALENCE("optional.of(x).orValue(y)", "x"), + OPTIONAL_OR_VALUE_FALLBACK_EQUIVALENCE("optional.none().orValue(y)", "y"), + OPTIONAL_OR_EQUIVALENCE("optional.of(x).or(optional.of(y))", "optional.of(x)"), + OPTIONAL_OR_FALLBACK_EQUIVALENCE("optional.none().or(optional.of(y))", "optional.of(y)"), + OPTIONAL_VALUE_EQUIVALENCE("optional.of(x).value()", "x"), + OPTIONAL_HAS_VALUE_EQUIVALENCE("optional.of(x).hasValue()", "true"), + OPTIONAL_NONE_HAS_VALUE_EQUIVALENCE("optional.none().hasValue()", "false"), + FUNCTIONS("size(\"abc\") == size(role)", "size(role) == size(\"abc\")"), + NOT_EQUALS("x != y", "!(x == y)"), + LESS("x < y", "y > x"), + LESS_EQUALS("x <= y", "y >= x"), + GREATER("x > y", "y < x"), + GREATER_EQUALS("x >= y", "y <= x"), + ADD("x + y", "y + x"), + SUBTRACT("x - y == 5", "5 == x - y"), + NESTED_LIST_VARIABLE_EQUALITY( + "[int_list] == [int_list_2] ? int_list == int_list_2 : true", "true"), + NESTED_SYMBOLIC_LIST_EXTENSIONALITY( + "size(nested_list) >= 2 && nested_list[0] == nested_list[1] ? [nested_list[0]] ==" + + " [nested_list[1]] : true", + "true"), + NESTED_DYNAMIC_MAP_LOOKUP_EXTENSIONALITY( + "has(dyn_map.a) && has(dyn_map.a.b) && has(dyn_map.c) && has(dyn_map.c.d) && dyn_map.a.b ==" + + " dyn_map.c.d ? [dyn_map.a.b] == [dyn_map.c.d] : true", + "true"), + MULTIPLY("x * y", "y * x"), + DIVIDE("x / y == 5", "5 == x / y"), + MODULO("x % y == 5", "5 == x % y"), + NEGATE("-x == 5", "5 == -x"), + NEGATE_DOUBLE("-d == 5.0", "5.0 == -d"), + PRESENCE_TEST("has(test_all_types.single_int32)", "has(test_all_types.single_int32) && true"), + PRESENCE_TEST_MAP("has(request.headers)", "has(request.headers) && true"), + PRESENCE_TEST_LIST( + "has(test_all_types.repeated_int32)", "has(test_all_types.repeated_int32) && true"), + INVARIANT_LIST_CONTAINS("x in [x, y]", "[x, y][0] == x || [x, y][1] == x"), + PRESENCE_TEST_TERNARY( + "has(test_all_types.single_int32) ? test_all_types.single_int32 : 0", + "has(test_all_types.single_int32) ? test_all_types.single_int32 : 0 * 1"), + STRUCT_EQUALITY( + "TestAllTypes{single_int32: 1, single_string: \"abc\"} == TestAllTypes{single_string:" + + " \"abc\", single_int32: 1}", + "true"), + STRUCT_DEEPLY_NESTED_CONTAINER_EQUALITY( + "TestAllTypes{ single_struct: {'nested_struct': {'map': [1, 2]}}} == TestAllTypes{ " + + " single_struct: {'nested_struct': {'map': [1, 2]}}}", + "true"), + STRUCT_EMPTY_MESSAGE_EQUALITY( + "TestAllTypes{standalone_message: TestAllTypes.NestedMessage{bb: 1}} ==" + + " TestAllTypes{standalone_message: TestAllTypes.NestedMessage{bb: 1}}", + "true"), + STRUCT_INEQUALITY("TestAllTypes{single_int32: 1} != TestAllTypes{single_int32: 2}", "true"), + STRUCT_FIELD_DEFAULT_PRIMITIVE_INT( + "test_all_types.single_int32", + "has(test_all_types.single_int32) ? test_all_types.single_int32 : 0"), + STRUCT_FIELD_DEFAULT_PRIMITIVE_STRING( + "test_all_types.single_string", + "has(test_all_types.single_string) ? test_all_types.single_string : \"\""), + STRUCT_FIELD_DEFAULT_PRIMITIVE_BOOL( + "test_all_types.single_bool", + "has(test_all_types.single_bool) ? test_all_types.single_bool : false"), + STRUCT_FIELD_DEFAULT_PRIMITIVE_BYTES( + "test_all_types.single_bytes", + "has(test_all_types.single_bytes) ? test_all_types.single_bytes : b\"\""), + STRUCT_FIELD_DEFAULT_PRIMITIVE_DOUBLE( + "test_all_types.single_double", + "has(test_all_types.single_double) ? test_all_types.single_double : 0.0"), + STRUCT_FIELD_DEFAULT_PRIMITIVE_UINT( + "test_all_types.single_uint32", + "has(test_all_types.single_uint32) ? test_all_types.single_uint32 : 0u"), + STRUCT_FIELD_DEFAULT_LIST( + "test_all_types.repeated_int32", + "has(test_all_types.repeated_int32) ? test_all_types.repeated_int32 : []"), + STRUCT_FIELD_DEFAULT_MAP( + "test_all_types.map_int32_int32", + "has(test_all_types.map_int32_int32) ? test_all_types.map_int32_int32 : {}"), + STRUCT_FIELD_DEFAULT_NESTED_PRIMITIVE("TestAllTypes{}.standalone_message.bb", "0"), + MAP_LITERALS("{'a': 1, 'b': 2} == {'b': 2, 'a': 1}", "true"), + MAP_INDEX_AND_IN("{'a': 1}['a'] == 1 && 'a' in {'a': 1} && !('b' in {'a': 1})", "true"), + MACRO_MAP_VARIABLE_EQUIVALENCE( + "{x: y, a: b}.all(i, i == x || i == a)", "{x: y, a: b}.all(j, j == x || j == a)"), + STRUCT_PROTO3_DEFAULT_EQUALITY("TestAllTypes{single_int32: 0} == TestAllTypes{}", "true"), + STRUCT_DEFAULT_FALLBACK_EQUALITY( + "TestAllTypes{}.standalone_message == TestAllTypes.NestedMessage{}", "true"), + DYNAMIC_OPERATOR_OVERLOAD("dyn(1) < 2 || dyn(1) >= 2", "dyn(1) < 2 || dyn(1) >= 2"), + DYNAMIC_MAP_SELECT("dyn({'a': 1}).a == 1", "dyn({'a': 1}).a == 1"), + DYNAMIC_MESSAGE_SELECT( + "dyn(TestAllTypes{single_int32: 1}).single_int32 == 1", + "dyn(TestAllTypes{single_int32: 1}).single_int32 == 1"), + DYNAMIC_SELECT_TEST_ONLY( + "has(dyn({'a': 1}).a) && has(dyn(TestAllTypes{single_int32: 1}).single_int32)", + "has(dyn({'a': 1}).a) && has(dyn(TestAllTypes{single_int32: 1}).single_int32)"), + DYNAMIC_INDEXING_TYPE_MISMATCH( + "type(request) == type(1) && request[1] == 1 && request[2] == 2", + "type(request) == type(1) && 1 / 0 == 1 && request[2] == 2"); + + private final String exprA; + private final String exprB; + + EquivalenceTestCase(String exprA, String exprB) { + this.exprA = exprA; + this.exprB = exprB; + } + } + + @Test + public void verifyEquivalence_success(@TestParameter EquivalenceTestCase testCase) + throws Exception { + CelAbstractSyntaxTree astA = CEL.compile(testCase.exprA).getAst(); + CelAbstractSyntaxTree astB = CEL.compile(testCase.exprB).getAst(); + + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + + assertWithMessage(result.message()) + .that(result.status()) + .isEqualTo(VerificationStatus.VERIFIED); + } + + private enum EquivalenceViolationTestCase { + INT("x > 10", "x > 5"), + UINT("u > 10u", "u > 5u"), + DOUBLE("d > 10.0", "d > 5.0"), + STRING("role == \"admin\"", "role == \"user\""), + BYTES("by == b\"abc\"", "by == b\"def\""), + STRING_CONCATENATION_NOT_COMMUTATIVE("\"def\" + \"abc\"", "\"abcdef\""), + PRESENCE("has(test_all_types.single_int32)", "test_all_types.single_int32 == 1"), + MAP_INDEX_MISSING("{'a': 1}['b'] == 1", "true"), + MESSAGE_TYPE_MISMATCH( + "dyn(TestAllTypes{single_int32: 1}) == dyn(TestAllTypes.NestedMessage{bb: 1})", "true"), + HETEROGENEOUS_FIELD_SELECTION( + "test_all_types.single_int32 == 10", "test_all_types.single_int64 == 10"), + STRUCT_VARIABLE_NOT_EQUIVALENT_TO_DEFAULT("test_all_types == TestAllTypes{}", "true"), + CROSS_TYPE_NUMERIC_INEQUALITY_INT_DOUBLE("request == 1.0", "request == 2.0 || request == 1"), + CROSS_TYPE_SYMBOLIC_INEQUALITY_INT_UINT("dyn(x) == dyn(u)", "false"), + CROSS_TYPE_SYMBOLIC_INEQUALITY_UINT_INT("dyn(u) == dyn(x)", "false"), + OPTIONAL_OR_VALUE_VIOLATION("optional.of(x).orValue(y)", "y"), + OPTIONAL_VALUE_VIOLATION("optional.of(x).value()", "y"), + CROSS_NUMERIC_EQUALITY_INT_DYN_VIOLATION("1 == request", "false"); + + final String exprA; + final String exprB; + + EquivalenceViolationTestCase(String exprA, String exprB) { + this.exprA = exprA; + this.exprB = exprB; + } + } + + @Test + public void verifyEquivalence_violation_returnsFalse( + @TestParameter EquivalenceViolationTestCase testCase) throws Exception { + CelAbstractSyntaxTree astA = CEL.compile(testCase.exprA).getAst(); + CelAbstractSyntaxTree astB = CEL.compile(testCase.exprB).getAst(); + + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()).contains("Equivalence violation detected."); + } + + @Test + public void verifyEquivalence_violation_hasCounterexampleMessage() throws Exception { + CelAbstractSyntaxTree astA = CEL.compile("x > y").getAst(); + CelAbstractSyntaxTree astB = CEL.compile("x > 5").getAst(); + + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + String message = result.message(); + assertThat(message).contains("Equivalence violation detected. Counterexample input:"); + assertThat(message).containsMatch(" x = -?\\d+"); + assertThat(message).containsMatch(" y = -?\\d+"); + } + + @Test + public void verifyEquivalence_divergesOnTernaryErrorSemantics() throws Exception { + // astA: Always True + // astB: Evaluates to Error + CelAbstractSyntaxTree astA = CEL.compile("true").getAst(); + CelAbstractSyntaxTree astB = CEL.compile("(1 / 0 == 1) ? true : true").getAst(); + + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + } + + + @Test + public void verifyEquivalence_dynamicIndexingWithExplicitMap_hydratesMap() throws Exception { + CelAbstractSyntaxTree astA = + CEL.compile("string_int_list_map == {\"a\": [1, 2], \"b\": [3, 4]}").getAst(); + CelAbstractSyntaxTree astB = + CEL.compile("string_int_list_map == {\"a\": [1, 2], \"b\": [3, 5]}").getAst(); + + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()).contains("string_int_list_map = {"); + assertThat(result.message()).contains("\"a\": [1, 2]"); + assertThat(result.message()).containsMatch("\"b\": \\[3, [45]\\]"); + } + + @Test + public void verifyEquivalence_logicalAndShortCircuitError_isVerified() throws Exception { + CelAbstractSyntaxTree astA = CEL.compile("false && (1 / 0 == 0)").getAst(); + CelAbstractSyntaxTree astB = CEL.compile("false").getAst(); + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void verifyEquivalence_logicalOrShortCircuitUnknown_isVerified() throws Exception { + CelAbstractSyntaxTree astA = CEL.compile("true || unknown_var").getAst(); + CelAbstractSyntaxTree astB = CEL.compile("true").getAst(); + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void verifyEquivalence_deepListExtensionality_isVerified() throws Exception { + CelAbstractSyntaxTree astA = CEL.compile("[[[[1]]]] == [[[[1]]]]").getAst(); + CelAbstractSyntaxTree astB = CEL.compile("true").getAst(); + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void verifyEquivalence_stringConcatenationExplosion_isVerified() throws Exception { + CelAbstractSyntaxTree astA = + CEL.compile("(\"a\" + \"b\" + \"c\" + \"d\" + \"e\") == \"abcde\"").getAst(); + CelAbstractSyntaxTree astB = CEL.compile("true").getAst(); + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void verifyEquivalence_divergesOnShortCircuitingErrors() throws Exception { + // astA: Short-circuits to True (Error is ignored) + // astB: A naive restructuring that might trigger an Error depending on evaluation order. + CelAbstractSyntaxTree astA = CEL.compile("true || (role == \"admin\" && 1 / 0 == 1)").getAst(); + CelAbstractSyntaxTree astB = CEL.compile("(role == \"admin\" && 1 / 0 == 1) || true").getAst(); + + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void verifyEquivalence_distinctErrorEquivalence_failsVerification() throws Exception { + // If error-collapsing is present, this will falsely pass because both sides yield CelError. + // It MUST fail verification. + CelAbstractSyntaxTree astA = CEL.compile("5u + 5u == 10u").getAst(); + CelAbstractSyntaxTree astB = CEL.compile("role + role == \"abc\"").getAst(); + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + } + + private enum CounterexampleFormatTestCase { + STRING( + "role == \"admin\"", + "role == \"user\"", + ImmutableList.of("role = \"admin\"", "role = \"user\"")), + DOUBLE("d == 1.5", "d == 2.5", ImmutableList.of("d = 1\\.5", "d = 2\\.5")), + DOUBLE_NAN("d != d", "false", ImmutableList.of("d = NaN")), + DOUBLE_INF("d == (1.0 / 0.0)", "false", ImmutableList.of("d = Infinity")), + DOUBLE_NEG_INF("d == (-1.0 / 0.0)", "false", ImmutableList.of("d = -Infinity")), + DOUBLE_NEG_ZERO("d == 0.0 && 1.0 / d == (-1.0 / 0.0)", "false", ImmutableList.of("d = -0\\.0")), + DOUBLE_POS_ZERO("d == 0.0 && 1.0 / d == (1.0 / 0.0)", "false", ImmutableList.of("d = 0\\.0")), + UINT("u == 443u", "u == 80u", ImmutableList.of("u = 443u", "u = 80u")), + BOOL("a == true", "a == false", ImmutableList.of("a = true", "a = false")), + EMPTY_MAP( + "string_int_map == {}", + "string_int_map == {\"a\": 1}", + ImmutableList.of("string_int_map = \\{\\}", "string_int_map = \\{\"a\": 1\\}")), + LIST( + "int_list == [1, 2]", + "int_list == [3, 4]", + ImmutableList.of("int_list = \\[1, 2\\]", "int_list = \\[3, 4\\]")), + MAP( + "string_int_map == {'a': 1}", + "string_int_map == {'b': 2}", + ImmutableList.of("string_int_map = \\{\"a\": 1\\}", "string_int_map = \\{\"b\": 2\\}")), + BYTES( + "bytes_val == b'foo'", + "bytes_val == b'bar'", + ImmutableList.of("bytes_val = b\"foo\"", "bytes_val = b\"bar\"")), + NESTED_COLLECTION( + "string_int_list_map == {\"a\": [1, 2]}", + "string_int_list_map == {\"a\": [3, 4]}", + ImmutableList.of( + "string_int_list_map = \\{\"a\": \\[1, 2\\]\\}", + "string_int_list_map = \\{\"a\": \\[3, 4\\]\\}")), + LIST_UNCONSTRAINED( + "int_list == int_list", "false", ImmutableList.of("int_list = \\[-?\\d*\\]")), + MAP_UNCONSTRAINED( + "string_int_map == string_int_map", "false", ImmutableList.of("string_int_map = \\{\\}")), + STRUCT_FIELD_MISSING_INT_NOT_EQUAL_TO_NONDEFAULT( + "test_all_types.single_int32", + "has(test_all_types.single_int32) ? test_all_types.single_int32 : 1", + ImmutableList.of( + "test_all_types = cel\\.expr\\.conformance\\.proto3\\.TestAllTypes\\{\\}")), + STRUCT_FIELD_MISSING_STRING_NOT_EQUAL_TO_NONDEFAULT( + "test_all_types.single_string", + "has(test_all_types.single_string) ? test_all_types.single_string : \"foo\"", + ImmutableList.of( + "test_all_types = cel\\.expr\\.conformance\\.proto3\\.TestAllTypes\\{\\}")), + STRUCT_FIELD_MISSING_BOOL_NOT_EQUAL_TO_NONDEFAULT( + "test_all_types.single_bool", + "has(test_all_types.single_bool) ? test_all_types.single_bool : true", + ImmutableList.of( + "test_all_types = cel\\.expr\\.conformance\\.proto3\\.TestAllTypes\\{\\}")), + STRUCT_FIELD_MISSING_UINT_NOT_EQUAL_TO_NONDEFAULT( + "test_all_types.single_uint32", + "has(test_all_types.single_uint32) ? test_all_types.single_uint32 : 1u", + ImmutableList.of( + "test_all_types = cel\\.expr\\.conformance\\.proto3\\.TestAllTypes\\{\\}")), + STRUCT_FIELD_MISSING_DOUBLE_NOT_EQUAL_TO_NONDEFAULT( + "test_all_types.single_double", + "has(test_all_types.single_double) ? test_all_types.single_double : 1.0", + ImmutableList.of( + "test_all_types = cel\\.expr\\.conformance\\.proto3\\.TestAllTypes\\{\\}")), + STRUCT_FIELD_MISSING_BYTES_NOT_EQUAL_TO_NONDEFAULT( + "test_all_types.single_bytes", + "has(test_all_types.single_bytes) ? test_all_types.single_bytes : b\"foo\"", + ImmutableList.of( + "test_all_types = cel\\.expr\\.conformance\\.proto3\\.TestAllTypes\\{\\}")), + STRUCT_FIELD_MISSING_LIST_NOT_EQUAL_TO_NONDEFAULT( + "test_all_types.repeated_int32", + "has(test_all_types.repeated_int32) ? test_all_types.repeated_int32 : [1]", + ImmutableList.of( + "test_all_types = cel\\.expr\\.conformance\\.proto3\\.TestAllTypes\\{\\}")), + STRUCT_FIELD_MISSING_MAP_NOT_EQUAL_TO_NONDEFAULT( + "test_all_types.map_int32_int32", + "has(test_all_types.map_int32_int32) ? test_all_types.map_int32_int32 : {1: 2}", + ImmutableList.of( + "test_all_types = cel\\.expr\\.conformance\\.proto3\\.TestAllTypes\\{\\}")), + STRUCT_FIELD_MISSING_MESSAGE_NOT_EQUAL_TO_NONDEFAULT( + "test_all_types.standalone_message", + "has(test_all_types.standalone_message) ? test_all_types.standalone_message :" + + " TestAllTypes.NestedMessage{bb: 1}", + ImmutableList.of( + "test_all_types = cel\\.expr\\.conformance\\.proto3\\.TestAllTypes\\{.*\\}")); + + final String exprA; + final String exprB; + final ImmutableList expectedFragments; + + CounterexampleFormatTestCase( + String exprA, String exprB, ImmutableList expectedFragments) { + this.exprA = exprA; + this.exprB = exprB; + this.expectedFragments = expectedFragments; + } + } + + @Test + public void verifyEquivalence_counterexampleFormat( + @TestParameter CounterexampleFormatTestCase testCase) throws Exception { + CelAbstractSyntaxTree astA = CEL.compile(testCase.exprA).getAst(); + CelAbstractSyntaxTree astB = CEL.compile(testCase.exprB).getAst(); + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + boolean matched = + testCase.expectedFragments.stream() + .anyMatch( + f -> + result + .message() + .matches( + "(?s).*Equivalence violation detected\\. Counterexample input:\n " + + f + + ".*")); + assertWithMessage(result.message()).that(matched).isTrue(); + } + + @Test + public void verifyEquivalence_structCounterexampleFormat() throws Exception { + CelAbstractSyntaxTree astA = + CEL.compile("test_all_types == TestAllTypes{single_int32: 1}").getAst(); + CelAbstractSyntaxTree astB = + CEL.compile("test_all_types == TestAllTypes{single_int32: 2}").getAst(); + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()) + .isAnyOf( + "Equivalence violation detected. Counterexample input:\n" + + " test_all_types = cel.expr.conformance.proto3.TestAllTypes{single_int32: 1}", + "Equivalence violation detected. Counterexample input:\n" + + " test_all_types = cel.expr.conformance.proto3.TestAllTypes{single_int32: 2}"); + } + + @Test + public void isAlwaysTrue_operationError_counterexampleFormat() throws Exception { + // x / x == x / x looks like a tautology, but if x = 0, 0 / 0 throws an Error. + // Error == Error evaluates to Error, which is not true. + // Thus, x = 0 is the only valid counterexample. + CelAbstractSyntaxTree ast = CEL.compile("x / x == x / x").getAst(); + CelVerificationResult result = VERIFIER.isAlwaysTrue(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()).contains("x = 0"); + } + + @Test + public void isAlwaysTrue_dynamicFunction_nan_violated() throws Exception { + // request() == request() is a tautology only if request() does not evaluate to NaN. + // Since request() returns DYN, Z3 can assign NaN to its return value, and NaN == NaN is false. + CelCompiler celCompiler = + CEL.toCompilerBuilder() + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "request", + CelOverloadDecl.newGlobalOverload("request_overload", SimpleType.DYN))) + .build(); + CelAbstractSyntaxTree ast = celCompiler.compile("request() == request()").getAst(); + CelVerificationResult result = VERIFIER.isAlwaysTrue(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()).contains("request = NaN"); + } + + @Test + public void isAlwaysTrue_boolFunction_verified() throws Exception { + CelCompiler celCompiler = + CEL.toCompilerBuilder() + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "bool_func", + CelOverloadDecl.newGlobalOverload("bool_func_overload", SimpleType.BOOL))) + .build(); + CelAbstractSyntaxTree ast = celCompiler.compile("bool_func() == bool_func()").getAst(); + CelVerificationResult result = VERIFIER.isAlwaysTrue(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void verifyEquivalence_functionError_equivalent() throws Exception { + // request() and request() are equivalent, even if request() throws an Error, + // because Error == Error structurally in Z3 equivalence. + CelCompiler celCompiler = + CEL.toCompilerBuilder() + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "request", + CelOverloadDecl.newGlobalOverload("request_overload", SimpleType.DYN))) + .build(); + CelAbstractSyntaxTree astA = celCompiler.compile("request()").getAst(); + CelAbstractSyntaxTree astB = celCompiler.compile("request()").getAst(); + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + @SuppressWarnings("GoodTime-ApiWithNumericTimeUnit") // Test only + public void setTimeout_invalidDuration_throws(@TestParameter({"0", "-1"}) long timeoutSeconds) { + CelVerifierBuilder builder = CelVerifierFactory.newVerifier(); + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> builder.setTimeout(Duration.ofSeconds(timeoutSeconds))); + assertThat(exception).hasMessageThat().contains("Timeout must be strictly positive"); + } + + @Test + public void isSatisfiable_divisionByZero_failsInCelWithErrors() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("1 / 0 == 5").getAst(); + + CelVerificationResult result = VERIFIER.isSatisfiable(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + } + + @Test + public void isSatisfiable_timeoutReached_throwsCelVerificationException() throws Exception { + CelVerifier timeoutVerifier = + CelVerifierFactory.newVerifier().setTimeout(Duration.ofMillis(1)).build(); + + Cel customCel = + CelFactory.plannerCelBuilder() + .addVar("d1", SimpleType.DOUBLE) + .addVar("d2", SimpleType.DOUBLE) + .addVar("d3", SimpleType.DOUBLE) + .addVar("d4", SimpleType.DOUBLE) + .build(); + + // An overly complex double multiplication to guarantee Z3 FPA theory solver timeouts. + CelAbstractSyntaxTree ast = + customCel + .compile( + "d1 * d2 * d3 * d4 * d1 * d2 * d3 * d4 == 9429185123491285.0 && d1 > 100000.0 &&" + + " d2 > 100000.0 && d3 > 100000.0 && d4 > 100000.0") + .getAst(); + + CelVerificationException e = + assertThrows(CelVerificationException.class, () -> timeoutVerifier.isSatisfiable(ast)); + assertThat(e).hasMessageThat().containsMatch("timeout|canceled"); + } + + @Test + public void addFunctionAxioms_iterable_addsToAxioms() throws Exception { + CelFunctionDecl dummyFunctionDecl = + CelFunctionDecl.newFunctionDeclaration( + "dummy", CelOverloadDecl.newGlobalOverload("dummy_overload", SimpleType.INT)); + CelCompiler celCompiler = + CEL.toCompilerBuilder() + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .addFunctionDeclarations(dummyFunctionDecl) + .build(); + CelZ3FunctionAxiom dummyAxiom = + CelZ3FunctionAxiom.newBuilder(dummyFunctionDecl) + .addOverloadTranslator( + "dummy_overload", + (ctx, typeSystem, constraintSink, unwrappedArgs, argApproximations) -> + Optional.of( + CelZ3OverloadResult.create( + typeSystem.wrap(typeSystem.intCons(), ctx.mkInt(42)), ctx.mkFalse()))) + .build(); + CelVerifier verifier = + CelVerifierZ3Impl.newBuilder().addFunctionAxioms(ImmutableList.of(dummyAxiom)).build(); + CelAbstractSyntaxTree ast = celCompiler.compile("dummy() == 42").getAst(); + CelVerificationResult result = verifier.isAlwaysTrue(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void isAlwaysTrue_functionAxiomWithErrorArgument_bubblesUpError() throws Exception { + CelFunctionDecl dummyFunctionDecl = + CelFunctionDecl.newFunctionDeclaration( + "dummy", + CelOverloadDecl.newGlobalOverload("dummy_overload", SimpleType.DYN, SimpleType.DYN)); + CelZ3FunctionAxiom dummyAxiom = + CelZ3FunctionAxiom.newBuilder(dummyFunctionDecl) + .addUnaryOverloadTranslator( + "dummy_overload", + (ctx, typeSystem, constraintSink, arg) -> Optional.of(typeSystem.mkInt(42))) + .build(); + CelVerifier verifier = + CelVerifierZ3Impl.newBuilder().addFunctionAxioms(ImmutableList.of(dummyAxiom)).build(); + CelCompiler celCompiler = + CEL.toCompilerBuilder() + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .addFunctionDeclarations(dummyFunctionDecl) + .build(); + + // dummy(x / x) == 42. If x == 0, x / x is Error. + // If the axiom bubbling works, dummy(Error) evaluates to Error, so the whole expression is + // Error (not always true). + // If the axiom bubbling is missing, dummy(Error) evaluates to 42, so 42 == 42 is always true. + CelAbstractSyntaxTree ast = celCompiler.compile("dummy(x / x) == 42").getAst(); + CelVerificationResult result = verifier.isAlwaysTrue(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()).contains("x = 0"); + } + + @Test + public void addFunctionAxioms_axiomHandlesNothing_fallsBackToUninterpretedFunction() + throws Exception { + CelFunctionDecl dummyAddDecl = + CelFunctionDecl.newFunctionDeclaration( + "dummy_add", + CelOverloadDecl.newGlobalOverload( + "dummy_add_overload", SimpleType.INT, SimpleType.INT, SimpleType.INT)); + CelZ3FunctionAxiom dummyAxiom = + CelZ3FunctionAxiom.newBuilder(dummyAddDecl) + .addBinaryOverloadTranslator( + "dummy_add_overload", + (ctx, typeSystem, constraintSink, arg1, arg2) -> Optional.empty()) + .build(); + CelVerifier verifier = + CelVerifierZ3Impl.newBuilder().addFunctionAxioms(ImmutableList.of(dummyAxiom)).build(); + Cel cel = CelFactory.plannerCelBuilder().addFunctionDeclarations(dummyAddDecl).build(); + + CelAbstractSyntaxTree ast = cel.compile("dummy_add(1, 1) == 2").getAst(); + CelVerificationResult result = verifier.isAlwaysTrue(ast); + + // Falls back to an uninterpreted function since it was not translated + // (thus comes back as inconclusive due to being an approximation) + assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + } + + private enum TernaryApproxTestCase { + APPROX_FIRST("dummy_ternary(uninterpreted_int(), 0, 0) == 99"), + APPROX_SECOND("dummy_ternary(0, uninterpreted_int(), 0) == 99"), + APPROX_THIRD("dummy_ternary(0, 0, uninterpreted_int()) == 99"); + + final String expr; + + TernaryApproxTestCase(String expr) { + this.expr = expr; + } + } + + @Test + public void addFunctionAxioms_ternaryOverload_approximation_inconclusive( + @TestParameter TernaryApproxTestCase testCase) throws Exception { + CelFunctionDecl dummyDecl = + CelFunctionDecl.newFunctionDeclaration( + "dummy_ternary", + CelOverloadDecl.newGlobalOverload( + "dummy_ternary_overload", + SimpleType.INT, + SimpleType.INT, + SimpleType.INT, + SimpleType.INT)); + CelZ3FunctionAxiom dummyAxiom = + CelZ3FunctionAxiom.newBuilder(dummyDecl) + .addTernaryOverloadTranslator( + "dummy_ternary_overload", + (ctx, ts, sink, arg1, arg2, arg3) -> + Optional.of( + ts.wrapInt( + (IntExpr) + ctx.mkAdd(ts.getInt(arg1), ts.getInt(arg2), ts.getInt(arg3))))) + .build(); + CelVerifier verifier = + CelVerifierZ3Impl.newBuilder().addFunctionAxioms(ImmutableList.of(dummyAxiom)).build(); + + CelFunctionDecl uninterpretedIntDecl = + CelFunctionDecl.newFunctionDeclaration( + "uninterpreted_int", + CelOverloadDecl.newGlobalOverload("uninterpreted_int_overload", SimpleType.INT)); + Cel cel = + CelFactory.plannerCelBuilder() + .addFunctionDeclarations(dummyDecl) + .addFunctionDeclarations(uninterpretedIntDecl) + .build(); + + CelAbstractSyntaxTree ast = cel.compile(testCase.expr).getAst(); + CelVerificationResult result = verifier.isAlwaysTrue(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + } + + @Test + public void addFunctionAxioms_heterogeneousArguments_checksAllTypes() throws Exception { + // Tests that the typeGuard is applied correctly to all arguments (including index 0). + CelFunctionDecl dummyDecl = + CelFunctionDecl.newFunctionDeclaration( + "dummy_func", + CelOverloadDecl.newGlobalOverload( + "dummy_func_list", + SimpleType.INT, + ListType.create(SimpleType.DYN), + ListType.create(SimpleType.DYN)), + CelOverloadDecl.newGlobalOverload( + "dummy_func_int", SimpleType.INT, SimpleType.INT, ListType.create(SimpleType.DYN))); + CelZ3FunctionAxiom dummyAxiom = + CelZ3FunctionAxiom.newBuilder(dummyDecl) + .addBinaryOverloadTranslator( + "dummy_func_list", + (ctx, typeSystem, constraintSink, arg1, arg2) -> + Optional.of(typeSystem.wrap(typeSystem.intCons(), ctx.mkInt(1)))) + .addBinaryOverloadTranslator( + "dummy_func_int", + (ctx, typeSystem, constraintSink, arg1, arg2) -> + Optional.of(typeSystem.wrap(typeSystem.intCons(), ctx.mkInt(2)))) + .build(); + CelVerifier verifier = + CelVerifierZ3Impl.newBuilder().addFunctionAxioms(ImmutableList.of(dummyAxiom)).build(); + Cel cel = + CelFactory.plannerCelBuilder() + .addVar("int_var", SimpleType.INT) + .addFunctionDeclarations(dummyDecl) + .build(); + // The call dummy_func(int_var, [1]) should match dummy_func_int, returning 2. + // If index 0 type check is skipped, it will match dummy_func_list, returning 1. + CelAbstractSyntaxTree ast = + cel.compile("int_var == 1 ? dummy_func(int_var, [1]) == 2 : true").getAst(); + + CelVerificationResult result = verifier.isAlwaysTrue(ast); + + assertWithMessage(result.message()) + .that(result.status()) + .isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void addFunctionAxioms_duplicateFunctionNames_throwsException() throws Exception { + CelFunctionDecl dummyDecl1 = + CelFunctionDecl.newFunctionDeclaration( + "dummy_func", + CelOverloadDecl.newGlobalOverload("dummy_func_int", SimpleType.INT, SimpleType.INT)); + CelFunctionDecl dummyDecl2 = + CelFunctionDecl.newFunctionDeclaration( + "dummy_func", + CelOverloadDecl.newGlobalOverload( + "dummy_func_list", SimpleType.INT, ListType.create(SimpleType.DYN))); + + CelZ3FunctionAxiom dummyAxiom1 = + CelZ3FunctionAxiom.newBuilder(dummyDecl1) + .addUnaryOverloadTranslator( + "dummy_func_int", + (ctx, typeSystem, constraintSink, arg) -> + Optional.of(typeSystem.wrap(typeSystem.intCons(), ctx.mkInt(1)))) + .build(); + CelZ3FunctionAxiom dummyAxiom2 = + CelZ3FunctionAxiom.newBuilder(dummyDecl2) + .addUnaryOverloadTranslator( + "dummy_func_list", + (ctx, typeSystem, constraintSink, arg) -> + Optional.of(typeSystem.wrap(typeSystem.intCons(), ctx.mkInt(2)))) + .build(); + + CelVerifierZ3Impl.Builder builder = + CelVerifierZ3Impl.newBuilder() + .addFunctionAxioms(ImmutableList.of(dummyAxiom1, dummyAxiom2)); + + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, () -> builder.build()); + + assertThat(exception).hasMessageThat().contains("dummy_func"); + } + + @Test + public void addFunctionAxioms_allPrimitiveTypes_checksAllTypes() throws Exception { + CelFunctionDecl dummyDecl = + CelFunctionDecl.newFunctionDeclaration( + "dummy_all_types", + CelOverloadDecl.newGlobalOverload( + "dummy_all_types_overload", + SimpleType.BOOL, + SimpleType.UINT, + SimpleType.DOUBLE, + SimpleType.BOOL, + SimpleType.STRING, + SimpleType.BYTES, + StructTypeReference.create("test.Message"))); + CelZ3FunctionAxiom dummyAxiom = + CelZ3FunctionAxiom.newBuilder(dummyDecl) + .addOverloadTranslator( + "dummy_all_types_overload", + (ctx, typeSystem, constraintSink, unwrappedArgs, argApproximations) -> + Optional.of( + CelZ3OverloadResult.create( + typeSystem.wrap(typeSystem.boolCons(), ctx.mkTrue()), ctx.mkFalse()))) + .build(); + CelVerifier verifier = + CelVerifierZ3Impl.newBuilder().addFunctionAxioms(ImmutableList.of(dummyAxiom)).build(); + Cel cel = + CelFactory.plannerCelBuilder() + .addVar("uint_var", SimpleType.UINT) + .addVar("double_var", SimpleType.DOUBLE) + .addVar("bool_var", SimpleType.BOOL) + .addVar("string_var", SimpleType.STRING) + .addVar("bytes_var", SimpleType.BYTES) + .addVar("msg_var", StructTypeReference.create("test.Message")) + .addFunctionDeclarations(dummyDecl) + .build(); + CelAbstractSyntaxTree ast = + cel.compile( + "dummy_all_types(uint_var, double_var, bool_var, string_var, bytes_var, msg_var) ==" + + " true") + .getAst(); + + CelVerificationResult result = verifier.isAlwaysTrue(ast); + assertWithMessage(result.message()) + .that(result.status()) + .isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void isAlwaysTrue_nanAndInfinityConstants_isFalse( + @TestParameter({ + "request.single_double == double('NaN')", + "request.single_double == double('Infinity')", + "request.single_double == double('-Infinity')" + }) + String expr) + throws Exception { + // There are no string literals for NaN or Infinity in CEL. We constant fold these expressions + // to produce the constant values in the optimized AST. + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) + .build(); + CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(VERIFIER.isAlwaysTrue(optimizedAst).status()).isEqualTo(VerificationStatus.VIOLATED); + } + + @Test + public void isAlwaysTrue_nanAndInfinityConstants_tautology( + @TestParameter({ + "double('Infinity') == double('Infinity')", + "double('-Infinity') == double('-Infinity')", + "double('Infinity') > double('-Infinity')", + "double('NaN') != double('NaN')" + }) + String expr) + throws Exception { + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) + .build(); + CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(VERIFIER.isAlwaysTrue(optimizedAst).status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void translateFunctionCall_staticallyResolved_skipsDeadOverloads() throws Exception { + CelFunctionDecl spyFuncDecl = + CelFunctionDecl.newFunctionDeclaration( + "spy_func", + CelOverloadDecl.newGlobalOverload("spy_func_int", SimpleType.BOOL, SimpleType.INT), + CelOverloadDecl.newGlobalOverload( + "spy_func_string", SimpleType.BOOL, SimpleType.STRING)); + + List requestedTranslations = new ArrayList<>(); + @SuppressWarnings("Immutable") // test only + class SpyTranslator implements CelZ3OverloadTranslator { + private final String overloadId; + + SpyTranslator(String overloadId) { + this.overloadId = overloadId; + } + + @Override + public Optional translate( + Context ctx, + CelZ3TypeSystem typeSystem, + Consumer constraintSink, + List> unwrappedArgs, + List argApproximations) { + requestedTranslations.add(overloadId); + return Optional.of( + CelZ3OverloadResult.create(typeSystem.wrapBool(ctx.mkTrue()), ctx.mkFalse())); + } + } + + CelZ3FunctionAxiom spyAxiom = + CelZ3FunctionAxiom.newBuilder(spyFuncDecl) + .addOverloadTranslator("spy_func_int", new SpyTranslator("spy_func_int")) + .addOverloadTranslator("spy_func_string", new SpyTranslator("spy_func_string")) + .build(); + CelVerifier verifier = CelVerifierZ3Impl.newBuilder().addFunctionAxioms(spyAxiom).build(); + + Cel cel = CelFactory.plannerCelBuilder().addFunctionDeclarations(spyFuncDecl).build(); + + CelAbstractSyntaxTree ast = cel.compile("spy_func(42) == true").getAst(); + + verifier.isSatisfiable(ast); + + assertThat(requestedTranslations).containsExactly("spy_func_int"); + } + + @Test + public void getNumericEqualityWithConstant_skipsIntForDouble() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("d == 5.0").getAst(); + try (Context ctx = new Context()) { + CelAstToZ3Translator translator = + new CelAstToZ3Translator( + ctx, + /* comprehensionUnrollLimit= */ 3, + /* unknownIdentifiers= */ ImmutableSet.of(), + /* functionRegistry= */ CelZ3FunctionRegistry.create(ImmutableList.of()), + /* typeProvider= */ CelVerifierZ3Impl.EMPTY_TYPE_PROVIDER); + + Expr result = translator.translate(ast).z3Expr(); + String resultString = result.toString(); + + assertThat(resultString).doesNotContain("getInt"); + assertThat(resultString).doesNotContain("getUint"); + assertThat(resultString).contains("getDouble"); + } + } + + @Test + public void getNumericEqualityWithConstant_skipsDoubleForInt() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("x == 5").getAst(); + try (Context ctx = new Context()) { + CelAstToZ3Translator translator = + new CelAstToZ3Translator( + ctx, + /* comprehensionUnrollLimit= */ 3, + /* unknownIdentifiers= */ ImmutableSet.of(), + /* functionRegistry= */ CelZ3FunctionRegistry.create(ImmutableList.of()), + /* typeProvider= */ CelVerifierZ3Impl.EMPTY_TYPE_PROVIDER); + + Expr result = translator.translate(ast).z3Expr(); + String resultString = result.toString(); + + assertThat(resultString).contains("getInt"); + } + } + + @Test + public void isAlwaysTrue_largeListCounterexample_truncatesOutput() throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .addVar("large_list", ListType.create(SimpleType.INT)) + .build(); + StringBuilder listLiteral = new StringBuilder("["); + for (int i = 0; i < 105; i++) { + listLiteral.append("1"); + if (i < 104) { + listLiteral.append(", "); + } + } + listLiteral.append("]"); + + CelAbstractSyntaxTree ast = cel.compile("!(large_list == " + listLiteral + ")").getAst(); + CelVerifier verifier = + CelVerifierFactory.newVerifier().setTimeout(Duration.ofSeconds(10)).build(); + + CelVerificationResult result = verifier.isAlwaysTrue(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()).contains("... (5 more elements)"); + } + + @Test + public void isAlwaysTrue_customComprehensionWithTrueAccuInit() throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .addVar("dyn_list", ListType.create(SimpleType.DYN)) + .addMacros( + CelMacro.newReceiverMacro( + "custom_fold", + 1, + (exprFactory, target, arguments) -> + Optional.of( + exprFactory.fold( + "x", + target, + "accu", + exprFactory.newBoolLiteral(true), + exprFactory.newBoolLiteral(true), + exprFactory.newGlobalCall( + Operator.LOGICAL_NOT.getFunction(), + exprFactory.newIdentifier("accu")), + exprFactory.newIdentifier("accu"))))) + .build(); + CelAbstractSyntaxTree ast = + cel.compile("dyn_list == [1, 2] ? dyn_list.custom_fold(x) == true : true").getAst(); + CelVerifier verifier = CelVerifierFactory.newVerifier().build(); + + CelVerificationResult result = verifier.isAlwaysTrue(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void isSatisfiable_maskedByBmcButAlwaysFalse_returnsFailed() throws Exception { + // The expression is false regardless of what the comprehension evaluates to. + // The verifier should not be pessimistic and should return FAILED (not INCONCLUSIVE) + // even though a loop is truncated. + String expr = "int_list == [1, 2, 3, 4] ? int_list.exists(x, x == 42) && false : false"; + CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); + + CelVerifier verifier = CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(3).build(); + CelVerificationResult result = verifier.isSatisfiable(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + } + + @Test + public void verifyEquivalence_maskedByBmcButAlwaysEqual_returnsVerified() throws Exception { + // Both expressions are always equal (false) regardless of the comprehensions. + // The verifier should return VERIFIED (not INCONCLUSIVE) even with truncation. + String exprA = "int_list == [1, 2, 3, 4] ? int_list.all(x, x > 0) && false : false"; + String exprB = "int_list == [1, 2, 3, 4] ? int_list.all(x, x > 1) && false : false"; + CelAbstractSyntaxTree astA = CEL.compile(exprA).getAst(); + CelAbstractSyntaxTree astB = CEL.compile(exprB).getAst(); + + CelVerifier verifier = CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(3).build(); + CelVerificationResult result = verifier.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void verifyEquivalence_timeoutReached_throwsCelVerificationException() throws Exception { + CelVerifier timeoutVerifier = + CelVerifierFactory.newVerifier().setTimeout(Duration.ofMillis(1)).build(); + + Cel customCel = + CelFactory.plannerCelBuilder() + .addVar("d1", SimpleType.DOUBLE) + .addVar("d2", SimpleType.DOUBLE) + .addVar("d3", SimpleType.DOUBLE) + .addVar("d4", SimpleType.DOUBLE) + .build(); + + CelAbstractSyntaxTree astA = + customCel + .compile( + "d1 * d2 * d3 * d4 * d1 * d2 * d3 * d4 == 9429185123491285.0 && d1 > 100000.0 &&" + + " d2 > 100000.0 && d3 > 100000.0 && d4 > 100000.0") + .getAst(); + CelAbstractSyntaxTree astB = customCel.compile("false").getAst(); + + CelVerificationException e = + assertThrows( + CelVerificationException.class, () -> timeoutVerifier.verifyEquivalence(astA, astB)); + assertThat(e).hasMessageThat().containsMatch("timeout|canceled"); + } +} From bf8633888fa8ddb539b1001d50fae999c3373865 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 15 Jul 2026 15:18:17 -0700 Subject: [PATCH 123/204] Internal Changes PiperOrigin-RevId: 948573764 --- .../cel/verifier/CelAstToZ3Translator.java | 140 +++++++++--------- .../dev/cel/verifier/CelZ3TypeSystem.java | 8 + 2 files changed, 77 insertions(+), 71 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java index dc2f377b4..38a7c8daa 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java @@ -464,6 +464,56 @@ private Expr getDefaultValueForType(CelType type) { return typeSystem.mkUnknown(); } + private static final class FieldAccess { + final Expr presence; + final Expr value; + + FieldAccess(Expr presence, Expr value) { + this.presence = presence; + this.value = value; + } + } + + private FieldAccess getMapAccess(Expr operand, String field, BoolExpr typeGuard) { + Expr mapRef = typeSystem.getMapRef(operand); + Expr mapFieldZ3Str = typeSystem.mkString(field); + Expr presence = ctx.mkSelect((ArrayExpr) typeSystem.getMapPresence(mapRef), mapFieldZ3Str); + Expr value = ctx.mkSelect((ArrayExpr) typeSystem.getMapValues(mapRef), mapFieldZ3Str); + + BoolExpr valNotError = ctx.mkNot(ctx.mkEq(value, typeSystem.mkError())); + typeConstraints.add( + ctx.mkImplies( + CelZ3TypeSystem.mkAndFlattened(ctx, typeGuard, (BoolExpr) presence), valNotError)); + if (unknownIdentifiers.isEmpty()) { + BoolExpr valNotUnknown = ctx.mkNot(ctx.mkEq(value, typeSystem.mkUnknown())); + typeConstraints.add( + ctx.mkImplies( + CelZ3TypeSystem.mkAndFlattened(ctx, typeGuard, (BoolExpr) presence), valNotUnknown)); + } + + return new FieldAccess(presence, value); + } + + private FieldAccess getMsgAccess(Expr operand, String field, BoolExpr typeGuard) { + Expr msgRef = typeSystem.getMessageRef(operand); + Expr msgFieldZ3Str = ctx.mkString(field); + Expr presence = ctx.mkSelect((ArrayExpr) typeSystem.getMsgPresence(msgRef), msgFieldZ3Str); + Expr value = ctx.mkSelect((ArrayExpr) typeSystem.getMsgValues(msgRef), msgFieldZ3Str); + + BoolExpr valNotError = ctx.mkNot(ctx.mkEq(value, typeSystem.mkError())); + typeConstraints.add( + ctx.mkImplies( + CelZ3TypeSystem.mkAndFlattened(ctx, typeGuard, (BoolExpr) presence), valNotError)); + if (unknownIdentifiers.isEmpty()) { + BoolExpr valNotUnknown = ctx.mkNot(ctx.mkEq(value, typeSystem.mkUnknown())); + typeConstraints.add( + ctx.mkImplies( + CelZ3TypeSystem.mkAndFlattened(ctx, typeGuard, (BoolExpr) presence), valNotUnknown)); + } + + return new FieldAccess(presence, value); + } + private TranslatedValue translateSelect(CelExpr celExpr, CelAbstractSyntaxTree ast) { CelExpr.CelSelect select = celExpr.select(); long exprId = celExpr.id(); @@ -476,76 +526,31 @@ private TranslatedValue translateSelect(CelExpr celExpr, CelAbstractSyntaxTree a Expr valueResult; if (operandType instanceof MapType) { - Expr mapFieldZ3Str = typeSystem.mkString(field); - Expr mapRef = typeSystem.getMapRef(operand); - ArrayExpr mapPresence = (ArrayExpr) typeSystem.getMapPresence(mapRef); - ArrayExpr mapValues = (ArrayExpr) typeSystem.getMapValues(mapRef); - Expr inMap = ctx.mkSelect(mapPresence, mapFieldZ3Str); - Expr mapVal = ctx.mkSelect(mapValues, mapFieldZ3Str); - - BoolExpr valNotError = ctx.mkNot(ctx.mkEq(mapVal, typeSystem.mkError())); - typeConstraints.add(ctx.mkImplies((BoolExpr) inMap, valNotError)); - if (unknownIdentifiers.isEmpty()) { - BoolExpr valNotUnknown = ctx.mkNot(ctx.mkEq(mapVal, typeSystem.mkUnknown())); - typeConstraints.add(ctx.mkImplies((BoolExpr) inMap, valNotUnknown)); - } - - presenceResult = inMap; - valueResult = ctx.mkITE((BoolExpr) inMap, mapVal, typeSystem.mkError()); + FieldAccess mapAcc = getMapAccess(operand, field, ctx.mkTrue()); + presenceResult = mapAcc.presence; + valueResult = ctx.mkITE((BoolExpr) mapAcc.presence, mapAcc.value, typeSystem.mkError()); } else if (operandType.kind() == CelKind.STRUCT) { - Expr msgFieldZ3Str = ctx.mkString(field); - Expr msgRef = typeSystem.getMessageRef(operand); - ArrayExpr msgPresence = (ArrayExpr) typeSystem.getMsgPresence(msgRef); - ArrayExpr msgValues = (ArrayExpr) typeSystem.getMsgValues(msgRef); - Expr inMsg = ctx.mkSelect(msgPresence, msgFieldZ3Str); - Expr msgVal = ctx.mkSelect(msgValues, msgFieldZ3Str); - - presenceResult = inMsg; + FieldAccess msgAcc = getMsgAccess(operand, field, ctx.mkTrue()); + presenceResult = msgAcc.presence; Expr defaultVal = getDefaultValueForType(extractAstTypeOrDefault(ast, exprId)); - valueResult = ctx.mkITE((BoolExpr) inMsg, msgVal, defaultVal); + valueResult = ctx.mkITE((BoolExpr) msgAcc.presence, msgAcc.value, defaultVal); } else { // Dynamic type: generate the full SMT decision tree - Expr mapFieldZ3Str = typeSystem.mkString(field); - Expr mapRef = typeSystem.getMapRef(operand); - ArrayExpr mapPresence = (ArrayExpr) typeSystem.getMapPresence(mapRef); - ArrayExpr mapValues = (ArrayExpr) typeSystem.getMapValues(mapRef); - Expr inMap = ctx.mkSelect(mapPresence, mapFieldZ3Str); - Expr mapVal = ctx.mkSelect(mapValues, mapFieldZ3Str); - - Expr msgFieldZ3Str = ctx.mkString(field); - Expr msgRef = typeSystem.getMessageRef(operand); - ArrayExpr msgPresence = (ArrayExpr) typeSystem.getMsgPresence(msgRef); - ArrayExpr msgValues = (ArrayExpr) typeSystem.getMsgValues(msgRef); - Expr inMsg = ctx.mkSelect(msgPresence, msgFieldZ3Str); - Expr msgVal = ctx.mkSelect(msgValues, msgFieldZ3Str); - BoolExpr isMap = typeSystem.isMap(operand); BoolExpr isMessage = typeSystem.isMessage(operand); - BoolExpr mapValNotError = ctx.mkNot(ctx.mkEq(mapVal, typeSystem.mkError())); - typeConstraints.add(ctx.mkImplies(ctx.mkAnd(isMap, (BoolExpr) inMap), mapValNotError)); - if (unknownIdentifiers.isEmpty()) { - BoolExpr mapValNotUnknown = ctx.mkNot(ctx.mkEq(mapVal, typeSystem.mkUnknown())); - typeConstraints.add(ctx.mkImplies(ctx.mkAnd(isMap, (BoolExpr) inMap), mapValNotUnknown)); - } - - BoolExpr msgValNotError = ctx.mkNot(ctx.mkEq(msgVal, typeSystem.mkError())); - typeConstraints.add(ctx.mkImplies(ctx.mkAnd(isMessage, (BoolExpr) inMsg), msgValNotError)); - if (unknownIdentifiers.isEmpty()) { - BoolExpr msgValNotUnknown = ctx.mkNot(ctx.mkEq(msgVal, typeSystem.mkUnknown())); - typeConstraints.add( - ctx.mkImplies(ctx.mkAnd(isMessage, (BoolExpr) inMsg), msgValNotUnknown)); - } + FieldAccess mapAcc = getMapAccess(operand, field, isMap); + FieldAccess msgAcc = getMsgAccess(operand, field, isMessage); presenceResult = CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) - .addCase(isMessage, inMsg) - .addCase(isMap, inMap) + .addCase(isMessage, msgAcc.presence) + .addCase(isMap, mapAcc.presence) .build(ctx.mkFalse()); Expr defaultVal = getDefaultValueForType(extractAstTypeOrDefault(ast, exprId)); - Expr msgRead = ctx.mkITE((BoolExpr) inMsg, msgVal, defaultVal); - Expr mapRead = ctx.mkITE((BoolExpr) inMap, mapVal, typeSystem.mkError()); + Expr msgRead = ctx.mkITE((BoolExpr) msgAcc.presence, msgAcc.value, defaultVal); + Expr mapRead = ctx.mkITE((BoolExpr) mapAcc.presence, mapAcc.value, typeSystem.mkError()); valueResult = CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) @@ -1022,27 +1027,20 @@ private TranslatedValue reduceAllOrExists( BoolExpr isActive = iter.inBounds; - hasMatchList.add(CelZ3TypeSystem.mkAndFlattened(ctx, Arrays.asList(isActive, isMatch))); - hasErrorList.add(CelZ3TypeSystem.mkAndFlattened(ctx, Arrays.asList(isActive, isE))); - hasUnknownList.add(CelZ3TypeSystem.mkAndFlattened(ctx, Arrays.asList(isActive, isU))); + hasMatchList.add(CelZ3TypeSystem.mkAndFlattened(ctx, isActive, isMatch)); + hasErrorList.add(CelZ3TypeSystem.mkAndFlattened(ctx, isActive, isE)); + hasUnknownList.add(CelZ3TypeSystem.mkAndFlattened(ctx, isActive, isU)); hasSafeMatchList.add( CelZ3TypeSystem.mkAndFlattened( - ctx, - Arrays.asList( - isActive, isMatch, CelZ3TypeSystem.mkNotFlattened(ctx, stepTv.isApproximate())))); + ctx, isActive, isMatch, CelZ3TypeSystem.mkNotFlattened(ctx, stepTv.isApproximate()))); hasSafeErrorList.add( CelZ3TypeSystem.mkAndFlattened( - ctx, - Arrays.asList( - isActive, isE, CelZ3TypeSystem.mkNotFlattened(ctx, stepTv.isApproximate())))); + ctx, isActive, isE, CelZ3TypeSystem.mkNotFlattened(ctx, stepTv.isApproximate()))); hasSafeUnknownList.add( CelZ3TypeSystem.mkAndFlattened( - ctx, - Arrays.asList( - isActive, isU, CelZ3TypeSystem.mkNotFlattened(ctx, stepTv.isApproximate())))); - activeTaints.add( - CelZ3TypeSystem.mkAndFlattened(ctx, Arrays.asList(isActive, stepTv.isApproximate()))); + ctx, isActive, isU, CelZ3TypeSystem.mkNotFlattened(ctx, stepTv.isApproximate()))); + activeTaints.add(CelZ3TypeSystem.mkAndFlattened(ctx, isActive, stepTv.isApproximate())); } BoolExpr hasMatch = CelZ3TypeSystem.mkOrFlattened(ctx, hasMatchList); diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java index 284050d44..8dfd16952 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java @@ -733,6 +733,10 @@ private SwitchBuilder(Context ctx) { * *

Returns {@code false} if the list is empty. */ + public static BoolExpr mkOrFlattened(Context ctx, BoolExpr... args) { + return mkOrFlattened(ctx, Arrays.asList(args)); + } + public static BoolExpr mkOrFlattened(Context ctx, List args) { // Pruning true/false constants in Java is significantly faster than building // larger ASTs and letting Z3 process them natively. @@ -759,6 +763,10 @@ public static BoolExpr mkOrFlattened(Context ctx, List args) { * *

Returns {@code true} if the list is empty. */ + public static BoolExpr mkAndFlattened(Context ctx, BoolExpr... args) { + return mkAndFlattened(ctx, Arrays.asList(args)); + } + public static BoolExpr mkAndFlattened(Context ctx, List args) { // Pruning true/false constants in Java is significantly faster than building // larger ASTs and letting Z3 process them natively. From 186eb6bd8a543c664836fd7e45738c7fc2031ce6 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 17 Jul 2026 10:02:24 -0700 Subject: [PATCH 124/204] Internal Changes PiperOrigin-RevId: 949627072 --- .../main/java/dev/cel/verifier/BUILD.bazel | 1 + .../dev/cel/verifier/CelAstAlphaHasher.java | 242 ++++++++++++++++++ .../cel/verifier/CelAstToZ3Translator.java | 35 ++- .../dev/cel/verifier/CelVerifierZ3Impl.java | 29 ++- .../CelZ3CounterexampleGenerator.java | 4 +- .../verifier/CelZ3ExtensionalityAxioms.java | 32 ++- .../cel/verifier/CelZ3OperatorTranslator.java | 10 +- .../dev/cel/verifier/CelZ3TypeSystem.java | 59 ++++- .../dev/cel/verifier/TranslatedValue.java | 4 +- .../cel/verifier/CelVerifierZ3ImplTest.java | 194 +++++++++++++- 10 files changed, 563 insertions(+), 47 deletions(-) create mode 100644 verifier/src/main/java/dev/cel/verifier/CelAstAlphaHasher.java diff --git a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel index 3f2fb509f..09a2039b4 100644 --- a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel +++ b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel @@ -93,6 +93,7 @@ java_library( java_library( name = "z3_impl", srcs = [ + "CelAstAlphaHasher.java", "CelAstToZ3Translator.java", "CelVerifierZ3Impl.java", "CelZ3CounterexampleGenerator.java", diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstAlphaHasher.java b/verifier/src/main/java/dev/cel/verifier/CelAstAlphaHasher.java new file mode 100644 index 000000000..7991290b0 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelAstAlphaHasher.java @@ -0,0 +1,242 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr; +import java.util.ArrayList; +import java.util.List; +import org.jspecify.annotations.Nullable; + +/** + * Utility for computing alpha-equivalence signatures of CEL expressions. + * + *

An alpha-equivalence signature uniquely identifies the structure of a CEL expression up to the + * renaming of bound variables (e.g., variables introduced by comprehensions). + * + *

The signature consists of: + * + *

    + *
  • A static hash of the expression's abstract syntax tree (AST). In this hash, bound variables + * are represented by their relative binder depth (de Bruijn index) rather than their string + * names, making the hash invariant to bound variable renaming. + *
  • A list of free variables encountered during the traversal of the AST, in the order of their + * appearance. + *
+ * + *

This utility is used by the verifier to parameterize Z3 unknowns generated during loop + * truncation. By using the alpha-equivalence signature, the verifier can identify when two + * different loop expressions are structurally identical and share the same free variables, allowing + * Z3 to treat their truncated outputs as equivalent. + */ +final class CelAstAlphaHasher { + + private static final HashFunction HASH_FUNCTION = Hashing.farmHashFingerprint64(); + + @AutoValue + abstract static class AlphaSignature { + abstract long staticHash(); + + abstract ImmutableList freeVariables(); + + static AlphaSignature create(long staticHash, ImmutableList freeVariables) { + return new AutoValue_CelAstAlphaHasher_AlphaSignature(staticHash, freeVariables); + } + } + + static AlphaSignature computeSignature(CelExpr expr) { + HasherContext context = new HasherContext(HASH_FUNCTION); + hashAst(expr, /* scope= */ null, context); + return AlphaSignature.create( + context.hasher.hash().asLong(), ImmutableList.copyOf(context.freeVars)); + } + + private static void hashAst(CelExpr expr, @Nullable Scope scope, HasherContext context) { + context.hasher.putString(expr.exprKind().getKind().name(), UTF_8); + switch (expr.exprKind().getKind()) { + case CONSTANT: + hashConstant(expr.constant(), context); + break; + case IDENT: + String name = expr.ident().name(); + int bIdx = scope == null ? -1 : scope.indexOf(name); + if (bIdx >= 0) { + context.hasher.putByte((byte) 0); // 0 = bound + context.hasher.putInt(bIdx); + } else { + int fIdx = -1; + for (int i = 0; i < context.freeVars.size(); i++) { + if (context.freeVars.get(i).ident().name().equals(name)) { + fIdx = i; + break; + } + } + if (fIdx == -1) { + context.freeVars.add(expr); + fIdx = context.freeVars.size() - 1; + } + context.hasher.putByte((byte) 1); // 1 = free + context.hasher.putInt(fIdx); + } + break; + case SELECT: + hashAst(expr.select().operand(), scope, context); + context.hasher.putInt(expr.select().field().length()); + context.hasher.putString(expr.select().field(), UTF_8); + context.hasher.putBoolean(expr.select().testOnly()); + break; + case CALL: + context.hasher.putInt(expr.call().function().length()); + context.hasher.putString(expr.call().function(), UTF_8); + context.hasher.putBoolean(expr.call().target().isPresent()); + if (expr.call().target().isPresent()) { + hashAst(expr.call().target().get(), scope, context); + } + context.hasher.putInt(expr.call().args().size()); + for (CelExpr arg : expr.call().args()) { + hashAst(arg, scope, context); + } + break; + case LIST: + context.hasher.putInt(expr.list().elements().size()); + for (CelExpr elem : expr.list().elements()) { + hashAst(elem, scope, context); + } + break; + case STRUCT: + context.hasher.putString(expr.struct().messageName(), UTF_8); + context.hasher.putInt(expr.struct().entries().size()); + for (CelExpr.CelStruct.Entry entry : expr.struct().entries()) { + context.hasher.putString(entry.fieldKey(), UTF_8); + context.hasher.putBoolean(entry.optionalEntry()); + hashAst(entry.value(), scope, context); + } + break; + case MAP: + context.hasher.putInt(expr.map().entries().size()); + for (CelExpr.CelMap.Entry entry : expr.map().entries()) { + context.hasher.putBoolean(entry.optionalEntry()); + hashAst(entry.key(), scope, context); + hashAst(entry.value(), scope, context); + } + break; + case COMPREHENSION: + CelExpr.CelComprehension comp = expr.comprehension(); + hashAst(comp.iterRange(), scope, context); + hashAst(comp.accuInit(), scope, context); + + context.hasher.putBoolean(!comp.accuVar().isEmpty()); + context.hasher.putBoolean(!comp.iterVar().isEmpty()); + context.hasher.putBoolean(!comp.iterVar2().isEmpty()); + + Scope loopScope = scope; + if (!comp.iterVar().isEmpty()) { + loopScope = new Scope(comp.iterVar(), loopScope); + } + if (!comp.iterVar2().isEmpty()) { + loopScope = new Scope(comp.iterVar2(), loopScope); + } + if (!comp.accuVar().isEmpty()) { + loopScope = new Scope(comp.accuVar(), loopScope); + } + + hashAst(comp.loopCondition(), loopScope, context); + hashAst(comp.loopStep(), loopScope, context); + + Scope resultScope = scope; + if (!comp.accuVar().isEmpty()) { + resultScope = new Scope(comp.accuVar(), resultScope); + } + hashAst(comp.result(), resultScope, context); + break; + case NOT_SET: + break; + } + } + + private static void hashConstant(CelConstant constant, HasherContext context) { + context.hasher.putString(constant.getKind().name(), UTF_8); + switch (constant.getKind()) { + case NULL_VALUE: + context.hasher.putInt(0); + break; + case BOOLEAN_VALUE: + context.hasher.putBoolean(constant.booleanValue()); + break; + case INT64_VALUE: + context.hasher.putLong(constant.int64Value()); + break; + case UINT64_VALUE: + context.hasher.putLong(constant.uint64Value().longValue()); + break; + case DOUBLE_VALUE: + context.hasher.putDouble(constant.doubleValue()); + break; + case STRING_VALUE: + context.hasher.putInt(constant.stringValue().length()); + context.hasher.putString(constant.stringValue(), UTF_8); + break; + case BYTES_VALUE: + context.hasher.putInt(constant.bytesValue().size()); + context.hasher.putBytes(constant.bytesValue().toByteArray()); + break; + case NOT_SET: + break; + default: + throw new UnsupportedOperationException("Unsupported constant kind: " + constant.getKind()); + } + } + + private static final class HasherContext { + final Hasher hasher; + final List freeVars = new ArrayList<>(); + + HasherContext(HashFunction hashFunction) { + this.hasher = hashFunction.newHasher(); + } + } + + private static final class Scope { + final String varName; + final Scope parent; + + Scope(String varName, Scope parent) { + this.varName = varName; + this.parent = parent; + } + + int indexOf(String name) { + int idx = 0; + Scope curr = this; + while (curr != null) { + if (curr.varName.equals(name)) { + return idx; + } + idx++; + curr = curr.parent; + } + return -1; + } + } + + private CelAstAlphaHasher() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java index 38a7c8daa..0562af693 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java @@ -485,7 +485,7 @@ private FieldAccess getMapAccess(Expr operand, String field, BoolExpr typeGua ctx.mkImplies( CelZ3TypeSystem.mkAndFlattened(ctx, typeGuard, (BoolExpr) presence), valNotError)); if (unknownIdentifiers.isEmpty()) { - BoolExpr valNotUnknown = ctx.mkNot(ctx.mkEq(value, typeSystem.mkUnknown())); + BoolExpr valNotUnknown = ctx.mkNot(typeSystem.isUnknown(value)); typeConstraints.add( ctx.mkImplies( CelZ3TypeSystem.mkAndFlattened(ctx, typeGuard, (BoolExpr) presence), valNotUnknown)); @@ -505,7 +505,7 @@ private FieldAccess getMsgAccess(Expr operand, String field, BoolExpr typeGua ctx.mkImplies( CelZ3TypeSystem.mkAndFlattened(ctx, typeGuard, (BoolExpr) presence), valNotError)); if (unknownIdentifiers.isEmpty()) { - BoolExpr valNotUnknown = ctx.mkNot(ctx.mkEq(value, typeSystem.mkUnknown())); + BoolExpr valNotUnknown = ctx.mkNot(typeSystem.isUnknown(value)); typeConstraints.add( ctx.mkImplies( CelZ3TypeSystem.mkAndFlattened(ctx, typeGuard, (BoolExpr) presence), valNotUnknown)); @@ -896,7 +896,7 @@ private TranslatedValue unrollAllAndExists( } TranslatedValue reducedTv = - reduceAllOrExists(iterations, isTruncated, iterRangeTv, isAllMacro(comp)); + reduceAllOrExists(iterations, isTruncated, iterRangeTv, isAllMacro(comp), celExpr, ast); return TranslatedValue.create( typeSystem.propagateErrorAndUnknown(reducedTv.z3Expr(), iterRangeTv.z3Expr()), celExpr, @@ -924,7 +924,6 @@ private TranslatedValue unrollMapAndFilter( List taints = new ArrayList<>(); taints.add(chainedAccuTv.isApproximate()); taints.add(iterRangeTv.isApproximate()); - taints.add(isTruncated); for (int i = 0; i < comprehensionUnrollLimit; i++) { IntExpr idx = ctx.mkInt(i); @@ -954,7 +953,10 @@ private TranslatedValue unrollMapAndFilter( Expr stepVal = ctx.mkITE((BoolExpr) typeSystem.unwrapBool(condExpr), stepExpr, currentAccu); Expr typeErrorOrStep = typeSystem.withRuntimeError(stepVal, ctx.mkNot(condIsBool)); - taints.add(condAndStep[1].isApproximate()); + // Standard macros' loop condition can't be approximate. However, we still + // keep the check here for custom macros to be safe. + taints.add(ctx.mkAnd(isActive, condAndStep[0].isApproximate())); + taints.add(ctx.mkAnd(isActive, condAndStep[1].isApproximate())); chainedAccu = ctx.mkITE( @@ -973,9 +975,13 @@ private TranslatedValue unrollMapAndFilter( taints.add(resultTv.isApproximate()); + BoolExpr isNotError = ctx.mkNot(typeSystem.isError(resultTv.z3Expr())); + BoolExpr shouldYieldUnknown = ctx.mkAnd(isTruncated, isNotError); + taints.add(shouldYieldUnknown); + return TranslatedValue.create( typeSystem.propagateErrorAndUnknown( - ctx.mkITE(isTruncated, typeSystem.mkUnknown(), resultTv.z3Expr()), + ctx.mkITE(shouldYieldUnknown, mkParameterizedUnknown(celExpr, ast), resultTv.z3Expr()), iterRangeTv.z3Expr()), celExpr, typeSystem, @@ -1001,7 +1007,9 @@ private TranslatedValue reduceAllOrExists( List iterations, BoolExpr isTruncated, TranslatedValue iterRangeTv, - boolean isAll) { + boolean isAll, + CelExpr compExpr, + CelAbstractSyntaxTree ast) { List hasMatchList = new ArrayList<>(); List hasErrorList = new ArrayList<>(); List hasUnknownList = new ArrayList<>(); @@ -1055,7 +1063,7 @@ private TranslatedValue reduceAllOrExists( Expr result = CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) .addCase(hasMatch, typeSystem.mkBool(!isAll)) - .addCase(ctx.mkOr(hasUnknown, isTruncated), typeSystem.mkUnknown()) + .addCase(ctx.mkOr(hasUnknown, isTruncated), mkParameterizedUnknown(compExpr, ast)) .addCase(hasError, typeSystem.mkError()) .build(typeSystem.mkBool(isAll)); @@ -1232,4 +1240,15 @@ private Optional toCacheKey(CelExpr expr) { return Optional.empty(); } } + + private Expr mkParameterizedUnknown(CelExpr expr, CelAbstractSyntaxTree ast) { + CelAstAlphaHasher.AlphaSignature sig = CelAstAlphaHasher.computeSignature(expr); + + ImmutableList.Builder> smtArgs = ImmutableList.builder(); + for (CelExpr freeVar : sig.freeVariables()) { + smtArgs.add(translateExpr(freeVar, ast).z3Expr()); + } + + return typeSystem.mkParameterizedUnknown(sig.staticHash(), smtArgs.build()); + } } diff --git a/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java b/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java index dbe4615da..4977bdd11 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java +++ b/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java @@ -172,14 +172,20 @@ public CelVerificationResult verifyEquivalence( solver.add(constraint); } - BoolExpr unknownCondition = - ctx.mkOr( - translator.getTypeSystem().isUnknown(tvA.z3Expr()), - translator.getTypeSystem().isUnknown(tvB.z3Expr())); + // Divergent parameterized Unknowns are naturally caught by Pass 2 (A != B is SAT). + // Identical Unknowns mean the ASTs are alpha-equivalent, so we WANT them to pass. + // We no longer need Pass 3 to conservatively bail out of equivalence checks. + BoolExpr unknownCondition = ctx.mkFalse(); SolverRunResult result = runThreePassVerification( - ctx, solver, divergenceCondition, combinedTaint, unknownCondition, translator); + ctx, + solver, + divergenceCondition, + combinedTaint, + unknownCondition, + translator, + /* checkTruncation= */ false); switch (result.outcome) { case EXACT_MATCH: @@ -230,7 +236,8 @@ private CelVerificationResult checkSatisfiability( condition, tv.isApproximate(), translator.getTypeSystem().isUnknown(tv.z3Expr()), - translator); + translator, + /* checkTruncation= */ true); switch (result.outcome) { case EXACT_MATCH: @@ -276,7 +283,8 @@ private SolverRunResult runThreePassVerification( BoolExpr condition, BoolExpr taint, BoolExpr unknownCondition, - CelAstToZ3Translator translator) + CelAstToZ3Translator translator, + boolean checkTruncation) throws CelVerificationException { // Pass 1: Search for an exact match/counterexample @@ -303,6 +311,13 @@ private SolverRunResult runThreePassVerification( return SolverRunResult.solverUnknown(checkTimeoutOrGetReason(solver)); } + // If we don't need to check truncation (e.g. for equivalence checks), + // we can pop the solver and return noMatch immediately. + if (!checkTruncation) { + solver.pop(); + return SolverRunResult.noMatch(); + } + // Pass 3: Check BMC truncation solver.pop(); solver.add(unknownCondition); diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java index 41a94b5a1..f9a438f7f 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java @@ -31,6 +31,8 @@ @SuppressWarnings({"unchecked", "rawtypes"}) // Z3 Java API uses raw types. final class CelZ3CounterexampleGenerator { + private static final int MAX_LIST_ELEMENTS_TO_PRINT = 15; + private CelZ3CounterexampleGenerator() {} static String generate(Context ctx, CelZ3TypeSystem typeSystem, Model model) { @@ -121,7 +123,7 @@ private static String reconstructList( ctx.mkLength(typeSystem.getSeq(listRef)), String.format("Z3 failed to evaluate length for list %s", listRef)); int length = ((IntNum) lenExpr).getInt(); - int printLimit = Math.min(length, 100); + int printLimit = Math.min(length, MAX_LIST_ELEMENTS_TO_PRINT); List elements = new ArrayList<>(); for (int i = 0; i < printLimit; i++) { Expr elem = diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3ExtensionalityAxioms.java b/verifier/src/main/java/dev/cel/verifier/CelZ3ExtensionalityAxioms.java index db325b429..2303abcaf 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3ExtensionalityAxioms.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3ExtensionalityAxioms.java @@ -82,12 +82,15 @@ private static void addListAxioms( FuncDecl mkListRef = ctx.mkFuncDecl(FUNC_MK_LIST_REF, new Sort[] {seqSort}, listRefSort); for (Expr ref : refs) { + if (isAppOf(ref, FUNC_MK_LIST_REF)) { + continue; + } Expr ufApp = ctx.mkApp(mkListRef, typeSystem.getSeq(ref)); // Ground assertion: f_list(seq(ref)) = ref BoolExpr axiom = ctx.mkEq(ufApp, ref); // Guard the axiom with type check if the reference is extracted from a CelValue - if (ref.isApp() && ref.getFuncDecl().equals(typeSystem.listCons().getAccessorDecls()[0])) { + if (isAppOf(ref, typeSystem.listCons().getAccessorDecls()[0])) { Expr inner = ref.getArgs()[0]; axiom = ctx.mkImplies(typeSystem.isList(inner), axiom); } @@ -109,13 +112,16 @@ private static void addMapAxioms( ctx.mkFuncDecl(FUNC_MK_MAP_REF, new Sort[] {valuesSort, presenceSort}, mapRefSort); for (Expr ref : refs) { + if (isAppOf(ref, FUNC_MK_MAP_REF)) { + continue; + } Expr ufApp = ctx.mkApp(mkMapRef, typeSystem.getMapValues(ref), typeSystem.getMapPresence(ref)); // Ground assertion: f_map(values(ref), presence(ref)) = ref BoolExpr axiom = ctx.mkEq(ufApp, ref); // Guard the axiom with type check if the reference is extracted from a CelValue - if (ref.isApp() && ref.getFuncDecl().equals(typeSystem.mapCons().getAccessorDecls()[0])) { + if (isAppOf(ref, typeSystem.mapCons().getAccessorDecls()[0])) { Expr inner = ref.getArgs()[0]; axiom = ctx.mkImplies(typeSystem.isMap(inner), axiom); } @@ -139,6 +145,9 @@ private static void addMessageAxioms( FUNC_MK_MSG_REF, new Sort[] {typeNameSort, valuesSort, presenceSort}, msgRefSort); for (Expr ref : refs) { + if (isAppOf(ref, FUNC_MK_MSG_REF)) { + continue; + } Expr ufApp = ctx.mkApp( mkMsgRef, @@ -149,7 +158,7 @@ private static void addMessageAxioms( BoolExpr axiom = ctx.mkEq(ufApp, ref); // Guard the axiom with type check if the reference is extracted from a CelValue - if (ref.isApp() && ref.getFuncDecl().equals(typeSystem.messageCons().getAccessorDecls()[0])) { + if (isAppOf(ref, typeSystem.messageCons().getAccessorDecls()[0])) { Expr inner = ref.getArgs()[0]; axiom = ctx.mkImplies(typeSystem.isMessage(inner), axiom); } @@ -157,5 +166,22 @@ private static void addMessageAxioms( } } + /** + * Returns true if the expression is an application of the specified uninterpreted function. + * + *

We check this to avoid generating redundant or nested extensionality axioms for references + * that are already constructed using the maker functions (e.g., {@code !mkListRef}). Congruence + * closure natively handles equality for these constructed references, so additional axioms are + * unnecessary and degrade solver performance. + */ + private static boolean isAppOf(Expr expr, String funcName) { + return expr.isApp() && expr.getFuncDecl().getName().toString().equals(funcName); + } + + /** Returns true if the expression is an application of the specified function declaration. */ + private static boolean isAppOf(Expr expr, FuncDecl funcDecl) { + return expr.isApp() && expr.getFuncDecl().equals(funcDecl); + } + private CelZ3ExtensionalityAxioms() {} } diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java index 3c24cc20d..e2df87b2a 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java @@ -293,10 +293,12 @@ private TranslatedValue translateBinaryLogicalAndOr( ctx.mkAnd(a.isZ3Unknown(), ctx.mkNot(a.isApproximate())), ctx.mkAnd(b.isZ3Unknown(), ctx.mkNot(b.isApproximate()))); + Expr unknownResult = ctx.mkITE(a.isZ3Unknown(), a.z3Expr(), b.z3Expr()); + Expr resultZ3 = CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) .addCase(hasMatch, typeSystem.mkBool(!isAnd)) - .addCase(hasUnknown, typeSystem.mkUnknown()) + .addCase(hasUnknown, unknownResult) .addCase(hasError, typeSystem.mkError()) .build(typeSystem.mkBool(isAnd)); @@ -593,7 +595,7 @@ private Expr buildListIndex(Expr lhsTrans, Expr rhsTrans) { BoolExpr valNotError = ctx.mkNot(ctx.mkEq(val, typeSystem.mkError())); constraintSink.accept(ctx.mkImplies(inBounds, valNotError)); if (!allowUnknowns) { - BoolExpr valNotUnknown = ctx.mkNot(ctx.mkEq(val, typeSystem.mkUnknown())); + BoolExpr valNotUnknown = ctx.mkNot(typeSystem.isUnknown(val)); constraintSink.accept(ctx.mkImplies(inBounds, valNotUnknown)); } @@ -610,7 +612,7 @@ private Expr buildMapIndex(Expr lhsTrans, Expr rhsTrans) { BoolExpr valNotError = ctx.mkNot(ctx.mkEq(val, typeSystem.mkError())); constraintSink.accept(ctx.mkImplies(inMap, valNotError)); if (!allowUnknowns) { - BoolExpr valNotUnknown = ctx.mkNot(ctx.mkEq(val, typeSystem.mkUnknown())); + BoolExpr valNotUnknown = ctx.mkNot(typeSystem.isUnknown(val)); constraintSink.accept(ctx.mkImplies(inMap, valNotUnknown)); } @@ -669,7 +671,7 @@ private TranslatedValue translateConditional( Expr resultZ3 = CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) - .addCase(hasUnknown, typeSystem.mkUnknown()) + .addCase(hasUnknown, cond.z3Expr()) .addCase(hasError, typeSystem.mkError()) .addCase(condTrue, trueBranch.z3Expr()) .build(falseBranch.z3Expr()); diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java index 8dfd16952..7bd6d0daf 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java @@ -86,6 +86,8 @@ public final class CelZ3TypeSystem { private static final String CONS_UNKNOWN = "CelUnknown"; private static final String IS_UNKNOWN = "isUnknown"; + private static final String GET_UNKNOWN = "getUnknownId"; + private static final String GENERIC_UNKNOWN_ID = "!generic_unknown"; private static final String CONS_NULL = "CelNull"; private static final String IS_NULL = "isNull"; @@ -172,6 +174,7 @@ public int hashCode() { private final Constructor nullCons; private final Constructor optionalCons; + private final Sort unknownIdSort; private final Sort optionalRefSort; private final FuncDecl optionalValueFunc; private final FuncDecl optionalOfRefFunc; @@ -402,7 +405,35 @@ Constructor errorCons() { /** Creates a CelValue representing an unknown value. */ public Expr mkUnknown() { - return ctx.mkConst(unknownCons.ConstructorDecl()); + return mkUnknown(ctx.mkConst(GENERIC_UNKNOWN_ID, unknownIdSort)); + } + + /** Creates a CelValue representing an unknown value with a specific ID. */ + public Expr mkUnknown(Expr unknownId) { + return ctx.mkApp(unknownCons.ConstructorDecl(), unknownId); + } + + /** Creates a parameterized unknown representing a truncated comprehension. */ + public Expr mkParameterizedUnknown(long staticHash, List> smtArgs) { + Sort[] domain = new Sort[smtArgs.size()]; + for (int i = 0; i < smtArgs.size(); i++) { + domain[i] = celValueSort(); + } + + String ufName = "!trunc_" + Long.toHexString(staticHash); + FuncDecl truncUf = internFuncDecl(ufName, domain, unknownIdSort()); + + Expr uniqueUnknownId = + smtArgs.isEmpty() + ? ctx.mkConst(ufName, unknownIdSort()) + : ctx.mkApp(truncUf, smtArgs.toArray(new Expr[0])); + + return mkUnknown(uniqueUnknownId); + } + + /** Gets the sort used for unknown identifiers. */ + public Sort unknownIdSort() { + return unknownIdSort; } /** @@ -422,19 +453,24 @@ Expr propagateErrorAndUnknown(Expr result, Collection> args) { if (args.isEmpty()) { return result; } - BoolExpr[] errors = new BoolExpr[args.size()]; - BoolExpr[] unknowns = new BoolExpr[args.size()]; - int i = 0; - for (Expr arg : args) { + List> argsList = new ArrayList<>(args); + BoolExpr[] errors = new BoolExpr[argsList.size()]; + BoolExpr[] unknowns = new BoolExpr[argsList.size()]; + Expr unknownResult = mkUnknown(); + // Walk backwards to preserve the earliest unknown in case of multiple unknowns (applicable for + // nested ITE chain) + for (int i = argsList.size() - 1; i >= 0; i--) { + Expr arg = argsList.get(i); errors[i] = isError(arg); - unknowns[i] = isUnknown(arg); - i++; + BoolExpr isUnknown = isUnknown(arg); + unknowns[i] = isUnknown; + unknownResult = ctx.mkITE(isUnknown, arg, unknownResult); } BoolExpr hasError = ctx.mkOr(errors); BoolExpr hasUnknown = ctx.mkOr(unknowns); // Unknowns have higher precedence than error return SwitchBuilder.newBuilder(ctx) - .addCase(hasUnknown, mkUnknown()) + .addCase(hasUnknown, unknownResult) .addCase(hasError, mkError()) .build(result); } @@ -832,7 +868,12 @@ public static BoolExpr mkNotFlattened(Context ctx, BoolExpr arg) { ctx.mkConstructor( CONS_BYTES, IS_BYTES, new String[] {GET_BYTES}, new Sort[] {ctx.getStringSort()}, null); this.errorCons = ctx.mkConstructor(CONS_ERROR, IS_ERROR, null, null, null); - this.unknownCons = ctx.mkConstructor(CONS_UNKNOWN, IS_UNKNOWN, null, null, null); + + this.unknownIdSort = ctx.mkUninterpretedSort("UnknownId"); + this.unknownCons = + ctx.mkConstructor( + CONS_UNKNOWN, IS_UNKNOWN, new String[] {GET_UNKNOWN}, new Sort[] {unknownIdSort}, null); + this.nullCons = ctx.mkConstructor(CONS_NULL, IS_NULL, null, null, null); this.optionalRefSort = ctx.mkUninterpretedSort(SORT_OPTIONAL_REF); this.optionalCons = diff --git a/verifier/src/main/java/dev/cel/verifier/TranslatedValue.java b/verifier/src/main/java/dev/cel/verifier/TranslatedValue.java index 336e5a7ed..919778270 100644 --- a/verifier/src/main/java/dev/cel/verifier/TranslatedValue.java +++ b/verifier/src/main/java/dev/cel/verifier/TranslatedValue.java @@ -150,7 +150,9 @@ static TranslatedValue propagateStrict( taints.add(baseTaint); boolean hasNonConstantArgs = false; - for (TranslatedValue arg : args) { + List argsList = new ArrayList<>(args); + for (int i = argsList.size() - 1; i >= 0; i--) { + TranslatedValue arg = argsList.get(i); taints.add(arg.isApproximate()); if (arg.isLiteral(ExprKind.Kind.CONSTANT)) { continue; diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index c16114dd2..8af4a8c62 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -16,6 +16,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static java.util.stream.Collectors.joining; import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; @@ -61,7 +62,6 @@ import java.util.List; import java.util.Optional; import java.util.function.Consumer; -import java.util.stream.Collectors; import java.util.stream.IntStream; import org.junit.Before; import org.junit.Test; @@ -371,7 +371,7 @@ private enum IsAlwaysTrueTestCase { LARGE_NUMBER_OF_LIST_EQUALITIES( IntStream.range(0, 1000) .mapToObj(i -> String.format("[%d] == [%d]", i, i)) - .collect(Collectors.joining(" && "))), + .collect(joining(" && "))), DYNAMIC_MAP_KEY_EXTENSIONALITY( "dyn_map == {'a': 1, 'b': 2} ? dyn_map.all(k, k in {'a': 1, 'b': 2}) : true"), DYNAMIC_LIST_MAP_STANDARD("int_list == [1, 2, 3] ? int_list.map(x, x * 2) == [2, 4, 6] : true"), @@ -774,6 +774,26 @@ public void isSatisfiable_approximateIterRangeInMap_inconclusive() throws Except assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); } + @Test + public void isSatisfiable_mapWithUnusedApproximateIteration_verified() throws Exception { + Cel celWithCustomFunc = + CelFactory.plannerCelBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addVar("y", ListType.create(SimpleType.INT)) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "approx_func", + CelOverloadDecl.newGlobalOverload( + "approx_func_overload", SimpleType.INT, SimpleType.INT))) + .build(); + CelAbstractSyntaxTree ast = + celWithCustomFunc.compile("y == [] && y.map(x, approx_func(1)) == y").getAst(); + + CelVerificationResult result = VERIFIER.isSatisfiable(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } + private enum UnconditionalErrorTestCase { MAP_MISSING_KEY("{'a': 1}['b'] == 0 || !({'a': 1}['b'] == 0)"), COLLECTION_ERROR("{'a': 1 / 0} == {'a': 1 / 0}"), @@ -1014,10 +1034,7 @@ private enum IsAlwaysTrueViolationTestCase { "Counterexample input:", "request = NaN"), CROSS_TYPE_NUMERIC_EQUALITY_APPROXIMATION_VIOLATION( - "dyn_var == 1.0", - "Condition is not always true.", - "Counterexample input:", - "dyn_var = false"), + "dyn_var == 1.0", "Condition is not always true.", "Counterexample input:", "dyn_var ="), DYNAMIC_NOT_TYPE_MISMATCH( "!dyn_var", "Condition is not always true.", "Counterexample input:", "dyn_var ="), DYNAMIC_CONDITIONAL_TYPE_MISMATCH( @@ -1079,7 +1096,16 @@ private enum IsInconclusiveTestCase { MAP_COMPREHENSION_APPROXIMATE_VALUE( "{\"key\": request.matches('a') ? 1 : 2}.all(k, v, v == 1)"), BIND_APPROXIMATE_ACCU("cel.bind(x, request.matches('a') ? 1 : 2, x == 1)"), - BIND_APPROXIMATE_BODY("cel.bind(x, 1, x == 1 && request.matches('a'))"); + BIND_APPROXIMATE_BODY("cel.bind(x, 1, x == 1 && request.matches('a'))"), + COMPREHENSION_OPTIONAL_MAP_ENTRY( + "size(int_list) == 6 ? size(int_list.map(x, {? x: optional.of(1)})) == 6 : true"), + COMPREHENSION_OPTIONAL_STRUCT_ENTRY( + "size(int_list) == 6 ? size(int_list.map(x, TestAllTypes{?single_int32: optional.of(x)}))" + + " == 6 : true"), + COMPREHENSION_NULL_CONSTANT("size(int_list) == 6 ? size(int_list.map(x, null)) == 6 : true"), + COMPREHENSION_UINT_CONSTANT("size(int_list) == 6 ? size(int_list.map(x, 1u)) == 6 : true"), + COMPREHENSION_DOUBLE_CONSTANT("size(int_list) == 6 ? size(int_list.map(x, 1.0)) == 6 : true"), + COMPREHENSION_BYTES_CONSTANT("size(int_list) == 6 ? size(int_list.map(x, b'abc')) == 6 : true"); final String expr; @@ -1102,11 +1128,25 @@ private enum EquivalenceInconclusiveTestCase { MASKED_BY_BMC( "int_list == [1, 2, 3, 4, 5, 6] ? int_list.all(x, x > 0) : true", "int_list == [1, 2, 3, 4, 5, 6] ? (int_list.all(x, x > 0) || size(int_list) == 6) : true"), - // TODO: Implement alpha equivalent unknowns to handle this case - TRUNCATION_EQUIVALENT( - "int_list == [1, 2, 3, 4, 5, 6] ? int_list.all(x, x > 0) : true", - "int_list == [1, 2, 3, 4, 5, 6] ? int_list.all(y, y > 0) : true"), - APPROXIMATION_DIVERGENCE("request.matches('a') == true", "request.matches('a') == false"); + APPROXIMATION_DIVERGENCE("request.matches('a') == true", "request.matches('a') == false"), + IDENTITY_ERASURE_INCONGRUENCE( + "size(int_list) == 6 && size(int_list_2) == 6 ? (int_list.map(x, x + 1) + [1]) : [1]", + "size(int_list) == 6 && size(int_list_2) == 6 ? (int_list_2.map(x, x + 1) + [1]) : [1]"), + TRUNCATION_DIVERGENCE_DIFFERENT_CONSTANTS( + "size(int_list) == 6 ? int_list.map(x, x + 1) : [1]", + "size(int_list) == 6 ? int_list.map(x, x + 2) : [1]"), + TRUNCATION_DIVERGENCE_DIFFERENT_VARIABLES( + "size(int_list) == 6 && size(int_list_2) == 6 ? size(int_list.filter(x, x > 2)) : 0", + "size(int_list) == 6 && size(int_list_2) == 6 ? size(int_list_2.filter(y, y > 2)) : 0"), + TRUNCATION_DIVERGENCE_DIFFERENT_UINTS( + "size(int_list) == 6 ? int_list.map(x, 1u) : [1u]", + "size(int_list) == 6 ? int_list.map(x, 2u) : [1u]"), + TRUNCATION_DIVERGENCE_DIFFERENT_DOUBLES( + "size(int_list) == 6 ? int_list.map(x, 1.0) : [1.0]", + "size(int_list) == 6 ? int_list.map(x, 2.0) : [1.0]"), + TRUNCATION_DIVERGENCE_DIFFERENT_BYTES( + "size(int_list) == 6 ? int_list.map(x, b'a') : [b'a']", + "size(int_list) == 6 ? int_list.map(x, b'b') : [b'a']"); final String exprA; final String exprB; @@ -1129,6 +1169,12 @@ public void verifyEquivalence_inconclusive( } private enum EquivalenceTestCase { + TRUNCATION_STRICT_PROPAGATION_EQUIVALENT( + "size(int_list) == 6 ? size(int_list.filter(x, x > 2)) : 0", + "size(int_list) == 6 ? size(int_list.filter(y, y > 2)) : 0"), + TRUNCATION_EQUIVALENT( + "int_list == [1, 2, 3, 4, 5, 6] ? int_list.all(x, x > 0) : true", + "int_list == [1, 2, 3, 4, 5, 6] ? int_list.all(y, y > 0) : true"), DE_MORGANS_LAW("!(a && b)", "!a || !b"), CONSTANT_FOLDING("x > 5 + 5", "x > 10"), STRING_COMPARISON("role == \"admin\"", "\"admin\" == role"), @@ -2081,9 +2127,9 @@ public void isAlwaysTrue_largeListCounterexample_truncatesOutput() throws Except .addVar("large_list", ListType.create(SimpleType.INT)) .build(); StringBuilder listLiteral = new StringBuilder("["); - for (int i = 0; i < 105; i++) { + for (int i = 0; i < 20; i++) { listLiteral.append("1"); - if (i < 104) { + if (i < 19) { listLiteral.append(", "); } } @@ -2159,6 +2205,126 @@ public void verifyEquivalence_maskedByBmcButAlwaysEqual_returnsVerified() throws assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); } + private enum EquivalenceZeroUnrollLimitTestCase { + DIFFERENT_CONSTANT_KINDS("dyn_list.exists(i, i == 0)", "dyn_list.exists(i, i == 0.0)"), + DIFFERENT_COLLECTION_KINDS("dyn_list.exists(i, i == [])", "dyn_list.exists(i, i == {})"), + DIFFERENT_CONSTANTS("dyn_list.exists(i, i == 1)", "dyn_list.exists(i, i == 2)"), + DIFFERENT_VARIABLES("dyn_list.exists(i, i == x)", "dyn_list.exists(i, i == y)"); + + final String exprA; + final String exprB; + + EquivalenceZeroUnrollLimitTestCase(String exprA, String exprB) { + this.exprA = exprA; + this.exprB = exprB; + } + } + + @Test + public void verifyEquivalence_zeroUnrollLimit_returnsInconclusive( + @TestParameter EquivalenceZeroUnrollLimitTestCase testCase) throws Exception { + CelAbstractSyntaxTree astA = CEL.compile(testCase.exprA).getAst(); + CelAbstractSyntaxTree astB = CEL.compile(testCase.exprB).getAst(); + + CelVerifier verifier = CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(0).build(); + CelVerificationResult result = verifier.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + } + + @Test + public void verifyEquivalence_comprehensionScopeShadowing_returnsInconclusive() throws Exception { + CelMacro macro1 = + CelMacro.newReceiverMacro( + "my_macro_1", + 1, + (exprFactory, target, arguments) -> + Optional.of( + exprFactory.fold( + "unused", + "x", + target, + "x", + arguments.get(0), + exprFactory.newBoolLiteral(true), + /* step= */ exprFactory.newIdentifier("x"), + /* result= */ exprFactory.newIdentifier("x")))); + + CelMacro macro2 = + CelMacro.newReceiverMacro( + "my_macro_2", + 1, + (exprFactory, target, arguments) -> + Optional.of( + exprFactory.fold( + "unused", + "y", + target, + "x", + arguments.get(0), + exprFactory.newBoolLiteral(true), + /* step= */ exprFactory.newIdentifier("y"), + /* result= */ exprFactory.newIdentifier("x")))); + + Cel customCel = + CelFactory.plannerCelBuilder() + .addVar("dyn_list", ListType.create(SimpleType.DYN)) + .addMacros(macro1, macro2) + .build(); + + CelAbstractSyntaxTree astA = customCel.compile("dyn_list.my_macro_1(true)").getAst(); + CelAbstractSyntaxTree astB = customCel.compile("dyn_list.my_macro_2(true)").getAst(); + + CelVerifier verifier = CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(0).build(); + CelVerificationResult result = verifier.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + } + + @Test + public void verifyEquivalence_comprehensionResultScopeIsolation_returnsInconclusive() + throws Exception { + CelMacro macro = + CelMacro.newReceiverMacro( + "my_macro", + 1, + (exprFactory, target, arguments) -> + Optional.of( + exprFactory.fold( + /* iterVar= */ "x", + /* iterRange= */ target, + /* accuVar= */ "accu", + /* accuInit= */ arguments.get(0), + /* condition= */ exprFactory.newBoolLiteral(true), + /* step= */ exprFactory.newIdentifier("accu"), + /* result= */ exprFactory.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + exprFactory.newGlobalCall( + Operator.EQUALS.getFunction(), + exprFactory.newGlobalCall("size", target), + exprFactory.newIntLiteral(0L)), + exprFactory.newIntLiteral(0L), + exprFactory.newIdentifier("x"))))); + + Cel customCel = + CelFactory.plannerCelBuilder() + .addVar("dyn_list", ListType.create(SimpleType.DYN)) + .addVar("x", SimpleType.INT) + .addMacros(macro) + .addCompilerLibraries(CelExtensions.bindings()) + .build(); + + CelAbstractSyntaxTree astA = + customCel.compile("cel.bind(x, 10, dyn_list.my_macro(1))").getAst(); + CelAbstractSyntaxTree astB = + customCel.compile("cel.bind(x, 20, dyn_list.my_macro(1))").getAst(); + + CelVerifier verifier = CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(0).build(); + CelVerificationResult result = verifier.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + } + @Test public void verifyEquivalence_timeoutReached_throwsCelVerificationException() throws Exception { CelVerifier timeoutVerifier = From a8a288e3b1de4ae42d74b8686853068fb7eefd60 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 17 Jul 2026 15:41:34 -0700 Subject: [PATCH 125/204] Internal Changes PiperOrigin-RevId: 949795380 --- .../dev/cel/verifier/CelVerifierZ3Impl.java | 24 ++++++++++++------- .../CelZ3CounterexampleGenerator.java | 6 +++-- .../cel/verifier/CelVerifierZ3ImplTest.java | 14 ++++++++++- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java b/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java index 4977bdd11..35d847d1b 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java +++ b/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java @@ -141,14 +141,14 @@ public CelVerifier build() { public CelVerificationResult isSatisfiable(CelAbstractSyntaxTree ast) throws CelVerificationException { Preconditions.checkArgument(ast.isChecked(), "AST must be type-checked."); - return checkSatisfiability(ast, false); + return checkSatisfiability(ast, /* searchForCounterexample= */ false); } @Override public CelVerificationResult isAlwaysTrue(CelAbstractSyntaxTree ast) throws CelVerificationException { Preconditions.checkArgument(ast.isChecked(), "AST must be type-checked."); - return checkSatisfiability(ast, true); + return checkSatisfiability(ast, /* searchForCounterexample= */ true); } @Override @@ -191,12 +191,14 @@ public CelVerificationResult verifyEquivalence( case EXACT_MATCH: return CelVerificationResult.failed( "Equivalence violation detected." - + getCounterexampleString(ctx, translator.getTypeSystem(), result.model)); + + getCounterexampleString( + ctx, translator.getTypeSystem(), result.model, /* isApproximate= */ false)); case APPROXIMATE_MATCH: return CelVerificationResult.inconclusive( "Inconclusive: a divergence may exist, but it depends on approximations, missing" + " theories, or loop bounds." - + getCounterexampleString(ctx, translator.getTypeSystem(), result.model)); + + getCounterexampleString( + ctx, translator.getTypeSystem(), result.model, /* isApproximate= */ true)); case TRUNCATED: return CelVerificationResult.inconclusive( "Inconclusive: expressions are equivalent within the current loop unroll limit, but" @@ -244,7 +246,11 @@ private CelVerificationResult checkSatisfiability( return searchForCounterexample ? CelVerificationResult.failed( "Condition is not always true." - + getCounterexampleString(ctx, translator.getTypeSystem(), result.model)) + + getCounterexampleString( + ctx, + translator.getTypeSystem(), + result.model, + /* isApproximate= */ false)) : CelVerificationResult.verified(); case APPROXIMATE_MATCH: @@ -255,7 +261,9 @@ private CelVerificationResult checkSatisfiability( : "Inconclusive: a satisfying model may exist, but it depends on" + " approximations, missing theories, or loop bounds."; return CelVerificationResult.inconclusive( - prefix + getCounterexampleString(ctx, translator.getTypeSystem(), result.model)); + prefix + + getCounterexampleString( + ctx, translator.getTypeSystem(), result.model, /* isApproximate= */ true)); case TRUNCATED: return CelVerificationResult.inconclusive( @@ -349,8 +357,8 @@ private Solver newSolver(Context ctx) { } private static String getCounterexampleString( - Context ctx, CelZ3TypeSystem typeSystem, Model model) { - return CelZ3CounterexampleGenerator.generate(ctx, typeSystem, model); + Context ctx, CelZ3TypeSystem typeSystem, Model model, boolean isApproximate) { + return CelZ3CounterexampleGenerator.generate(ctx, typeSystem, model, isApproximate); } CelVerifierZ3Impl( diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java index f9a438f7f..8dcc73435 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java @@ -35,7 +35,8 @@ final class CelZ3CounterexampleGenerator { private CelZ3CounterexampleGenerator() {} - static String generate(Context ctx, CelZ3TypeSystem typeSystem, Model model) { + static String generate( + Context ctx, CelZ3TypeSystem typeSystem, Model model, boolean isApproximate) { FuncDecl[] constDecls = model.getConstDecls(); List bindings = new ArrayList<>(); @@ -57,7 +58,8 @@ static String generate(Context ctx, CelZ3TypeSystem typeSystem, Model model) { return " (The expression fails unconditionally, regardless of input state)"; } - return " Counterexample input:" + String.join("", bindings); + String prefix = isApproximate ? " Potential counterexample input:" : " Counterexample input:"; + return prefix + String.join("", bindings); } private static String formatExpr( diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index 8af4a8c62..5c13cc8de 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -602,7 +602,8 @@ private enum IsAlwaysTrueTestCase { "'key' in string_int_map ? type(string_int_map['key']) == int : true"), LITERAL_LIST_INDEX("[1, 2][0] == 1"), NESTED_LIST_VARIABLES_EQUALITY( - "nested_list == [[1]] && nested_list_2 == [[1]] ? nested_list == nested_list_2 : true"); + "nested_list == [[1]] && nested_list_2 == [[1]] ? nested_list == nested_list_2 : true"), + ; final String expr; @@ -1124,6 +1125,17 @@ public void isAlwaysTrue_inconclusive(@TestParameter IsInconclusiveTestCase test assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); } + @Test + public void isAlwaysTrue_inconclusive_containsPotentialCounterexample() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("request.matches('^[a-z]+$') == true").getAst(); + + CelVerificationResult result = VERIFIER.isAlwaysTrue(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + assertThat(result.message()) + .containsMatch("Potential counterexample input:\\n\\s*request = .*"); + } + private enum EquivalenceInconclusiveTestCase { MASKED_BY_BMC( "int_list == [1, 2, 3, 4, 5, 6] ? int_list.all(x, x > 0) : true", From ff7bccfe61d49894e156c1e994a2c4f161627340 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 17 Jul 2026 21:19:31 -0700 Subject: [PATCH 126/204] Open source the CEL formal verification framework PiperOrigin-RevId: 949908577 --- .../cross_artifact_dependencies_check.sh | 1 + MODULE.bazel | 1 + publish/BUILD.bazel | 33 ++ verifier/README.md | 304 ++++++++++++++++++ .../main/java/dev/cel/verifier/BUILD.bazel | 7 +- .../java/dev/cel/verifier/axioms/BUILD.bazel | 5 +- 6 files changed, 349 insertions(+), 2 deletions(-) create mode 100644 verifier/README.md diff --git a/.github/workflows/cross_artifact_dependencies_check.sh b/.github/workflows/cross_artifact_dependencies_check.sh index 0802a299a..de213e125 100755 --- a/.github/workflows/cross_artifact_dependencies_check.sh +++ b/.github/workflows/cross_artifact_dependencies_check.sh @@ -22,6 +22,7 @@ TARGETS=( "//publish:cel_runtime" "//publish:cel_protobuf" "//publish:cel_v1alpha1" + "//publish:cel_verifier" ) echo "------------------------------------------------" diff --git a/MODULE.bazel b/MODULE.bazel index fcaf041ba..ce9c67fde 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -98,6 +98,7 @@ maven.install( "org.jspecify:jspecify:1.0.0", "org.threeten:threeten-extra:1.8.0", "org.yaml:snakeyaml:2.5", + "tools.aqua:z3-turnkey:4.14.1", ], repositories = [ "https://maven.google.com", diff --git a/publish/BUILD.bazel b/publish/BUILD.bazel index 69622aada..185c7fb7d 100644 --- a/publish/BUILD.bazel +++ b/publish/BUILD.bazel @@ -130,6 +130,18 @@ BUNDLE_TARGETS = [ CEL_MISC_TARGETS = BUNDLE_TARGETS + EXTENSION_TARGETS + OPTIMIZER_TARGETS + VALIDATOR_TARGETS + POLICY_COMPILER_TARGETS +# keep sorted +VERIFIER_TARGETS = [ + "//verifier/src/main/java/dev/cel/verifier", + "//verifier/src/main/java/dev/cel/verifier:policy_verifier", + "//verifier/src/main/java/dev/cel/verifier:policy_verifier_factory", + "//verifier/src/main/java/dev/cel/verifier:policy_verifier_impl", + "//verifier/src/main/java/dev/cel/verifier:type_system", + "//verifier/src/main/java/dev/cel/verifier:verifier_factory", + "//verifier/src/main/java/dev/cel/verifier:z3_impl", + "//verifier/src/main/java/dev/cel/verifier/axioms", +] + # Excluded from the JAR as their source of truth is elsewhere EXCLUDED_TARGETS = [ "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", @@ -316,3 +328,24 @@ java_export( pom_template = ":cel_runtime_android_pom", exports = LITE_RUNTIME_TARGETS, ) + +pom_file( + name = "cel_verifier_pom", + substitutions = { + "CEL_VERSION": CEL_VERSION, + "CEL_ARTIFACT_ID": "verifier", + "PACKAGE_NAME": "CEL Java Verifier", + "PACKAGE_DESC": "Formal verification tools for Common Expression Language for Java.", + }, + targets = VERIFIER_TARGETS, + template_file = "pom_template.xml", +) + +java_export( + name = "cel_verifier", + deploy_env = EXCLUDED_TARGETS, + javadocopts = JAVA_DOC_OPTIONS, + maven_coordinates = "dev.cel:verifier:%s" % CEL_VERSION, + pom_template = ":cel_verifier_pom", + exports = VERIFIER_TARGETS + [":cel"], +) diff --git a/verifier/README.md b/verifier/README.md new file mode 100644 index 000000000..a5889d5db --- /dev/null +++ b/verifier/README.md @@ -0,0 +1,304 @@ +# CEL Java Verifier + +The **CEL Java Verifier** is a formal verification framework for CEL Java, +designed to statically prove semantic properties of CEL expressions and +policies. + +Powered by the [Z3 SMT solver](https://github.com/Z3Prover/z3), the verifier +translates CEL Abstract Syntax Trees (ASTs) into mathematical formulas to prove +equivalence, satisfiability, and validity without executing the expressions. + +--- + +## Overview + +CEL is side-effect free with guaranteed termination, but as expressions grow in +complexity, ensuring correctness under all possible inputs becomes challenging. +The CEL Verifier addresses this by allowing you to mathematically prove +properties about your expressions. + +### Common Use Cases + +* **Compliance Auditing:** Statically prove that a critical resource is + mathematically protected (e.g., "prove that access is never allowed unless + `request.auth.claims.role == 'admin'`"). +* **Optimization & Safe Refactoring:** Verify that a simplified or optimized + version of an expression behaves identically to the original version for + all possible inputs. +* **Dead Code Detection:** Identify branches in an expression that can never + be reached (are unsatisfiable). + +--- + +## Key Features + +* **Satisfiability & Validity Proving:** Check if an expression can ever + evaluate to `true` (satisfiability) or if it is guaranteed to always be + `true` (validity). +* **Logical Equivalence:** Prove that two different ASTs or Policies are + semantically identical. +* **Bounded Model Checking (BMC):** Safely verify list and map comprehensions + (`all`, `exists`, `map`, `filter`) by statically unrolling them up to a + configurable limit. +* **Counterexample Generation:** When verification fails (e.g., two + expressions are not equivalent), the verifier generates a human-readable + counterexample showing the inputs that caused the mismatch. +* **Partial Evaluation (Unknowns) Support:** Define variables that are + permitted to evaluate to `Unknown` during verification, mirroring CEL's + runtime partial evaluation. + +```java +CelVerifier verifier = CelVerifierFactory.newVerifier() + .addUnknownIdentifier("request.headers") // Exclude dynamic fields from failure paths + .build(); +``` + +--- + +## Upcoming Capabilities + +The following features are planned for future releases: + +* **Custom Invariants Verification:** Allows policy authors to define safety + invariants (e.g., "port must always be secure if external access is + allowed") and mathematically prove that the policy never violates them. +* **Deep Reachability Analysis:** Statically analyzes nested policy rules + to detect unreachable execution paths (dead code) that can never be + executed under any input. +* **Rule Shadowing & Independence Detection:** Detects when sibling rules + in a policy conflict or shadow each other (i.e., a rule is partially or + fully shadowed by a preceding rule with overlapping conditions), ensuring + deterministic and intended policy routing. + +--- + +## Usage + +### 1. AST Equivalence Verification + +The following example demonstrates how to verify if two CEL expressions +are logically equivalent. + +```java +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.types.SimpleType; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.verifier.CelVerificationException; +import dev.cel.verifier.CelVerificationResult; +import dev.cel.verifier.CelVerificationResult.VerificationStatus; +import dev.cel.verifier.CelVerifier; +import dev.cel.verifier.CelVerifierFactory; +import java.time.Duration; + +public class VerifierExample { + public static void main(String[] args) throws Exception { + CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder() + .addVar("x", SimpleType.INT) + .build(); + + // Compile two logically equivalent expressions + CelAbstractSyntaxTree astA = compiler.compile("x > 10").getAst(); + CelAbstractSyntaxTree astB = compiler.compile("10 < x").getAst(); + + // Create and configure the verifier + CelVerifier verifier = CelVerifierFactory.newVerifier() + .setTimeout(Duration.ofSeconds(2)) + .build(); + + CelVerificationResult result; + try { + // Verify equivalence + result = verifier.verifyEquivalence(astA, astB); + } catch (CelVerificationException e) { + System.out.println("Verification failed or timed out: " + e.getMessage()); + return; + } + + if (result.status() == VerificationStatus.VERIFIED) { + System.out.println("Expressions are logically equivalent!"); + } else if (result.status() == VerificationStatus.VIOLATED) { + System.out.println(result.message()); + // Example output if expressions were NOT equivalent: + // Equivalence violation detected. Counterexample input: + // x = ... + } else { + // INCONCLUSIVE means the solver could not positively confirm VERIFIED or VIOLATED + // due to things like loop truncation (BMC) or uninterpreted functions. + System.out.println("Verification was inconclusive: " + result.message()); + } + } +} +``` + +### 2. Policy Equivalence Verification + +You can also verify the equivalence of two structured CEL Policies. + +```java +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelFactory; +import dev.cel.common.types.SimpleType; +import dev.cel.policy.CelPolicy; +import dev.cel.policy.CelPolicyCompiler; +import dev.cel.policy.CelPolicyCompilerFactory; +import dev.cel.policy.CelPolicyParser; +import dev.cel.policy.CelPolicyParserFactory; +import dev.cel.verifier.CelPolicyVerifier; +import dev.cel.verifier.CelPolicyVerifierFactory; +import dev.cel.verifier.CelVerificationException; +import dev.cel.verifier.CelVerificationResult; +import dev.cel.verifier.CelVerificationResult.VerificationStatus; +import dev.cel.verifier.CelVerifier; +import dev.cel.verifier.CelVerifierFactory; + +public class PolicyVerifierExample { + private static final Cel CEL = CelFactory.standardCelBuilder() + .addVar("role", SimpleType.STRING) + .addVar("country", SimpleType.STRING) + .addVar("port", SimpleType.INT) + .build(); + + private static final CelPolicyParser PARSER = CelPolicyParserFactory.newYamlParserBuilder() + .enableSimpleVariables(true) + .build(); + + private static final CelPolicyCompiler POLICY_COMPILER = + CelPolicyCompilerFactory.newPolicyCompiler(CEL).build(); + + // Verifiers are immutable and thread-safe, making them safe to store as static constants + private static final CelVerifier AST_VERIFIER = CelVerifierFactory.newVerifier().build(); + + private static final CelPolicyVerifier POLICY_VERIFIER = + CelPolicyVerifierFactory.newVerifier(POLICY_COMPILER, AST_VERIFIER).build(); + + public static void main(String[] args) throws Exception { + // Define legacy policy (single complex expression) + String yamlLegacy = """ + name: legacy_authz + rule: + match: + - output: '(role == "admin" || (role == "editor" && country == "US")) && port == 443' + """; + + // Define refactored policy with a bug (missing country check) + String yamlRefactored = """ + name: refactored_authz + rule: + variables: + - is_admin: 'role == "admin"' + - is_editor: 'role == "editor"' # Bug: Missing country == 'US' check! + - is_secure: 'port == 443' + match: + - output: '(variables.is_admin || variables.is_editor) && variables.is_secure' + """; + + // Parse both policies + CelPolicy policyLegacy = PARSER.parse(yamlLegacy); + CelPolicy policyRefactored = PARSER.parse(yamlRefactored); + + CelVerificationResult result; + try { + // Verify equivalence + result = POLICY_VERIFIER.verifyEquivalence(policyLegacy, policyRefactored); + } catch (CelVerificationException e) { + System.out.println("Verification failed or timed out: " + e.getMessage()); + return; + } + + if (result.status() == VerificationStatus.VERIFIED) { + System.out.println("Policies are equivalent!"); + } else if (result.status() == VerificationStatus.VIOLATED) { + System.out.println("Refactoring bug detected!"); + System.out.println(result.message()); + // Output: + // Equivalence violation detected. Counterexample input: + // country = "a" + // role = "editor" + // port = 443 + } else { + System.out.println("Verification was inconclusive: " + result.message()); + } + } +} +``` + +--- + +## Limitations & Best Practices + +### Limitations + +* **Cross-Type Numeric Comparisons:** + * **Equality (`==`, `!=`):** Equality comparisons between different + numeric types (e.g., `int` vs `double` or `uint` vs `double`) are + currently not supported and will evaluate to `false` during + verification, even if they have the same mathematical value (e.g., + `dyn(1) == dyn(1.0)` is false). Note that `int` vs `uint` equality *is* + supported. + * **Relational Operators (`<`, `>`, `<=`, `>=`):** Cross-type relational + comparisons are fully supported mathematically across all numeric + combinations (`int`, `uint`, and `double`). +* **Unsupported Standard Functions (Uninterpreted Functions):** Not all + CEL standard library functions have SMT axioms defined yet. Unsupported + functions are treated as *uninterpreted functions* by Z3 (the solver + only guarantees that identical inputs yield identical outputs, but does + not understand the function's internal logic or return types). + Consequently, verifications that rely on the specific semantics of + these functions may return `VerificationStatus.INCONCLUSIVE`. Support for + more standard library functions will be added incrementally. + +### Best Practices & Performance + +* **Prefer Strong Typing over `dyn`:** Always declare variables with + specific concrete types (e.g., `int`, `string`, `bool`, or specific + protobuf message types). Omitting the type or using `dyn` forces the + verifier to perform expensive runtime type checks symbolically across + multiple theories, which significantly slows down verification. Using + concrete types allows Z3 to use specialized solvers directly for much + faster results. +* **Avoid Floating Point Numbers:** Using floating point numbers in your + CEL expressions can significantly increase the time it takes for the + verifier to produce a result. It is recommended to use integers for all + counting and comparison logic unless floating point precision is + explicitly required. + +--- + +## Configuration & Tuning + +### Timeouts + +SMT solving is NP-complete and can theoretically stop responding or take an +exponential amount of time for complex formulas. +The verifier uses a default timeout of 10 seconds. It is recommended to +configure this to a reasonable duration for your specific use case using +`setTimeout(Duration)`. Note that this is a soft timeout evaluated periodically +by the Z3 solver during its search phase. +If the solver times out, the verifier will throw a `CelVerificationException` +which should be explicitly caught and handled by the caller. + +### Comprehensions and Bounded Model Checking + +Because CEL lists/maps can be dynamically sized, the verifier cannot +statically evaluate loops of infinite or unknown size. +To handle comprehensions (`all`, `exists`, `map`, `filter`), the verifier uses +Bounded Model Checking (BMC) to statically unroll loops up to a limit +configured via `setComprehensionUnrollLimit(int)` (defaults to 5). + +What this means for verification: + +* **Equivalence Checking:** Works seamlessly out-of-the-box for refactored + policies containing matching comprehensions, as both sides are unrolled + to the same limit. +* **Inconclusive Verification:** Expressions with comprehensions over + unconstrained dynamic lists will safely evaluate to `Unknown` for + inputs exceeding the unroll limit. If the verifier cannot definitively + prove or disprove a property for all possible list sizes (e.g., when + checking `isAlwaysTrue` or `isSatisfiable`), it will return + `VerificationStatus.INCONCLUSIVE`. +* **Warning:** Setting the unroll limit too high will exponentially + increase verification time and memory usage. Prefer keeping it near the + default unless you have a specific need and bounded inputs. + +--- diff --git a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel index 09a2039b4..6cdb793db 100644 --- a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel +++ b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel @@ -2,7 +2,10 @@ load("@rules_java//java:defs.bzl", "java_library") package( default_applicable_licenses = ["//:license"], - default_visibility = ["//verifier:__pkg__"], + default_visibility = [ + "//publish:__pkg__", + "//verifier:__pkg__", + ], ) java_library( @@ -19,6 +22,7 @@ java_library( "//:auto_value", "//common:cel_ast", "//common/types:type_providers", + "@maven//:com_google_errorprone_error_prone_annotations", ], ) @@ -119,6 +123,7 @@ java_library( "//verifier/axioms", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", "@maven//:tools_aqua_z3_turnkey", ], ) diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/BUILD.bazel b/verifier/src/main/java/dev/cel/verifier/axioms/BUILD.bazel index d5f9bf74a..02752c10e 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/BUILD.bazel +++ b/verifier/src/main/java/dev/cel/verifier/axioms/BUILD.bazel @@ -2,7 +2,10 @@ load("@rules_java//java:defs.bzl", "java_library") package( default_applicable_licenses = ["//:license"], - default_visibility = ["//verifier/axioms:__pkg__"], + default_visibility = [ + "//publish:__pkg__", + "//verifier/axioms:__pkg__", + ], ) java_library( From b3d11580997e25453b9cce4dde62123c233ba629 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 20 Jul 2026 12:11:31 -0700 Subject: [PATCH 127/204] Support cross-type numeric equality in verifier PiperOrigin-RevId: 950980672 --- verifier/README.md | 10 - .../cel/verifier/CelAstToZ3Translator.java | 2 +- .../cel/verifier/CelZ3OperatorTranslator.java | 282 ++++++++++++++---- .../dev/cel/verifier/CelZ3TypeSystem.java | 4 - .../dev/cel/verifier/axioms/TypeAxiom.java | 33 +- .../verifier/axioms/TypeConversionAxioms.java | 3 +- .../verifier/CelPolicyVerifierImplTest.java | 2 +- .../cel/verifier/CelVerifierZ3ImplTest.java | 83 +++++- 8 files changed, 300 insertions(+), 119 deletions(-) diff --git a/verifier/README.md b/verifier/README.md index a5889d5db..c10ce06eb 100644 --- a/verifier/README.md +++ b/verifier/README.md @@ -229,16 +229,6 @@ public class PolicyVerifierExample { ### Limitations -* **Cross-Type Numeric Comparisons:** - * **Equality (`==`, `!=`):** Equality comparisons between different - numeric types (e.g., `int` vs `double` or `uint` vs `double`) are - currently not supported and will evaluate to `false` during - verification, even if they have the same mathematical value (e.g., - `dyn(1) == dyn(1.0)` is false). Note that `int` vs `uint` equality *is* - supported. - * **Relational Operators (`<`, `>`, `<=`, `>=`):** Cross-type relational - comparisons are fully supported mathematically across all numeric - combinations (`int`, `uint`, and `double`). * **Unsupported Standard Functions (Uninterpreted Functions):** Not all CEL standard library functions have SMT axioms defined yet. Unsupported functions are treated as *uninterpreted functions* by Z3 (the solver diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java index 0562af693..54049a1b5 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java @@ -564,7 +564,7 @@ private TranslatedValue translateSelect(CelExpr celExpr, CelAbstractSyntaxTree a typeConstraints.add(createTypeConstraint(fieldAccess, exprId, ast)); return TranslatedValue.propagateStrict( - ctx, typeSystem, fieldAccess, celExpr, Arrays.asList(operandTv)); + ctx, typeSystem, fieldAccess, celExpr, ImmutableList.of(operandTv)); } private TranslatedValue translateBlock( diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java index e2df87b2a..67a540a26 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java @@ -23,7 +23,9 @@ import com.microsoft.z3.Context; import com.microsoft.z3.Expr; import com.microsoft.z3.FPExpr; +import com.microsoft.z3.FuncDecl; import com.microsoft.z3.IntExpr; +import com.microsoft.z3.IntNum; import com.microsoft.z3.SeqExpr; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelFunctionDecl; @@ -223,9 +225,9 @@ private TranslatedValue translateOperatorCall( case LOGICAL_NOT: return translateLogicalNot(args, ast); case EQUALS: - return translateEquals(args.get(0), args.get(1), ast); + return translateEquality(args.get(0), args.get(1), ast, /* isEquals= */ true); case NOT_EQUALS: - return translateNotEquals(args.get(0), args.get(1), ast); + return translateEquality(args.get(0), args.get(1), ast, /* isEquals= */ false); case LESS: case GREATER: case LESS_EQUALS: @@ -422,9 +424,7 @@ private BoolExpr getNumericEquality( return getDynamicNumericEquality(arg0.z3Expr(), arg1.z3Expr()); } - /** - * Evaluates numeric equality when types are statically known. - */ + /** Evaluates numeric equality when types are statically known. */ private BoolExpr getStaticallyKnownNumericEquality( Expr z3Expr0, CelType type0, Expr z3Expr1) { switch (type0.kind()) { @@ -440,22 +440,51 @@ private BoolExpr getStaticallyKnownNumericEquality( } } + private BoolExpr mkIsFiniteDouble(Expr z3Expr) { + Expr fpVal = typeSystem.getDouble(z3Expr); + return ctx.mkAnd( + typeSystem.isDouble(z3Expr), + ctx.mkNot(ctx.mkOr(ctx.mkFPIsNaN((FPExpr) fpVal), ctx.mkFPIsInfinite((FPExpr) fpVal)))); + } + private BoolExpr getDynamicNumericEquality(Expr z3Expr0, Expr z3Expr1) { BoolExpr isIntOrUint0 = ctx.mkOr(typeSystem.isInt(z3Expr0), typeSystem.isUint(z3Expr0)); BoolExpr isIntOrUint1 = ctx.mkOr(typeSystem.isInt(z3Expr1), typeSystem.isUint(z3Expr1)); BoolExpr bothIntOrUint = ctx.mkAnd(isIntOrUint0, isIntOrUint1); + // Fall back to 0 if the expression is neither an INT nor a UINT. + // This prevents Z3 from evaluating getUint() on a DOUBLE, which causes the OSS solver to enter + // an incomplete state. IntExpr val0 = (IntExpr) ctx.mkITE( - typeSystem.isInt(z3Expr0), typeSystem.getInt(z3Expr0), typeSystem.getUint(z3Expr0)); + typeSystem.isInt(z3Expr0), + typeSystem.getInt(z3Expr0), + ctx.mkITE(typeSystem.isUint(z3Expr0), typeSystem.getUint(z3Expr0), ctx.mkInt(0))); IntExpr val1 = (IntExpr) ctx.mkITE( - typeSystem.isInt(z3Expr1), typeSystem.getInt(z3Expr1), typeSystem.getUint(z3Expr1)); + typeSystem.isInt(z3Expr1), + typeSystem.getInt(z3Expr1), + ctx.mkITE(typeSystem.isUint(z3Expr1), typeSystem.getUint(z3Expr1), ctx.mkInt(0))); BoolExpr bothDouble = ctx.mkAnd(typeSystem.isDouble(z3Expr0), typeSystem.isDouble(z3Expr1)); + BoolExpr isIntOrUintAndDouble = ctx.mkAnd(isIntOrUint0, typeSystem.isDouble(z3Expr1)); + BoolExpr isDoubleAndIntOrUint = ctx.mkAnd(typeSystem.isDouble(z3Expr0), isIntOrUint1); + + Expr fpVal1 = typeSystem.getDouble(z3Expr1); + BoolExpr intDoubleEq = + ctx.mkAnd( + mkIsFiniteDouble(z3Expr1), + ctx.mkEq(ctx.mkInt2Real(val0), ctx.mkFPToReal((FPExpr) fpVal1))); + + Expr fpVal0 = typeSystem.getDouble(z3Expr0); + BoolExpr doubleIntEq = + ctx.mkAnd( + mkIsFiniteDouble(z3Expr0), + ctx.mkEq(ctx.mkFPToReal((FPExpr) fpVal0), ctx.mkInt2Real(val1))); + return (BoolExpr) CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) .addCase(bothIntOrUint, ctx.mkEq(val0, val1)) @@ -463,6 +492,8 @@ private BoolExpr getDynamicNumericEquality(Expr z3Expr0, Expr z3Expr1) { bothDouble, ctx.mkFPEq( (FPExpr) typeSystem.getDouble(z3Expr0), (FPExpr) typeSystem.getDouble(z3Expr1))) + .addCase(isIntOrUintAndDouble, intDoubleEq) + .addCase(isDoubleAndIntOrUint, doubleIntEq) .build(ctx.mkFalse()); } @@ -487,7 +518,7 @@ private BoolExpr unrollListEquality( TranslatedValue elemB = TranslatedValue.create(elem1, listB.listElementAt(i), typeSystem, listB.isApproximate()); - TranslatedValue elemEquality = translateEquals(elemA, elemB, ast); + TranslatedValue elemEquality = translateEquality(elemA, elemB, ast, /* isEquals= */ true); Expr eqZ3 = elemEquality.z3Expr(); BoolExpr isBool = typeSystem.isBool(eqZ3); @@ -500,14 +531,12 @@ private BoolExpr unrollListEquality( } private TranslatedValue translateEquality( - TranslatedValue arg0, TranslatedValue arg1, CelAbstractSyntaxTree ast, boolean isNotEquals) { + TranslatedValue arg0, TranslatedValue arg1, CelAbstractSyntaxTree ast, boolean isEquals) { Expr z3Arg0 = arg0.z3Expr(); Expr z3Arg1 = arg1.z3Expr(); - CelType type0 = - arg0.celExpr().map(node -> ast.getTypeOrThrow(node.id())).orElse(SimpleType.DYN); - CelType type1 = - arg1.celExpr().map(node -> ast.getTypeOrThrow(node.id())).orElse(SimpleType.DYN); + CelType type0 = extractAstTypeOrDefault(arg0, ast); + CelType type1 = extractAstTypeOrDefault(arg1, ast); BoolExpr equality; @@ -520,7 +549,8 @@ private TranslatedValue translateEquality( } else if (isStaticallyKnown(type0) && isStaticallyKnown(type1)) { equality = typeSystem.getStructuralEquality(z3Arg0, z3Arg1); } else { - BoolExpr bothNumeric = ctx.mkAnd(isNumeric(z3Arg0), isNumeric(z3Arg1)); + boolean canBeNumeric0 = type0.kind() == CelKind.DYN || isNumericType(type0); + boolean canBeNumeric1 = type1.kind() == CelKind.DYN || isNumericType(type1); // Check if one side is an explicit LIST that we can unroll BoolExpr structuralEq = typeSystem.getStructuralEquality(z3Arg0, z3Arg1); @@ -533,11 +563,16 @@ private TranslatedValue translateEquality( structuralEq); } - equality = - (BoolExpr) ctx.mkITE(bothNumeric, getNumericEquality(arg0, arg1, ast), structuralEq); + if (canBeNumeric0 && canBeNumeric1) { + BoolExpr bothNumeric = ctx.mkAnd(isNumeric(z3Arg0), isNumeric(z3Arg1)); + equality = + (BoolExpr) ctx.mkITE(bothNumeric, getNumericEquality(arg0, arg1, ast), structuralEq); + } else { + equality = structuralEq; + } } - if (isNotEquals) { + if (!isEquals) { equality = ctx.mkNot(equality); } @@ -550,42 +585,16 @@ private TranslatedValue translateEquality( return TranslatedValue.create(finalResult, typeSystem, ctx.mkFalse()); } - BoolExpr bothIntOrUint = - ctx.mkAnd( - ctx.mkOr(typeSystem.isInt(z3Arg0), typeSystem.isUint(z3Arg0)), - ctx.mkOr(typeSystem.isInt(z3Arg1), typeSystem.isUint(z3Arg1))); - BoolExpr bothDouble = ctx.mkAnd(typeSystem.isDouble(z3Arg0), typeSystem.isDouble(z3Arg1)); - BoolExpr bothNumeric = ctx.mkAnd(isNumeric(z3Arg0), isNumeric(z3Arg1)); - BoolExpr sameNumericType = ctx.mkOr(bothIntOrUint, bothDouble); - - // crossNumeric equality is only an approximation if we rely on getDynamicNumericEquality. - // getNumericEqualityWithConstant is exact because it evaluates heterogeneous numeric equality - // accurately. - BoolExpr crossNumeric; - if (arg0.isNumericConstant() || arg1.isNumericConstant()) { - crossNumeric = ctx.mkFalse(); - } else { - crossNumeric = ctx.mkAnd(bothNumeric, ctx.mkNot(sameNumericType)); - } - return TranslatedValue.propagateStrict(ctx, typeSystem, equalityExpr, arg0, arg1) - .withApproximation(crossNumeric); - } - - private TranslatedValue translateEquals( - TranslatedValue arg0, TranslatedValue arg1, CelAbstractSyntaxTree ast) { - return translateEquality(arg0, arg1, ast, false); - } - - private TranslatedValue translateNotEquals( - TranslatedValue arg0, TranslatedValue arg1, CelAbstractSyntaxTree ast) { - return translateEquality(arg0, arg1, ast, true); + // Mathematically redundant, but needed to prevent exponentially branching Z3 logic tree of + // mkOr tracking exact unknowns and errors + .withApproximation(ctx.mkFalse()); } - private Expr buildListIndex(Expr lhsTrans, Expr rhsTrans) { + private Expr buildListIndex(Expr lhsTrans, Expr rhsTrans, BoolExpr typeGuard) { Expr listRef = typeSystem.getListRef(lhsTrans); SeqExpr seq = typeSystem.getSeq(listRef); - Expr index = ctx.mkApp(typeSystem.intCons().getAccessorDecls()[0], rhsTrans); + Expr index = typeSystem.getInt(rhsTrans); BoolExpr inBounds = ctx.mkAnd( ctx.mkGe((ArithExpr) index, ctx.mkInt(0)), @@ -593,30 +602,175 @@ private Expr buildListIndex(Expr lhsTrans, Expr rhsTrans) { Expr val = ctx.mkNth(seq, (ArithExpr) index); BoolExpr valNotError = ctx.mkNot(ctx.mkEq(val, typeSystem.mkError())); - constraintSink.accept(ctx.mkImplies(inBounds, valNotError)); + constraintSink.accept(ctx.mkImplies(ctx.mkAnd(typeGuard, inBounds), valNotError)); if (!allowUnknowns) { BoolExpr valNotUnknown = ctx.mkNot(typeSystem.isUnknown(val)); - constraintSink.accept(ctx.mkImplies(inBounds, valNotUnknown)); + constraintSink.accept(ctx.mkImplies(ctx.mkAnd(typeGuard, inBounds), valNotUnknown)); } return ctx.mkITE(inBounds, val, typeSystem.mkError()); } - private Expr buildMapIndex(Expr lhsTrans, Expr rhsTrans) { + private Optional extractIntNumSafe(Expr expr) { + Expr simplified = expr.simplify(); + + if (!simplified.isApp()) { + return Optional.empty(); + } + + FuncDecl decl = simplified.getFuncDecl(); + if (decl.equals(typeSystem.intCons().ConstructorDecl()) + || decl.equals(typeSystem.uintCons().ConstructorDecl())) { + return Optional.of(simplified.getArgs()[0]) + .filter(IntNum.class::isInstance) + .map(IntNum.class::cast) + .map(IntNum::getInt64); + } + + return Optional.empty(); + } + + private static final class ProbeResult { + final BoolExpr altInMap; + final Expr altVal; + + ProbeResult(BoolExpr altInMap, Expr altVal) { + this.altInMap = altInMap; + this.altVal = altVal; + } + } + + private ProbeResult createProbeResult( + BoolExpr inMapOrig, + Expr valOrig, + BoolExpr cond1, + Expr key1, + BoolExpr cond2, + Expr key2, + ArrayExpr mapPresence, + ArrayExpr mapValues) { + BoolExpr inMap1 = (BoolExpr) ctx.mkSelect(mapPresence, key1); + Expr val1 = ctx.mkSelect(mapValues, key1); + BoolExpr inMap2 = (BoolExpr) ctx.mkSelect(mapPresence, key2); + Expr val2 = ctx.mkSelect(mapValues, key2); + + BoolExpr altInMap = ctx.mkOr(inMapOrig, ctx.mkAnd(cond1, inMap1), ctx.mkAnd(cond2, inMap2)); + Expr altVal = + ctx.mkITE( + inMapOrig, + valOrig, + ctx.mkITE( + ctx.mkAnd(cond1, inMap1), + val1, + ctx.mkITE(ctx.mkAnd(cond2, inMap2), val2, valOrig))); + + return new ProbeResult(altInMap, altVal); + } + + private Expr buildMapIndex(Expr lhsTrans, Expr rhsTrans, BoolExpr typeGuard) { Expr mapRef = typeSystem.getMapRef(lhsTrans); ArrayExpr mapValues = (ArrayExpr) typeSystem.getMapValues(mapRef); ArrayExpr mapPresence = (ArrayExpr) typeSystem.getMapPresence(mapRef); - BoolExpr inMap = (BoolExpr) ctx.mkSelect(mapPresence, rhsTrans); - Expr val = ctx.mkSelect(mapValues, rhsTrans); - BoolExpr valNotError = ctx.mkNot(ctx.mkEq(val, typeSystem.mkError())); - constraintSink.accept(ctx.mkImplies(inMap, valNotError)); + BoolExpr inMapOrig = (BoolExpr) ctx.mkSelect(mapPresence, rhsTrans); + Expr valOrig = ctx.mkSelect(mapValues, rhsTrans); + + BoolExpr isInt = typeSystem.isInt(rhsTrans); + BoolExpr isUint = typeSystem.isUint(rhsTrans); + BoolExpr isDouble = typeSystem.isDouble(rhsTrans); + + // Common double key for both int and uint + FPExpr intUintFp; + Optional rhsNum = extractIntNumSafe(rhsTrans); + boolean hasExactDouble = rhsNum.isPresent(); + if (hasExactDouble) { + intUintFp = typeSystem.mkFpDouble((double) rhsNum.get()); + } else { + intUintFp = typeSystem.mkFpDouble(0.0); + } + Expr intUintDoubleKey = typeSystem.wrapDouble(intUintFp); + + // Int probes + IntExpr rawInt = (IntExpr) ctx.mkITE(isInt, typeSystem.getInt(rhsTrans), ctx.mkInt(0)); + BoolExpr intHasUint = ctx.mkGe(rawInt, ctx.mkInt(0)); + Expr intUintKey = typeSystem.wrapUint(rawInt); + + BoolExpr intHasDouble = hasExactDouble ? isInt : ctx.mkFalse(); + ProbeResult intProbe = + createProbeResult( + inMapOrig, + valOrig, + intHasUint, + intUintKey, + intHasDouble, + intUintDoubleKey, + mapPresence, + mapValues); + + // Uint probes + IntExpr rawUint = (IntExpr) ctx.mkITE(isUint, typeSystem.getUint(rhsTrans), ctx.mkInt(0)); + BoolExpr uintHasInt = ctx.mkLe(rawUint, ctx.mkInt(CelZ3TypeSystem.MAX_INT64)); + Expr uintIntKey = typeSystem.wrapInt(rawUint); + + BoolExpr uintHasDouble = hasExactDouble ? isUint : ctx.mkFalse(); + ProbeResult uintProbe = + createProbeResult( + inMapOrig, + valOrig, + uintHasInt, + uintIntKey, + uintHasDouble, + intUintDoubleKey, + mapPresence, + mapValues); + + // Double probes + FPExpr dVal = + (FPExpr) + (Expr) ctx.mkITE(isDouble, typeSystem.getDouble(rhsTrans), typeSystem.mkFpDouble(0.0)); + IntExpr dInt = ctx.mkReal2Int(ctx.mkFPToReal(dVal)); + BoolExpr doubleIsExact = + ctx.mkAnd( + isDouble, + ctx.mkNot(ctx.mkOr(ctx.mkFPIsNaN(dVal), ctx.mkFPIsInfinite(dVal))), + ctx.mkEq(ctx.mkInt2Real(dInt), ctx.mkFPToReal(dVal))); + Expr doubleIntKey = typeSystem.wrapInt(dInt); + Expr doubleUintKey = typeSystem.wrapUint(dInt); + BoolExpr doubleHasUint = ctx.mkAnd(doubleIsExact, ctx.mkGe(dInt, ctx.mkInt(0))); + ProbeResult doubleProbe = + createProbeResult( + inMapOrig, + valOrig, + doubleIsExact, + doubleIntKey, + doubleHasUint, + doubleUintKey, + mapPresence, + mapValues); + + BoolExpr finalInMap = + (BoolExpr) + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(isInt, intProbe.altInMap) + .addCase(isUint, uintProbe.altInMap) + .addCase(isDouble, doubleProbe.altInMap) + .build(inMapOrig); + + Expr finalVal = + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(isInt, intProbe.altVal) + .addCase(isUint, uintProbe.altVal) + .addCase(isDouble, doubleProbe.altVal) + .build(valOrig); + + BoolExpr valNotError = ctx.mkNot(ctx.mkEq(finalVal, typeSystem.mkError())); + constraintSink.accept(ctx.mkImplies(ctx.mkAnd(typeGuard, finalInMap), valNotError)); if (!allowUnknowns) { - BoolExpr valNotUnknown = ctx.mkNot(typeSystem.isUnknown(val)); - constraintSink.accept(ctx.mkImplies(inMap, valNotUnknown)); + BoolExpr valNotUnknown = ctx.mkNot(typeSystem.isUnknown(finalVal)); + constraintSink.accept(ctx.mkImplies(ctx.mkAnd(typeGuard, finalInMap), valNotUnknown)); } - return ctx.mkITE(inMap, val, typeSystem.mkError()); + return ctx.mkITE(finalInMap, finalVal, typeSystem.mkError()); } private TranslatedValue translateIndex(List args, CelAbstractSyntaxTree ast) { @@ -630,24 +784,24 @@ private TranslatedValue translateIndex(List args, CelAbstractSy Expr actualValue; if (lhsType.kind() == CelKind.LIST && rhsType.kind() == CelKind.INT) { - actualValue = buildListIndex(lhsTrans, rhsTrans); + actualValue = buildListIndex(lhsTrans, rhsTrans, ctx.mkTrue()); constraintSink.accept( ctx.mkImplies( ctx.mkNot(typeSystem.isError(actualValue)), typeConstraintGenerator.apply(actualValue, ((ListType) lhsType).elemType()))); } else if (lhsType.kind() == CelKind.MAP) { - actualValue = buildMapIndex(lhsTrans, rhsTrans); + actualValue = buildMapIndex(lhsTrans, rhsTrans, ctx.mkTrue()); constraintSink.accept( ctx.mkImplies( ctx.mkNot(typeSystem.isError(actualValue)), typeConstraintGenerator.apply(actualValue, ((MapType) lhsType).valueType()))); } else { + BoolExpr isListGuard = ctx.mkAnd(typeSystem.isList(lhsTrans), typeSystem.isInt(rhsTrans)); + BoolExpr isMapGuard = typeSystem.isMap(lhsTrans); actualValue = CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) - .addCase( - ctx.mkAnd(typeSystem.isList(lhsTrans), typeSystem.isInt(rhsTrans)), - buildListIndex(lhsTrans, rhsTrans)) - .addCase(typeSystem.isMap(lhsTrans), buildMapIndex(lhsTrans, rhsTrans)) + .addCase(isListGuard, buildListIndex(lhsTrans, rhsTrans, isListGuard)) + .addCase(isMapGuard, buildMapIndex(lhsTrans, rhsTrans, isMapGuard)) .build(typeSystem.mkError()); } diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java index 7bd6d0daf..c5eae2c76 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java @@ -769,10 +769,6 @@ private SwitchBuilder(Context ctx) { * *

Returns {@code false} if the list is empty. */ - public static BoolExpr mkOrFlattened(Context ctx, BoolExpr... args) { - return mkOrFlattened(ctx, Arrays.asList(args)); - } - public static BoolExpr mkOrFlattened(Context ctx, List args) { // Pruning true/false constants in Java is significantly faster than building // larger ASTs and letting Z3 process them natively. diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/TypeAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/TypeAxiom.java index 5b7fdba3a..fc4757686 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/TypeAxiom.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/TypeAxiom.java @@ -24,9 +24,7 @@ import dev.cel.common.types.OptionalType; import dev.cel.common.types.SimpleType; import dev.cel.verifier.CelZ3TypeSystem; -import java.util.List; import java.util.Optional; -import java.util.function.Consumer; /** Axiomatization for CEL's type() function. */ final class TypeAxiom { @@ -38,30 +36,21 @@ final class TypeAxiom { CelZ3FunctionAxiom.newBuilder(StandardFunction.TYPE.functionDecl()) .addOverloadTranslator( StandardFunction.Overload.InternalOperator.TYPE.celOverloadDecl(), - new CelZ3OverloadTranslator() { - @Override - public Optional translate( - Context ctx, - CelZ3TypeSystem typeSystem, - Consumer constraintSink, - List> unwrappedArgs, - List argApproximations) { - Preconditions.checkArgument(unwrappedArgs.size() == 1); - Preconditions.checkArgument(argApproximations.size() == 1); + (ctx, typeSystem, constraintSink, unwrappedArgs, argApproximations) -> { + Preconditions.checkArgument(unwrappedArgs.size() == 1); + Preconditions.checkArgument(argApproximations.size() == 1); - Expr val = unwrappedArgs.get(0); - BoolExpr argApprox = argApproximations.get(0); + Expr val = unwrappedArgs.get(0); + BoolExpr argApprox = argApproximations.get(0); - Expr result = getTypeExpression(ctx, typeSystem, val); + Expr result = getTypeExpression(ctx, typeSystem, val); - // Custom approximation logic for type(): it is only approximate if the argument - // is approximate AND the argument is an Error or Unknown. - BoolExpr isErrOrUnk = - ctx.mkOr(typeSystem.isError(val), typeSystem.isUnknown(val)); - BoolExpr typeApprox = ctx.mkAnd(argApprox, isErrOrUnk); + // Custom approximation logic for type(): it is only approximate if the argument + // is approximate AND the argument is an Error or Unknown. + BoolExpr isErrOrUnk = ctx.mkOr(typeSystem.isError(val), typeSystem.isUnknown(val)); + BoolExpr typeApprox = ctx.mkAnd(argApprox, isErrOrUnk); - return Optional.of(CelZ3OverloadResult.create(result, typeApprox)); - } + return Optional.of(CelZ3OverloadResult.create(result, typeApprox)); }) .build(); diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java b/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java index 741513daf..94bdf0651 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java @@ -95,8 +95,7 @@ final class TypeConversionAxioms { true) .addUnaryOverloadTranslator( Conversions.STRING_TO_DOUBLE.celOverloadDecl(), - createUninterpretedConversion(Conversions.STRING_TO_DOUBLE), - true) + createUninterpretedConversion(Conversions.STRING_TO_DOUBLE)) .build(); private static final CelZ3FunctionAxiom STRING_AXIOM = diff --git a/verifier/src/test/java/dev/cel/verifier/CelPolicyVerifierImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelPolicyVerifierImplTest.java index a5567c1f1..2bc500bde 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelPolicyVerifierImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelPolicyVerifierImplTest.java @@ -277,7 +277,7 @@ public void verifyEquivalence_violation_throws() throws Exception { assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); assertThat(result.message()) .containsMatch( - "Equivalence violation detected\\. Counterexample input:\\n x = (6|7|8|9|10)"); + "Equivalence violation detected\\. Counterexample input:\\n {2}x = (6|7|8|9|10)"); } @Test diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index 5c13cc8de..898604808 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -266,6 +266,7 @@ private enum IsAlwaysTrueTestCase { UINT_ARITHMETIC_ZERO("0u + 0u == 0u"), MAP_COMPREHENSION("{1: 2, 3: 4}.all(k, k > 0)"), NESTED_COMPREHENSIONS("[1, 2].all(x, [3, 4].all(y, x < y || y <= x))"), + CYCLIC_BIND_DOES_NOT_HANG("cel.bind(x, x, x) == x"), CEL_BIND_SHADOWING("cel.bind(x, 1, cel.bind(x, 2, x) + x) == 3"), CEL_BIND_TO_TRUE("cel.bind(x, true, !x) == false"), CEL_BIND_TO_FALSE("cel.bind(x, false, !x) == true"), @@ -460,7 +461,14 @@ private enum IsAlwaysTrueTestCase { HETEROGENEOUS_INT_UINT_GE("2 >= 1u"), HETEROGENEOUS_UINT_INT_GE("2u >= 1"), HETEROGENEOUS_INT_DOUBLE_PRECISION("9007199254740993 > 9007199254740992.0"), - CYCLIC_BIND_DOES_NOT_HANG("cel.bind(x, x, x) == x"), + HETEROGENEOUS_INT_EQ_DOUBLE( + "type(dyn_var) == int && type(dyn_var2) == double && dyn_var == 1 && dyn_var2 == 1.0 ?" + + " dyn_var == dyn_var2 : true"), + HETEROGENEOUS_INT_NEQ_DOUBLE( + "type(dyn_var) == int && type(dyn_var2) == double && dyn_var == 1 && dyn_var2 == 1.5 ?" + + " dyn_var != dyn_var2 : true"), + HETEROGENEOUS_INF_VS_INT("dyn_var == 9223372036854775807 ? dyn_var != 1.0 / 0.0 : true"), + HETEROGENEOUS_NAN_VS_INT("dyn_var == 1 ? dyn_var != 0.0 / 0.0 : true"), HETEROGENEOUS_INT_UINT_VARIABLE_EQ("unknown_var == 1u ? unknown_var == 1 : true"), HETEROGENEOUS_INT_UINT_VARIABLE_VARIABLE_EQ( "unknown_var == u && x == 1 && u == 1u ? unknown_var == x : true"), @@ -469,6 +477,32 @@ private enum IsAlwaysTrueTestCase { HETEROGENEOUS_INT_UINT_VARIABLE_NEQ("unknown_var == 2u ? unknown_var != 1 : true"), HETEROGENEOUS_DOUBLE_INT_OVERFLOW( "unknown_var == 9223372036854775807 ? unknown_var != 1e100 : true"), + HETEROGENEOUS_MAX_EXACT_INT("dyn(9007199254740992) == 9007199254740992.0"), + HETEROGENEOUS_MIN_EXACT_INT("dyn(-9007199254740992) == -9007199254740992.0"), + HETEROGENEOUS_INT_PRECISION_LOSS_POS("dyn(9007199254740993) != 9007199254740992.0"), + HETEROGENEOUS_INT_PRECISION_LOSS_NEG("dyn(-9007199254740993) != -9007199254740992.0"), + HETEROGENEOUS_UINT_PRECISION_LOSS("dyn(9007199254740993u) != 9007199254740992.0"), + HETEROGENEOUS_LONG_MAX_VS_DOUBLE("dyn(9223372036854775807) == 9223372036854775808.0"), + HETEROGENEOUS_LONG_MIN_VS_DOUBLE("dyn(-9223372036854775808) == -9223372036854775808.0"), + HETEROGENEOUS_UINT_MAX_VS_DOUBLE("dyn(18446744073709551615u) != 18446744073709551616.0"), + HETEROGENEOUS_DYNAMIC_PRECISION( + "type(dyn_var) == int && type(dyn_var2) == double && dyn_var == 9007199254740993 &&" + + " dyn_var2 == 9007199254740992.0 ? dyn_var != dyn_var2 : true"), + HETEROGENEOUS_DYNAMIC_TRANSITIVITY( + "type(dyn_var) == int && type(dyn_var2) == double && dyn_var == 1 && dyn_var2 == 1.0 ?" + + " dyn_var == dyn_var2 : true"), + HETEROGENEOUS_DYNAMIC_ZERO("type(dyn_var) == double && dyn_var == -0.0 ? dyn_var == 0 : true"), + HETEROGENEOUS_MAP_INT_KEY_DOUBLE_LOOKUP("{1: 'a'}[dyn(1.0)] == 'a'"), + HETEROGENEOUS_MAP_DOUBLE_KEY_INT_LOOKUP("{1.0: 'a'}[dyn(1)] == 'a'"), + HETEROGENEOUS_MAP_DOUBLE_KEY_UINT_LOOKUP("{1.0: 'a'}[dyn(1u)] == 'a'"), + HETEROGENEOUS_MAP_UINT_KEY_DOUBLE_LOOKUP("{1u: 'a'}[dyn(1.0)] == 'a'"), + HETEROGENEOUS_MAP_UINT_KEY_INT_LOOKUP_ZERO("{0u: 'a'}[dyn(0)] == 'a'"), + HETEROGENEOUS_MAP_UINT_KEY_DOUBLE_LOOKUP_ZERO("{0u: 'a'}[dyn(0.0)] == 'a'"), + HETEROGENEOUS_MAP_DOUBLE_KEY_UINT_LOOKUP_ZERO("{0.0: 'a'}[dyn(0u)] == 'a'"), + HETEROGENEOUS_MAP_INT_KEY_UINT_LOOKUP_ZERO("{0: 'a'}[dyn(0u)] == 'a'"), + HETEROGENEOUS_MAP_PRECISION_MISS( + "{9007199254740993: 'exact', 9007199254740992: 'rounded'}[dyn(9007199254740992.0)] ==" + + " 'rounded'"), DYNAMIC_LIST_RESOLVES_QUANTIFIER_LOOPS( "int_list == [1] ? !(int_list.all(x, int_list.exists(y, y == x + 1))) : true"), DYNAMIC_LIST_RESOLVES_PIGEONHOLE( @@ -493,10 +527,6 @@ private enum IsAlwaysTrueTestCase { "string_int_map == {'a': 1} ? string_int_map.map(k, k) == ['a'] : true"), UINT_SUBTRACT("3u - 2u == 1u"), DOUBLE_SUBTRACT("3.0 - 2.0 == 1.0"), - // TODO: Cross-type numeric equality evaluates to strictly false, causing this - // test's condition to become a tautology. - // CROSS_TYPE_DYNAMIC_EQUALITY_NOT_ALWAYS_UNEQUAL_INT_DOUBLE( - // "!(request == unknown_var && type(request) == int && type(unknown_var) == double)"), DYNAMIC_LIST_V2_ALL("int_list == [1, 2] ? int_list.all(i, v, v > 0 && i >= 0) : true"), DYNAMIC_LIST_V2_EXISTS("int_list == [1, 2] ? int_list.exists(i, v, i == 0 && v == 1) : true"), DYNAMIC_MAP_V2_ALL( @@ -603,6 +633,29 @@ private enum IsAlwaysTrueTestCase { LITERAL_LIST_INDEX("[1, 2][0] == 1"), NESTED_LIST_VARIABLES_EQUALITY( "nested_list == [[1]] && nested_list_2 == [[1]] ? nested_list == nested_list_2 : true"), + DYNAMIC_NUMERIC_EQUALITY_CROSS_TYPE_INT_DOUBLE( + "type(dyn_var) == int && type(dyn_var2) == double && dyn_var == 1 && dyn_var2 == 1.0 ?" + + " dyn_var == dyn_var2 : true"), + DYNAMIC_NUMERIC_EQUALITY_CROSS_TYPE_DOUBLE_INT( + "type(dyn_var) == double && type(dyn_var2) == int && dyn_var == 1.0 && dyn_var2 == 1 ?" + + " dyn_var == dyn_var2 : true"), + DYNAMIC_NUMERIC_EQUALITY_CROSS_TYPE_UINT_DOUBLE( + "type(dyn_var) == uint && type(dyn_var2) == double && dyn_var == 1u && dyn_var2 == 1.0 ?" + + " dyn_var == dyn_var2 : true"), + DYNAMIC_EQUALITY_NON_NUMERIC_WITH_DYN( + "role == 'admin' && dyn_var == 'admin' ? role == dyn_var : true"), + DYNAMIC_EQUALITY_DYN_WITH_NON_NUMERIC( + "dyn_var == 'admin' && role == 'admin' ? dyn_var == role : true"), + DYNAMIC_NUMERIC_EQUALITY_CROSS_TYPE_DOUBLE_UINT( + "type(dyn_var) == double && type(dyn_var2) == uint && dyn_var == 1.0 && dyn_var2 == 1u ?" + + " dyn_var == dyn_var2 : true"), + DYNAMIC_NUMERIC_EQUALITY_CROSS_TYPE_DYN_INT( + "type(dyn_var) == int && dyn_var == 5 && x == 5 ? dyn_var == x : true"), + DYNAMIC_NUMERIC_EQUALITY_CROSS_TYPE_DYN_UINT( + "type(dyn_var) == uint && dyn_var == 5u && u == 5u ? dyn_var == u : true"), + DYNAMIC_NUMERIC_EQUALITY_CROSS_TYPE_DYN_DOUBLE( + "type(dyn_var) == double && dyn_var == 5.0 && dyn_var2 == 5.0 && type(dyn_var2) == double ?" + + " dyn_var == dyn_var2 : true"), ; final String expr; @@ -838,8 +891,6 @@ public void verifyEquivalence_unconditionalError_failsVerification( + " input state)"); } - - @Test public void verifyEquivalence_dynamicMapEquality_enforcesExtensionalityOnPresentKeys() throws Exception { @@ -883,8 +934,6 @@ private enum MalformedAstTestCase { } } - - @Test public void isAlwaysTrue_malformedAst_throwsIllegalArgumentException( @TestParameter MalformedAstTestCase testCase) throws Exception { @@ -937,6 +986,10 @@ private enum IsAlwaysTrueViolationTestCase { "dyn(x) != dyn(u)", "Condition is not always true."), CROSS_TYPE_SYMBOLIC_EQUALITY_NOT_ALWAYS_UNEQUAL_UINT_INT( "dyn(u) != dyn(x)", "Condition is not always true."), + CROSS_TYPE_DYNAMIC_EQUALITY_NOT_ALWAYS_UNEQUAL_INT_DOUBLE( + "!(request == unknown_var && type(request) == int && type(unknown_var) == double)", + "Condition is not always true.", + "Counterexample input:"), DYNAMIC_MAP_ALL_VIOLATION( "string_int_map == {'a': 1, 'b': 2} ? string_int_map.all(k, k == 'a') : true", "Condition is not always true.", @@ -1404,8 +1457,8 @@ public void verifyEquivalence_violation_hasCounterexampleMessage() throws Except assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); String message = result.message(); assertThat(message).contains("Equivalence violation detected. Counterexample input:"); - assertThat(message).containsMatch(" x = -?\\d+"); - assertThat(message).containsMatch(" y = -?\\d+"); + assertThat(message).containsMatch(" {2}x = -?\\d+"); + assertThat(message).containsMatch(" {2}y = -?\\d+"); } @Test @@ -1420,7 +1473,6 @@ public void verifyEquivalence_divergesOnTernaryErrorSemantics() throws Exception assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); } - @Test public void verifyEquivalence_dynamicIndexingWithExplicitMap_hydratesMap() throws Exception { CelAbstractSyntaxTree astA = @@ -1433,7 +1485,7 @@ public void verifyEquivalence_dynamicIndexingWithExplicitMap_hydratesMap() throw assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); assertThat(result.message()).contains("string_int_list_map = {"); assertThat(result.message()).contains("\"a\": [1, 2]"); - assertThat(result.message()).containsMatch("\"b\": \\[3, [45]\\]"); + assertThat(result.message()).containsMatch("\"b\": \\[3, [45]]"); } @Test @@ -1605,7 +1657,8 @@ public void verifyEquivalence_counterexampleFormat( result .message() .matches( - "(?s).*Equivalence violation detected\\. Counterexample input:\n " + "(?s).*Equivalence violation detected\\. Counterexample input:\n" + + " {2}" + f + ".*")); assertWithMessage(result.message()).that(matched).isTrue(); @@ -1952,7 +2005,7 @@ public void addFunctionAxioms_duplicateFunctionNames_throwsException() throws Ex .addFunctionAxioms(ImmutableList.of(dummyAxiom1, dummyAxiom2)); IllegalArgumentException exception = - assertThrows(IllegalArgumentException.class, () -> builder.build()); + assertThrows(IllegalArgumentException.class, builder::build); assertThat(exception).hasMessageThat().contains("dummy_func"); } From 9617d142a9ba57cc02b812e12024005d7a831261 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 20 Jul 2026 14:04:56 -0700 Subject: [PATCH 128/204] Optimize SwitchBuilder to prune dead SMT branches PiperOrigin-RevId: 951041974 --- .../cel/verifier/CelZ3OperatorTranslator.java | 17 ++++++++------- .../dev/cel/verifier/CelZ3TypeSystem.java | 21 ++++++++++++++++++- .../dev/cel/verifier/TranslatedValue.java | 10 ++++----- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java index 67a540a26..ea5f1b74c 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java @@ -654,15 +654,16 @@ private ProbeResult createProbeResult( BoolExpr inMap2 = (BoolExpr) ctx.mkSelect(mapPresence, key2); Expr val2 = ctx.mkSelect(mapValues, key2); - BoolExpr altInMap = ctx.mkOr(inMapOrig, ctx.mkAnd(cond1, inMap1), ctx.mkAnd(cond2, inMap2)); + BoolExpr condMap1 = CelZ3TypeSystem.mkAndFlattened(ctx, cond1, inMap1); + BoolExpr condMap2 = CelZ3TypeSystem.mkAndFlattened(ctx, cond2, inMap2); + + BoolExpr altInMap = CelZ3TypeSystem.mkOrFlattened(ctx, inMapOrig, condMap1, condMap2); Expr altVal = - ctx.mkITE( - inMapOrig, - valOrig, - ctx.mkITE( - ctx.mkAnd(cond1, inMap1), - val1, - ctx.mkITE(ctx.mkAnd(cond2, inMap2), val2, valOrig))); + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(inMapOrig, valOrig) + .addCase(condMap1, val1) + .addCase(condMap2, val2) + .build(valOrig); return new ProbeResult(altInMap, altVal); } diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java index c5eae2c76..cf29d0207 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java @@ -723,7 +723,9 @@ public SeqExpr mkConcatSafe(Expr arg1, Expr arg2) { /** * Helper to build a chain of nested ITE (If-Then-Else) conditions. * - *

Conditions are evaluated in the order they are added. + *

Conditions are evaluated in the order they are added. Branches with {@code isFalse()} + * conditions are skipped, and redundant {@code ITE(condition, X, X)} creations are omitted to + * avoid allocating dead AST paths in Z3. */ public static final class SwitchBuilder { @@ -746,6 +748,10 @@ public static SwitchBuilder newBuilder(Context ctx) { @CanIgnoreReturnValue public SwitchBuilder addCase(BoolExpr condition, Expr value) { + // Skip branches that can never be hit (e.g. `isFalse()` probes). + if (condition.isFalse()) { + return this; + } cases.add(new SwitchCase(condition, value)); return this; } @@ -753,6 +759,10 @@ public SwitchBuilder addCase(BoolExpr condition, Expr value) { public Expr build(Expr defaultFallback) { Expr result = defaultFallback; for (SwitchCase c : Lists.reverse(cases)) { + // ITE(condition, X, X) simplifies to X; skip calling into native C++ Z3_mk_ite. + if (c.value.equals(result)) { + continue; + } result = ctx.mkITE(c.condition, c.value, result); } return result; @@ -764,6 +774,15 @@ private SwitchBuilder(Context ctx) { } } + /** + * Helper to construct a flattened logical OR expression to avoid deep left-leaning ASTs. + * + *

Returns {@code false} if the list is empty. + */ + public static BoolExpr mkOrFlattened(Context ctx, BoolExpr... args) { + return mkOrFlattened(ctx, Arrays.asList(args)); + } + /** * Helper to construct a flattened logical OR expression to avoid deep left-leaning ASTs. * diff --git a/verifier/src/main/java/dev/cel/verifier/TranslatedValue.java b/verifier/src/main/java/dev/cel/verifier/TranslatedValue.java index 919778270..032c9dcdc 100644 --- a/verifier/src/main/java/dev/cel/verifier/TranslatedValue.java +++ b/verifier/src/main/java/dev/cel/verifier/TranslatedValue.java @@ -194,12 +194,10 @@ static TranslatedValue propagateStrict( BoolExpr isSafe = CelZ3TypeSystem.mkOrFlattened( ctx, - Arrays.asList( - hasExactUnknown, - CelZ3TypeSystem.mkAndFlattened( - ctx, - Arrays.asList(hasExactError, CelZ3TypeSystem.mkNotFlattened(ctx, hasUnknown))), - CelZ3TypeSystem.mkNotFlattened(ctx, anyTaint))); + hasExactUnknown, + CelZ3TypeSystem.mkAndFlattened( + ctx, hasExactError, CelZ3TypeSystem.mkNotFlattened(ctx, hasUnknown)), + CelZ3TypeSystem.mkNotFlattened(ctx, anyTaint)); return create(finalResult, celExpr, ts, CelZ3TypeSystem.mkNotFlattened(ctx, isSafe)); } From 4cd8411a5fa0c794077ed0a8ec5da074dc912ca4 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 20 Jul 2026 14:46:20 -0700 Subject: [PATCH 129/204] Generate satisfiable model for isSatisfiable PiperOrigin-RevId: 951064191 --- .../cel/verifier/CelVerificationResult.java | 9 ++++- .../java/dev/cel/verifier/CelVerifier.java | 4 +- .../dev/cel/verifier/CelVerifierZ3Impl.java | 39 +++++++++++++++---- .../CelZ3CounterexampleGenerator.java | 20 ++++++++-- .../cel/verifier/CelVerifierZ3ImplTest.java | 37 ++++++++++++++++++ 5 files changed, 96 insertions(+), 13 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/CelVerificationResult.java b/verifier/src/main/java/dev/cel/verifier/CelVerificationResult.java index b7510ccf0..f243537e1 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelVerificationResult.java +++ b/verifier/src/main/java/dev/cel/verifier/CelVerificationResult.java @@ -34,8 +34,9 @@ public enum VerificationStatus { public abstract VerificationStatus status(); /** - * Returns a message detailing why the verification failed or was inconclusive (e.g., the - * counterexample input or truncation reason). Empty if status is VERIFIED. + * Returns a message detailing the outcome of the verification check, such as a counterexample + * input, satisfying model assignments, or truncation reason. May be empty if status is VERIFIED + * and no model inputs apply (e.g., when verifying isAlwaysTrue without counterexamples). */ public abstract String message(); @@ -43,6 +44,10 @@ static CelVerificationResult verified() { return new AutoValue_CelVerificationResult(VerificationStatus.VERIFIED, ""); } + static CelVerificationResult verified(String message) { + return new AutoValue_CelVerificationResult(VerificationStatus.VERIFIED, message); + } + static CelVerificationResult failed(String message) { return new AutoValue_CelVerificationResult(VerificationStatus.VIOLATED, message); } diff --git a/verifier/src/main/java/dev/cel/verifier/CelVerifier.java b/verifier/src/main/java/dev/cel/verifier/CelVerifier.java index 7394886cb..32bffa1d3 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelVerifier.java +++ b/verifier/src/main/java/dev/cel/verifier/CelVerifier.java @@ -22,7 +22,9 @@ public interface CelVerifier { /** - * Returns verified if there is at least one input combination where the AST evaluates to true. + * Returns verified if there is at least one input combination where the AST evaluates to true. If + * the expression is satisfiable and depends on input variables, the result message will contain a + * satisfying model (witness) with concrete variable assignments. * * @param ast The input expression to verify. Must be a type-checked AST. */ diff --git a/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java b/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java index 35d847d1b..9d8b87e07 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java +++ b/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java @@ -192,13 +192,21 @@ public CelVerificationResult verifyEquivalence( return CelVerificationResult.failed( "Equivalence violation detected." + getCounterexampleString( - ctx, translator.getTypeSystem(), result.model, /* isApproximate= */ false)); + ctx, + translator.getTypeSystem(), + result.model, + /* isApproximate= */ false, + /* isCounterexample= */ true)); case APPROXIMATE_MATCH: return CelVerificationResult.inconclusive( "Inconclusive: a divergence may exist, but it depends on approximations, missing" + " theories, or loop bounds." + getCounterexampleString( - ctx, translator.getTypeSystem(), result.model, /* isApproximate= */ true)); + ctx, + translator.getTypeSystem(), + result.model, + /* isApproximate= */ true, + /* isCounterexample= */ true)); case TRUNCATED: return CelVerificationResult.inconclusive( "Inconclusive: expressions are equivalent within the current loop unroll limit, but" @@ -250,8 +258,16 @@ private CelVerificationResult checkSatisfiability( ctx, translator.getTypeSystem(), result.model, - /* isApproximate= */ false)) - : CelVerificationResult.verified(); + /* isApproximate= */ false, + /* isCounterexample= */ true)) + : CelVerificationResult.verified( + "Condition is satisfiable." + + getCounterexampleString( + ctx, + translator.getTypeSystem(), + result.model, + /* isApproximate= */ false, + /* isCounterexample= */ false)); case APPROXIMATE_MATCH: String prefix = @@ -263,7 +279,11 @@ private CelVerificationResult checkSatisfiability( return CelVerificationResult.inconclusive( prefix + getCounterexampleString( - ctx, translator.getTypeSystem(), result.model, /* isApproximate= */ true)); + ctx, + translator.getTypeSystem(), + result.model, + /* isApproximate= */ true, + /* isCounterexample= */ searchForCounterexample)); case TRUNCATED: return CelVerificationResult.inconclusive( @@ -357,8 +377,13 @@ private Solver newSolver(Context ctx) { } private static String getCounterexampleString( - Context ctx, CelZ3TypeSystem typeSystem, Model model, boolean isApproximate) { - return CelZ3CounterexampleGenerator.generate(ctx, typeSystem, model, isApproximate); + Context ctx, + CelZ3TypeSystem typeSystem, + Model model, + boolean isApproximate, + boolean isCounterexample) { + return CelZ3CounterexampleGenerator.generate( + ctx, typeSystem, model, isApproximate, isCounterexample); } CelVerifierZ3Impl( diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java index 8dcc73435..5cea7468d 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java @@ -36,7 +36,11 @@ final class CelZ3CounterexampleGenerator { private CelZ3CounterexampleGenerator() {} static String generate( - Context ctx, CelZ3TypeSystem typeSystem, Model model, boolean isApproximate) { + Context ctx, + CelZ3TypeSystem typeSystem, + Model model, + boolean isApproximate, + boolean isCounterexample) { FuncDecl[] constDecls = model.getConstDecls(); List bindings = new ArrayList<>(); @@ -55,10 +59,17 @@ static String generate( } if (bindings.isEmpty()) { - return " (The expression fails unconditionally, regardless of input state)"; + return isCounterexample + ? " (The expression fails unconditionally, regardless of input state)" + : " (The expression is satisfiable unconditionally, regardless of input state)"; } - String prefix = isApproximate ? " Potential counterexample input:" : " Counterexample input:"; + String prefix; + if (isCounterexample) { + prefix = isApproximate ? " Potential counterexample input:" : " Counterexample input:"; + } else { + prefix = isApproximate ? " Potential satisfying input:" : " Satisfying input:"; + } return prefix + String.join("", bindings); } @@ -228,6 +239,9 @@ private static void extractKeys(Expr arrayExpr, List> keys) { if (++iterations > 100_000) { throw new IllegalStateException("Exceeded maximum number of extractKeys iterations."); } + if (!arrayExpr.isApp()) { + break; + } FuncDecl decl = arrayExpr.getFuncDecl(); String declName = decl.getName().toString(); diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index 898604808..f56c26cfc 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -172,6 +172,43 @@ public void isSatisfiable_success(@TestParameter IsSatisfiableTestCase testCase) assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); } + @Test + public void isSatisfiable_withVariable_returnsSatisfyingModel() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("x > 5").getAst(); + + CelVerificationResult result = VERIFIER.isSatisfiable(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + assertThat(result.message()).contains("Condition is satisfiable."); + assertThat(result.message()).contains("Satisfying input:"); + assertThat(result.message()).containsMatch("x = (?:[6-9]|[1-9]\\d+)"); + } + + @Test + public void isSatisfiable_unconditional_returnsUnconditionalMessage() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("1 + 1 == 2").getAst(); + + CelVerificationResult result = VERIFIER.isSatisfiable(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + assertThat(result.message()) + .isEqualTo( + "Condition is satisfiable. (The expression is satisfiable unconditionally, regardless" + + " of input state)"); + } + + @Test + public void isSatisfiable_approximate_returnsPotentialSatisfyingInput() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("int('123') == 123 ? x > 5 : false").getAst(); + + CelVerificationResult result = VERIFIER.isSatisfiable(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + assertThat(result.message()).contains("Inconclusive: a satisfying model may exist"); + assertThat(result.message()).contains("Potential satisfying input:"); + assertThat(result.message()).containsMatch("x = (?:[6-9]|[1-9]\\d+)"); + } + private enum IsSatisfiableInconclusiveTestCase { MASKED_BY_BMC("int_list == [1, 2, 3, 4, 5, 6] ? int_list.exists(x, x == 42) : false"), MASKED_BY_BMC_ALL("int_list == [1, 2, 3, 4, 5, 6] ? int_list.all(x, x > 0) : false"), From 8b7d909ffc203ca104ea97caaf224b0d66380e96 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 20 Jul 2026 14:54:25 -0700 Subject: [PATCH 130/204] Fix notStrictlyFalse semantics in verifier PiperOrigin-RevId: 951068000 --- .../main/java/dev/cel/verifier/CelZ3OperatorTranslator.java | 4 ++-- .../src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java index ea5f1b74c..f13411f28 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java @@ -848,8 +848,8 @@ private TranslatedValue translateConditional( private TranslatedValue translateNotStrictlyFalse(List args) { TranslatedValue arg = args.get(0); BoolExpr isFalse = ctx.mkAnd(arg.isZ3Bool(), ctx.mkNot((BoolExpr) arg.unwrapZ3Bool())); - return TranslatedValue.propagateStrict( - ctx, typeSystem, typeSystem.wrapBool(ctx.mkNot(isFalse)), arg); + return TranslatedValue.create( + typeSystem.wrapBool(ctx.mkNot(isFalse)), typeSystem, arg.isApproximate()); } private static CelType extractAstTypeOrDefault(TranslatedValue val, CelAbstractSyntaxTree ast) { diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index f56c26cfc..6d046b6b9 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -303,6 +303,7 @@ private enum IsAlwaysTrueTestCase { UINT_ARITHMETIC_ZERO("0u + 0u == 0u"), MAP_COMPREHENSION("{1: 2, 3: 4}.all(k, k > 0)"), NESTED_COMPREHENSIONS("[1, 2].all(x, [3, 4].all(y, x < y || y <= x))"), + COMPREHENSION_EXISTS_UNKNOWN_INITIAL_STEP("[1, 2].exists(x, x == 1 ? unknown_var > 0 : true)"), CYCLIC_BIND_DOES_NOT_HANG("cel.bind(x, x, x) == x"), CEL_BIND_SHADOWING("cel.bind(x, 1, cel.bind(x, 2, x) + x) == 3"), CEL_BIND_TO_TRUE("cel.bind(x, true, !x) == false"), From 077d9a9506bf939b5430c553b23d02793ce140d5 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 20 Jul 2026 18:13:14 -0700 Subject: [PATCH 131/204] Implement custom policy invariants verification Enables policy authors to declare custom logical invariants (`assume` preconditions and `assert` clauses) on `CelPolicy` definitions, mathematically verifying that properties hold across all possible input states. PiperOrigin-RevId: 951159491 --- .../main/java/dev/cel/policy/CelPolicy.java | 144 +++++++ .../dev/cel/policy/CelPolicyYamlParser.java | 145 +++++++ .../dev/cel/policy/PolicyParserContext.java | 3 + .../src/test/java/dev/cel/policy/BUILD.bazel | 4 +- .../cel/policy/CelPolicyYamlParserTest.java | 176 +++++++- .../policy/verification/flawed_policy.yaml | 26 ++ .../verification/multi_invariant_policy.yaml | 37 ++ .../restricted_destinations_policy.yaml | 63 +++ .../verification/secure_resource_access.yaml | 38 ++ .../workload_admission_fixed.yaml | 57 +++ .../workload_admission_flawed.yaml | 67 +++ verifier/BUILD.bazel | 1 + verifier/README.md | 203 +++++++-- .../main/java/dev/cel/verifier/BUILD.bazel | 10 + .../cel/verifier/CelAstToZ3Translator.java | 10 +- .../dev/cel/verifier/CelPolicyVerifier.java | 12 + .../cel/verifier/CelPolicyVerifierImpl.java | 97 +++++ .../java/dev/cel/verifier/CelVerifier.java | 2 + .../dev/cel/verifier/CelVerifierZ3Impl.java | 92 ++++ .../test/java/dev/cel/verifier/BUILD.bazel | 5 + .../verifier/CelPolicyVerifierImplTest.java | 392 +++++++++++++++++- .../cel/verifier/CelVerifierZ3ImplTest.java | 18 + .../dev/cel/verifier/VerifierTestHelper.java | 58 +++ 23 files changed, 1622 insertions(+), 38 deletions(-) create mode 100644 testing/src/test/resources/policy/verification/flawed_policy.yaml create mode 100644 testing/src/test/resources/policy/verification/multi_invariant_policy.yaml create mode 100644 testing/src/test/resources/policy/verification/restricted_destinations_policy.yaml create mode 100644 testing/src/test/resources/policy/verification/secure_resource_access.yaml create mode 100644 testing/src/test/resources/policy/verification/workload_admission_fixed.yaml create mode 100644 testing/src/test/resources/policy/verification/workload_admission_flawed.yaml create mode 100644 verifier/src/test/java/dev/cel/verifier/VerifierTestHelper.java diff --git a/policy/src/main/java/dev/cel/policy/CelPolicy.java b/policy/src/main/java/dev/cel/policy/CelPolicy.java index 19f6631d0..6756481df 100644 --- a/policy/src/main/java/dev/cel/policy/CelPolicy.java +++ b/policy/src/main/java/dev/cel/policy/CelPolicy.java @@ -15,6 +15,7 @@ package dev.cel.policy; import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.joining; import com.google.auto.value.AutoOneOf; import com.google.auto.value.AutoValue; @@ -53,6 +54,10 @@ public abstract class CelPolicy { public abstract ImmutableList imports(); + public abstract ImmutableList invariants(); + + public abstract ImmutableList verificationVariables(); + /** Creates a new builder to construct a {@link CelPolicy} instance. */ public static Builder newBuilder() { return new AutoValue_CelPolicy.Builder() @@ -74,6 +79,8 @@ public abstract static class Builder { public abstract Builder setDisplayName(ValueString displayName); + public abstract Rule rule(); + public abstract Builder setRule(Rule rule); public abstract Builder setPolicySource(CelPolicySource policySource); @@ -90,6 +97,14 @@ public List imports() { return Collections.unmodifiableList(importList); } + abstract ImmutableList invariants(); + + abstract ImmutableList.Builder invariantsBuilder(); + + abstract ImmutableList verificationVariables(); + + abstract ImmutableList.Builder verificationVariablesBuilder(); + public Map metadata() { return Collections.unmodifiableMap(metadata); } @@ -106,6 +121,24 @@ public Builder addImports(Collection values) { return this; } + @CanIgnoreReturnValue + public Builder addInvariant(Invariant value) { + invariantsBuilder().add(value); + return this; + } + + @CanIgnoreReturnValue + public Builder addVerificationVariable(Variable value) { + verificationVariablesBuilder().add(value); + return this; + } + + @CanIgnoreReturnValue + public Builder addVerificationVariables(Collection values) { + verificationVariablesBuilder().addAll(values); + return this; + } + @CanIgnoreReturnValue public Builder putMetadata(String key, Object value) { metadata.put(key, value); @@ -328,4 +361,115 @@ public static Import create(long id, ValueString name) { return new AutoValue_CelPolicy_Import(id, name); } } + + /** + * Invariant declares a required logical property that must hold true under specified + * preconditions. + */ + @AutoValue + public abstract static class Invariant { + public abstract long id(); + + public abstract ValueString invariantId(); + + public abstract Optional description(); + + public abstract ImmutableList assume(); + + public abstract ImmutableList assertClause(); + + public String assumeSourceString() { + if (assume().isEmpty()) { + return "true"; + } + if (assume().size() == 1) { + return assume().get(0).value(); + } + return assume().stream().map(v -> "(" + v.value() + ")").collect(joining(" && ")); + } + + public String assertSourceString() { + if (assertClause().isEmpty()) { + return "true"; + } + if (assertClause().size() == 1) { + return assertClause().get(0).value(); + } + return assertClause().stream().map(v -> "(" + v.value() + ")").collect(joining(" && ")); + } + + /** Builder for {@link Invariant}. */ + @AutoValue.Builder + public abstract static class Builder implements RequiredFieldsChecker { + public abstract Builder setId(long value); + + abstract Optional invariantId(); + + abstract ImmutableList assume(); + + abstract ImmutableList.Builder assumeBuilder(); + + abstract ImmutableList assertClause(); + + abstract ImmutableList.Builder assertClauseBuilder(); + + public abstract Builder setInvariantId(ValueString value); + + public abstract Builder setDescription(ValueString value); + + public Builder setAssume(ValueString value) { + return setAssume(ImmutableList.of(value)); + } + + abstract Builder setAssume(ImmutableList values); + + @CanIgnoreReturnValue + public Builder addAssume(ValueString value) { + assumeBuilder().add(value); + return this; + } + + @CanIgnoreReturnValue + public Builder addAssume(Iterable values) { + assumeBuilder().addAll(values); + return this; + } + + public Builder setAssertClause(ValueString value) { + return setAssertClause(ImmutableList.of(value)); + } + + abstract Builder setAssertClause(ImmutableList values); + + @CanIgnoreReturnValue + public Builder addAssertClause(ValueString value) { + assertClauseBuilder().add(value); + return this; + } + + @CanIgnoreReturnValue + public Builder addAssertClause(Iterable values) { + assertClauseBuilder().addAll(values); + return this; + } + + @Override + public ImmutableList requiredFields() { + return ImmutableList.of( + RequiredField.of("id", this::invariantId), + RequiredField.of( + "assert", + () -> + assertClause().isEmpty() + ? Optional.empty() + : Optional.of(assertClause().get(0)))); + } + + public abstract Invariant build(); + } + + public static Builder newBuilder(long id) { + return new AutoValue_CelPolicy_Invariant.Builder().setId(id); + } + } } diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java b/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java index 18b406af0..408c86247 100644 --- a/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java +++ b/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java @@ -15,6 +15,7 @@ package dev.cel.policy; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableSet.toImmutableSet; import static dev.cel.common.formats.YamlHelper.ERROR; import static dev.cel.common.formats.YamlHelper.assertRequiredFields; import static dev.cel.common.formats.YamlHelper.assertYamlType; @@ -28,6 +29,7 @@ import dev.cel.common.formats.YamlParserContextImpl; import dev.cel.common.internal.CelCodePointArray; import dev.cel.policy.CelPolicy.Import; +import dev.cel.policy.CelPolicy.Invariant; import dev.cel.policy.CelPolicy.Match; import dev.cel.policy.CelPolicy.Match.Result; import dev.cel.policy.CelPolicy.Variable; @@ -47,6 +49,8 @@ final class CelPolicyYamlParser implements CelPolicyParser { Match.newBuilder(0).setCondition(ERROR_VALUE).setResult(Result.ofOutput(ERROR_VALUE)).build(); private static final Variable ERROR_VARIABLE = Variable.newBuilder().setExpression(ERROR_VALUE).setName(ERROR_VALUE).build(); + private static final Invariant ERROR_INVARIANT = + Invariant.newBuilder(0).setInvariantId(ERROR_VALUE).setAssertClause(ERROR_VALUE).build(); private final TagVisitor tagVisitor; private final boolean enableSimpleVariables; @@ -137,17 +141,82 @@ public CelPolicy parsePolicy(PolicyParserContext ctx, Node node) { case "rule": policyBuilder.setRule(parseRule(ctx, policyBuilder, valueNode)); break; + case "verification": + parseVerification(policyBuilder, ctx, valueNode); + break; default: tagVisitor.visitPolicyTag(ctx, keyId, fieldName, valueNode, policyBuilder); break; } } + ImmutableSet ruleVarNames = + policyBuilder.rule().variables().stream() + .map(CelPolicy.Variable::name) + .filter(name -> !name.equals(ERROR_VALUE)) + .map(ValueString::value) + .collect(toImmutableSet()); + for (Variable verVar : policyBuilder.verificationVariables()) { + if (!verVar.name().equals(ERROR_VALUE) + && ruleVarNames.contains(verVar.name().value())) { + ctx.reportError( + verVar.name().id(), + "Duplicate variable name '" + + verVar.name().value() + + "' in verification.variables; already defined in rule.variables"); + } + } + return policyBuilder .setPolicySource(policySource.toBuilder().setPositionsMap(ctx.getIdToOffsetMap()).build()) .build(); } + private void parseVerification( + CelPolicy.Builder policyBuilder, PolicyParserContext ctx, Node node) { + long id = ctx.collectMetadata(node); + if (!assertYamlType(ctx, id, node, YamlNodeType.MAP)) { + return; + } + MappingNode mappingNode = (MappingNode) node; + for (NodeTuple nodeTuple : mappingNode.getValue()) { + Node key = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(key); + if (!assertYamlType(ctx, keyId, key, YamlNodeType.STRING, YamlNodeType.TEXT)) { + continue; + } + String fieldName = ((ScalarNode) key).getValue(); + Node valueNode = nodeTuple.getValueNode(); + switch (fieldName) { + case "invariants": { + long valueId = ctx.collectMetadata(valueNode); + if (!assertYamlType(ctx, valueId, valueNode, YamlNodeType.LIST)) { + continue; + } + SequenceNode invariantListNode = (SequenceNode) valueNode; + for (Node invariantNode : invariantListNode.getValue()) { + policyBuilder.addInvariant(parseInvariant(ctx, policyBuilder, invariantNode)); + } + break; + } + case "variables": { + long valueId = ctx.collectMetadata(valueNode); + if (!assertYamlType(ctx, valueId, valueNode, YamlNodeType.LIST)) { + continue; + } + SequenceNode variableListNode = (SequenceNode) valueNode; + for (Node varNode : variableListNode.getValue()) { + policyBuilder.addVerificationVariable(parseVariable(ctx, policyBuilder, varNode)); + } + break; + } + default: + ctx.reportError(keyId, "Unexpected key in verification block: " + fieldName); + break; + } + } + } + private void parseImports( CelPolicy.Builder policyBuilder, PolicyParserContext ctx, Node node) { long id = ctx.collectMetadata(node); @@ -409,6 +478,82 @@ private Variable parseVariableObject( return builder.build(); } + @Override + public CelPolicy.Invariant parseInvariant( + PolicyParserContext ctx, CelPolicy.Builder policyBuilder, Node node) { + long id = ctx.collectMetadata(node); + Invariant.Builder builder = Invariant.newBuilder(id); + if (!assertYamlType(ctx, id, node, YamlNodeType.MAP)) { + return ERROR_INVARIANT; + } + + MappingNode invariantMap = (MappingNode) node; + for (NodeTuple nodeTuple : invariantMap.getValue()) { + Node keyNode = nodeTuple.getKeyNode(); + long keyId = ctx.collectMetadata(keyNode); + if (!assertYamlType(ctx, keyId, keyNode, YamlNodeType.STRING, YamlNodeType.TEXT)) { + continue; + } + Node valueNode = nodeTuple.getValueNode(); + String keyName = ((ScalarNode) keyNode).getValue(); + switch (keyName) { + case "id": + builder.setInvariantId(ctx.newYamlString(valueNode)); + break; + case "description": + builder.setDescription(ctx.newYamlString(valueNode)); + break; + case "assume": { + if (!assertYamlType( + ctx, + ctx.collectMetadata(valueNode), + valueNode, + YamlNodeType.STRING, + YamlNodeType.TEXT, + YamlNodeType.LIST)) { + break; + } + if (valueNode instanceof SequenceNode) { + for (Node itemNode : ((SequenceNode) valueNode).getValue()) { + builder.addAssume(ctx.newSourceString(itemNode)); + } + } else { + builder.addAssume(ctx.newSourceString(valueNode)); + } + break; + } + case "assert": { + if (!assertYamlType( + ctx, + ctx.collectMetadata(valueNode), + valueNode, + YamlNodeType.STRING, + YamlNodeType.TEXT, + YamlNodeType.LIST)) { + break; + } + if (valueNode instanceof SequenceNode) { + for (Node itemNode : ((SequenceNode) valueNode).getValue()) { + builder.addAssertClause(ctx.newSourceString(itemNode)); + } + } else { + builder.addAssertClause(ctx.newSourceString(valueNode)); + } + break; + } + default: + ctx.reportError(keyId, "Unexpected key in invariant block: " + keyName); + break; + } + } + + if (!assertRequiredFields(ctx, id, builder.getMissingRequiredFieldNames())) { + return ERROR_INVARIANT; + } + + return builder.build(); + } + private ParserImpl( TagVisitor tagVisitor, boolean enableSimpleVariables, diff --git a/policy/src/main/java/dev/cel/policy/PolicyParserContext.java b/policy/src/main/java/dev/cel/policy/PolicyParserContext.java index 204bf591f..d06167139 100644 --- a/policy/src/main/java/dev/cel/policy/PolicyParserContext.java +++ b/policy/src/main/java/dev/cel/policy/PolicyParserContext.java @@ -16,6 +16,7 @@ import com.google.auto.value.AutoValue; import dev.cel.common.formats.ParserContext; +import dev.cel.policy.CelPolicy.Invariant; import dev.cel.policy.CelPolicy.Match; import dev.cel.policy.CelPolicy.Rule; import dev.cel.policy.CelPolicy.Variable; @@ -51,4 +52,6 @@ static NewPolicyMetadata create(CelPolicySource source, long id) { Match parseMatch(PolicyParserContext ctx, CelPolicy.Builder policyBuilder, T node); Variable parseVariable(PolicyParserContext ctx, CelPolicy.Builder policyBuilder, T node); + + Invariant parseInvariant(PolicyParserContext ctx, CelPolicy.Builder policyBuilder, T node); } diff --git a/policy/src/test/java/dev/cel/policy/BUILD.bazel b/policy/src/test/java/dev/cel/policy/BUILD.bazel index 5157e0c74..6a76cf3b0 100644 --- a/policy/src/test/java/dev/cel/policy/BUILD.bazel +++ b/policy/src/test/java/dev/cel/policy/BUILD.bazel @@ -8,9 +8,7 @@ package( java_library( name = "tests", testonly = True, - srcs = glob( - ["*.java"], - ), + srcs = glob(["*.java"]), data = [ "@cel_policy//conformance:testdata", ], diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java index 2a2c47a98..aaa30518a 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java @@ -22,6 +22,8 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.common.formats.ValueString; import dev.cel.policy.CelPolicy.Import; +import dev.cel.policy.CelPolicy.Invariant; +import dev.cel.policy.CelPolicy.Variable; import dev.cel.policy.PolicyTestHelper.TestYamlPolicy; import dev.cel.policy.testing.K8sTagHandler; import org.junit.Test; @@ -194,6 +196,115 @@ public void parseYamlPolicy_errors(@TestParameter PolicyParseErrorTestCase testC assertThat(e).hasMessageThat().isEqualTo(testCase.expectedErrorMessage); } + @Test + public void policyBuilder_addInvariant() { + Invariant invariant = + Invariant.newBuilder(1L) + .setInvariantId(ValueString.newBuilder().setValue("id").build()) + .setAssertClause(ValueString.newBuilder().setValue("true").build()) + .build(); + CelPolicy policy = + CelPolicy.newBuilder() + .setName(ValueString.of(0, "test")) + .setPolicySource(CelPolicySource.newBuilder("").build()) + .addInvariant(invariant) + .build(); + assertThat(policy.invariants()).containsExactly(invariant); + } + + @Test + public void parseYamlPolicy_invariants_success() throws Exception { + String policySource = + "name: 'policy_with_invariants'\n" + + "verification:\n" + + " invariants:\n" + + " - id: 'inv_1'\n" + + " description: 'invariant description'\n" + + " assume: 'true'\n" + + " assert: 'rule.result == true'"; + + CelPolicy policy = POLICY_PARSER.parse(policySource); + + assertThat(policy.invariants()).hasSize(1); + Invariant invariant = Iterables.getOnlyElement(policy.invariants()); + assertThat(invariant.invariantId().value()).isEqualTo("inv_1"); + assertThat(invariant.description().get().value()).isEqualTo("invariant description"); + assertThat(invariant.assume().get(0).value()).isEqualTo("true"); + assertThat(invariant.assertClause().get(0).value()).isEqualTo("rule.result == true"); + } + + @Test + public void parseYamlPolicy_invariants_multiClauseLists_success() throws Exception { + String policySource = + "name: 'policy_with_list_invariants'\n" + + "verification:\n" + + " invariants:\n" + + " - id: 'inv_multi'\n" + + " assume:\n" + + " - 'x > 0'\n" + + " - 'y > 0'\n" + + " assert:\n" + + " - 'rule.result == true'\n" + + " - 'x + y > 0'"; + + CelPolicy policy = POLICY_PARSER.parse(policySource); + + Invariant invariant = Iterables.getOnlyElement(policy.invariants()); + assertThat(invariant.assumeSourceString()).isEqualTo("(x > 0) && (y > 0)"); + assertThat(invariant.assertSourceString()).isEqualTo("(rule.result == true) && (x + y > 0)"); + } + + @Test + public void parseYamlPolicy_verificationVariables_success() throws Exception { + String policySource = + "name: 'policy_with_verification_vars'\n" + + "rule:\n" + + " variables:\n" + + " - name: rule_var\n" + + " expression: 'true'\n" + + "verification:\n" + + " variables:\n" + + " - name: ver_var\n" + + " expression: 'rule_var && true'\n" + + " invariants:\n" + + " - id: 'inv_var'\n" + + " assume: 'variables.ver_var'\n" + + " assert: 'rule.result == true'"; + + CelPolicy policy = POLICY_PARSER.parse(policySource); + + assertThat(policy.verificationVariables()).hasSize(1); + Variable var = Iterables.getOnlyElement(policy.verificationVariables()); + assertThat(var.name().value()).isEqualTo("ver_var"); + assertThat(var.expression().value()).isEqualTo("rule_var && true"); + } + + @Test + public void parseYamlPolicy_verificationVariables_duplicateWithRuleVariables_throwsError() { + String policySource = + "name: 'policy_with_dup_vars'\n" + + "rule:\n" + + " variables:\n" + + " - name: dup_var\n" + + " expression: 'true'\n" + + "verification:\n" + + " variables:\n" + + " - name: dup_var\n" + + " expression: 'false'\n" + + " invariants:\n" + + " - id: 'inv_dup'\n" + + " assume: 'variables.dup_var'\n" + + " assert: 'rule.result == true'"; + + CelPolicyValidationException e = + assertThrows(CelPolicyValidationException.class, () -> POLICY_PARSER.parse(policySource)); + assertThat(e) + .hasMessageThat() + .contains( + "Duplicate variable name 'dup_var' in verification.variables; already defined in" + + " rule.variables"); + } + private enum PolicyParseErrorTestCase { MALFORMED_YAML_DOCUMENT( "a:\na", @@ -400,7 +511,70 @@ private enum PolicyParseErrorTestCase { + "- foo: bar", "ERROR: :2:3: Invalid import key: foo, expected 'name'\n" + " | - foo: bar\n" - + " | ..^"); + + " | ..^"), + UNSUPPORTED_VERIFICATION_TAG( + "verification:\n" // + + " bad_key: true", + "ERROR: :2:3: Unexpected key in verification block: bad_key\n" + + " | bad_key: true\n" + + " | ..^"), + UNSUPPORTED_INVARIANT_TAG( + "verification:\n" // + + " invariants:\n" // + + " - id: foo\n" // + + " bad_inv_key: true\n" // + + " assert: 'true'", + "ERROR: :4:7: Unexpected key in invariant block: bad_inv_key\n" + + " | bad_inv_key: true\n" + + " | ......^"), + MISSING_INVARIANT_ID( + "verification:\n" // + + " invariants:\n" // + + " - assert: 'true'", + "ERROR: :3:7: Missing required attribute(s): id\n" + + " | - assert: 'true'\n" + + " | ......^"), + MISSING_INVARIANT_ASSERT( + "verification:\n" // + + " invariants:\n" // + + " - id: foo", + "ERROR: :3:7: Missing required attribute(s): assert\n" + + " | - id: foo\n" + + " | ......^"), + ILLEGAL_YAML_TYPE_ON_VERIFICATION_VALUE( + "verification: illegal\n", + "ERROR: :1:15: Got yaml node type tag:yaml.org,2002:str, wanted type(s)" + + " [tag:yaml.org,2002:map]\n" + + " | verification: illegal\n" + + " | ..............^"), + ILLEGAL_YAML_TYPE_ON_VERIFICATION_MAP_KEY( + "verification:\n" + " 1: foo", + "ERROR: :2:3: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:str !txt]\n" + + " | 1: foo\n" + + " | ..^"), + ILLEGAL_YAML_TYPE_ON_INVARIANTS_VALUE( + "verification:\n" + " invariants: illegal\n", + "ERROR: :2:15: Got yaml node type tag:yaml.org,2002:str, wanted type(s)" + + " [tag:yaml.org,2002:seq]\n" + + " | invariants: illegal\n" + + " | ..............^"), + ILLEGAL_YAML_TYPE_ON_INVARIANTS_LIST( + "verification:\n" + " invariants:\n" + " - illegal", + "ERROR: :3:7: Got yaml node type tag:yaml.org,2002:str, wanted type(s)" + + " [tag:yaml.org,2002:map]\n" + + " | - illegal\n" + + " | ......^"), + ILLEGAL_YAML_TYPE_ON_INVARIANT_MAP_KEY( + "verification:\n" + + " invariants:\n" + + " - 1: foo\n" + + " id: 'hi'\n" + + " assert: 'true'", + "ERROR: :3:7: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" + + " [tag:yaml.org,2002:str !txt]\n" + + " | - 1: foo\n" + + " | ......^"); private final String yamlPolicy; private final String expectedErrorMessage; diff --git a/testing/src/test/resources/policy/verification/flawed_policy.yaml b/testing/src/test/resources/policy/verification/flawed_policy.yaml new file mode 100644 index 000000000..1e276c899 --- /dev/null +++ b/testing/src/test/resources/policy/verification/flawed_policy.yaml @@ -0,0 +1,26 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: flawed_policy +description: Tests detecting an invariant violation when a policy allows insecure output (port == 80), checking accurate counterexample generation. +rule: + match: + - condition: port == 80 + output: 'true' + - output: 'false' +verification: + invariants: + - id: always_secure + description: Asserts that insecure output is never allowed, producing a counterexample when port is 80. + assert: rule.result == false diff --git a/testing/src/test/resources/policy/verification/multi_invariant_policy.yaml b/testing/src/test/resources/policy/verification/multi_invariant_policy.yaml new file mode 100644 index 000000000..11d11ad0f --- /dev/null +++ b/testing/src/test/resources/policy/verification/multi_invariant_policy.yaml @@ -0,0 +1,37 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: multi_invariant_policy +description: > + Tests multi-invariant verification where one invariant passes + ('admin_granted') and another is intentionally violated + ('viewer_granted'), checking mixed counterexample reporting. +rule: + match: + - condition: role == 'admin' || role == 'editor' + output: 'true' + - output: 'false' +verification: + invariants: + - id: admin_granted + description: Verifies that admin roles always evaluate to true. + assume: role == 'admin' + assert: rule.result == true + - id: viewer_granted + description: > + Intentionally false assertion that viewer roles are granted + permission, verifying that the solver detects the violation and + reports 'role = "viewer"' as the counterexample. + assume: role == 'viewer' + assert: rule.result == true diff --git a/testing/src/test/resources/policy/verification/restricted_destinations_policy.yaml b/testing/src/test/resources/policy/verification/restricted_destinations_policy.yaml new file mode 100644 index 000000000..bfaa812f4 --- /dev/null +++ b/testing/src/test/resources/policy/verification/restricted_destinations_policy.yaml @@ -0,0 +1,63 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "restricted_destinations" +description: > + Tests a realistic conformance policy verbatim with restricted + destinations, verifying security boundary assertions. +rule: + variables: + - matches_origin_ip: "locationCode(origin.ip) == spec.origin" + - has_nationality: "has(request.auth.claims.nationality)" + - matches_nationality: "variables.has_nationality && request.auth.claims.nationality == spec.origin" + - matches_dest_ip: "locationCode(destination.ip) in spec.restricted_destinations" + - matches_dest_label: "resource.labels.location in spec.restricted_destinations" + - matches_dest: "variables.matches_dest_ip || variables.matches_dest_label" + match: + - condition: "variables.matches_nationality && variables.matches_dest" + output: "true" + - condition: "!variables.has_nationality && variables.matches_origin_ip && variables.matches_dest" + output: "true" + - output: "false" +verification: + variables: + - is_prohibited_origin_ip: "!variables.has_nationality && variables.matches_origin_ip" + - is_unrestricted_dest: "!variables.matches_dest_ip && !variables.matches_dest_label" + invariants: + - id: restricted_by_nationality_prohibited + description: > + Verifies that requests to restricted destinations from users with + matching origin nationality are prohibited. + assume: + - "variables.matches_nationality" + - "variables.matches_dest" + assert: + - "rule.result == true" + - id: restricted_by_origin_ip_prohibited + description: > + Verifies that requests to restricted destinations from users without + nationality claims but matching origin IP are prohibited. + assume: + - "variables.is_prohibited_origin_ip" + - "variables.matches_dest" + assert: + - "rule.result == true" + - id: unrestricted_destination_allowed + description: > + Verifies that requests to destinations not in the restricted list + and without restricted labels are allowed ('false'). + assume: + - "variables.is_unrestricted_dest" + assert: + - "rule.result == false" diff --git a/testing/src/test/resources/policy/verification/secure_resource_access.yaml b/testing/src/test/resources/policy/verification/secure_resource_access.yaml new file mode 100644 index 000000000..375957491 --- /dev/null +++ b/testing/src/test/resources/policy/verification/secure_resource_access.yaml @@ -0,0 +1,38 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: secure_resource_access +description: > + Tests verifying security invariants across policy variables and protobuf + message fields ('test_all_types.repeated_string'). +rule: + variables: + - is_admin: "'admin' in test_all_types.repeated_string" + - is_break_glass: "test_all_types.single_string != ''" + match: + - condition: variables.is_admin && variables.is_break_glass + output: 'true' + - output: 'false' +verification: + variables: + - is_unprivileged: "!('admin' in test_all_types.repeated_string)" + invariants: + - id: no_unprivileged_break_glass + description: > + Verifies that unprivileged users without the admin role can never + trigger break-glass access. + assume: + - "variables.is_unprivileged" + assert: + - "rule.result == false" diff --git a/testing/src/test/resources/policy/verification/workload_admission_fixed.yaml b/testing/src/test/resources/policy/verification/workload_admission_fixed.yaml new file mode 100644 index 000000000..d6d3cf92a --- /dev/null +++ b/testing/src/test/resources/policy/verification/workload_admission_fixed.yaml @@ -0,0 +1,57 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "workload_admission_fixed" +description: "Controls deployment of workloads to Kubernetes clusters." +rule: + variables: + - is_cluster_admin: "is_admin" + - is_namespace_owner: "is_owner" + - is_privileged_container: "is_privileged" + - is_prod_cluster: "is_prod" + - has_security_approval: "has_approval" + + match: + # Hard DENY unapproved privileged containers in prod. + - condition: "variables.is_privileged_container && variables.is_prod_cluster && !variables.has_security_approval" + output: "'DENY'" + explanation: "'Privileged containers in production require explicit security approval.'" + + # Break-glass admin bypass (Now safely guarded) + - condition: "variables.is_cluster_admin" + output: "'ALLOW'" + + # Namespace owners + - condition: "variables.is_namespace_owner" + output: "'ALLOW'" + + # Standard non-privileged deployments + - condition: "!variables.is_privileged_container" + output: "'ALLOW'" + + # Default Deny + - output: "'DENY'" + +verification: + invariants: + - id: universal_no_unapproved_privileged_prod + description: > + Guarantees that NO ONE can deploy a privileged container + to production without explicit security approval, including admins. + assume: + - "variables.is_privileged_container" + - "variables.is_prod_cluster" + - "!variables.has_security_approval" + assert: + - "rule.result == 'DENY'" diff --git a/testing/src/test/resources/policy/verification/workload_admission_flawed.yaml b/testing/src/test/resources/policy/verification/workload_admission_flawed.yaml new file mode 100644 index 000000000..3ca591a09 --- /dev/null +++ b/testing/src/test/resources/policy/verification/workload_admission_flawed.yaml @@ -0,0 +1,67 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "workload_admission_flawed" +description: > + Controls deployment of workloads to Kubernetes clusters. + Note: This is an intentional buggy example. + The fixed example is shown in workload_admission_fixed. +rule: + variables: + - is_cluster_admin: "is_admin" + - is_namespace_owner: "is_owner" + - is_privileged_container: "is_privileged" + - is_prod_cluster: "is_prod" + - has_security_approval: "has_approval" + + match: + # Break-glass admin bypass + # BUG: This completely bypasses the production restriction for privileged containers below! + - condition: "variables.is_cluster_admin" + output: "'ALLOW'" + + # Namespace owners can deploy workloads to their namespace + # BUG: The author forgot to enforce the privileged container rule for namespace owners! + - condition: "variables.is_namespace_owner" + output: "'ALLOW'" + + # Strictly restrict privileged containers in production + - condition: "variables.is_privileged_container && variables.is_prod_cluster" + rule: + match: + - condition: "variables.has_security_approval" + output: "'ALLOW'" + - output: "'DENY'" + explanation: > + 'Privileged containers in production require explicit security approval.' + + # Standard non-privileged deployments + - condition: "!variables.is_privileged_container" + output: "'ALLOW'" + + # Default Deny + - output: "'DENY'" + +verification: + invariants: + - id: universal_no_unapproved_privileged_prod + description: > + Mathematically guarantees that NO ONE can deploy a privileged container + to production without explicit security approval, including admins. + assume: + - "variables.is_privileged_container" + - "variables.is_prod_cluster" + - "!variables.has_security_approval" + assert: + - "rule.result == 'DENY'" diff --git a/verifier/BUILD.bazel b/verifier/BUILD.bazel index 41837d1bc..1c2e5adfa 100644 --- a/verifier/BUILD.bazel +++ b/verifier/BUILD.bazel @@ -17,6 +17,7 @@ java_library( java_library( name = "policy_verifier_factory", + compatible_with = [], exports = ["//verifier/src/main/java/dev/cel/verifier:policy_verifier_factory"], ) diff --git a/verifier/README.md b/verifier/README.md index c10ce06eb..bf98643af 100644 --- a/verifier/README.md +++ b/verifier/README.md @@ -34,18 +34,26 @@ properties about your expressions. * **Satisfiability & Validity Proving:** Check if an expression can ever evaluate to `true` (satisfiability) or if it is guaranteed to always be - `true` (validity). + `true` (validity). When checking satisfiability (`isSatisfiable`), the + verifier produces a satisfying model (witness assignments) showing concrete + inputs that make the expression true. * **Logical Equivalence:** Prove that two different ASTs or Policies are semantically identical. * **Bounded Model Checking (BMC):** Safely verify list and map comprehensions (`all`, `exists`, `map`, `filter`) by statically unrolling them up to a configurable limit. -* **Counterexample Generation:** When verification fails (e.g., two - expressions are not equivalent), the verifier generates a human-readable - counterexample showing the inputs that caused the mismatch. +* **Counterexample & Witness Generation:** When validity (`isAlwaysTrue`) or + equivalence verification fails, the verifier generates a human-readable + counterexample showing the inputs that caused the violation. When checking + satisfiability (`isSatisfiable`), it generates concrete variable assignments + (satisfying model / witness) showing the inputs that satisfy the condition. * **Partial Evaluation (Unknowns) Support:** Define variables that are permitted to evaluate to `Unknown` during verification, mirroring CEL's runtime partial evaluation. +* **Custom Invariants Verification:** Allows policy authors to define safety + invariants (e.g., "port must always be secure if external access is + allowed") and mathematically prove that the policy never violates them + across all possible input states. ```java CelVerifier verifier = CelVerifierFactory.newVerifier() @@ -59,9 +67,6 @@ CelVerifier verifier = CelVerifierFactory.newVerifier() The following features are planned for future releases: -* **Custom Invariants Verification:** Allows policy authors to define safety - invariants (e.g., "port must always be secure if external access is - allowed") and mathematically prove that the policy never violates them. * **Deep Reachability Analysis:** Statically analyzes nested policy rules to detect unreachable execution paths (dead code) that can never be executed under any input. @@ -115,17 +120,21 @@ public class VerifierExample { return; } - if (result.status() == VerificationStatus.VERIFIED) { - System.out.println("Expressions are logically equivalent!"); - } else if (result.status() == VerificationStatus.VIOLATED) { - System.out.println(result.message()); - // Example output if expressions were NOT equivalent: - // Equivalence violation detected. Counterexample input: - // x = ... - } else { - // INCONCLUSIVE means the solver could not positively confirm VERIFIED or VIOLATED - // due to things like loop truncation (BMC) or uninterpreted functions. - System.out.println("Verification was inconclusive: " + result.message()); + switch (result.status()) { + case VERIFIED: + System.out.println("Expressions are logically equivalent!"); + break; + case VIOLATED: + System.out.println(result.message()); + // Example output if expressions were NOT equivalent: + // Equivalence violation detected. Counterexample input: + // x = ... + break; + case INCONCLUSIVE: + // INCONCLUSIVE means the solver could not positively confirm VERIFIED or VIOLATED + // due to things like loop truncation (BMC) or uninterpreted functions. + System.out.println("Verification was inconclusive: " + result.message()); + break; } } } @@ -153,7 +162,7 @@ import dev.cel.verifier.CelVerifier; import dev.cel.verifier.CelVerifierFactory; public class PolicyVerifierExample { - private static final Cel CEL = CelFactory.standardCelBuilder() + private static final Cel CEL = CelFactory.plannerCelBuilder() .addVar("role", SimpleType.STRING) .addVar("country", SimpleType.STRING) .addVar("port", SimpleType.INT) @@ -206,18 +215,150 @@ public class PolicyVerifierExample { return; } - if (result.status() == VerificationStatus.VERIFIED) { - System.out.println("Policies are equivalent!"); - } else if (result.status() == VerificationStatus.VIOLATED) { - System.out.println("Refactoring bug detected!"); - System.out.println(result.message()); - // Output: - // Equivalence violation detected. Counterexample input: - // country = "a" - // role = "editor" - // port = 443 - } else { - System.out.println("Verification was inconclusive: " + result.message()); + switch (result.status()) { + case VERIFIED: + System.out.println("Policies are equivalent!"); + break; + case VIOLATED: + System.out.println("Refactoring bug detected!"); + System.out.println(result.message()); + // Output: + // Equivalence violation detected. Counterexample input: + // country = "a" + // role = "editor" + // port = 443 + break; + case INCONCLUSIVE: + System.out.println("Verification was inconclusive: " + result.message()); + break; + } + } +} +``` + +### 3. Satisfiability Checking & Witness Generation + +When checking whether an expression is satisfiable using `isSatisfiable`, the +verifier returns `VERIFIED` if there exists at least one input combination +where the expression evaluates to `true`. Furthermore, `result.message()` +provides concrete satisfying model assignments (witness/test case generation). + +```java +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.types.SimpleType; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.verifier.CelVerificationResult; +import dev.cel.verifier.CelVerificationResult.VerificationStatus; +import dev.cel.verifier.CelVerifier; +import dev.cel.verifier.CelVerifierFactory; + +public class SatisfiabilityExample { + public static void main(String[] args) throws Exception { + CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder() + .addVar("role", SimpleType.STRING) + .addVar("port", SimpleType.INT) + .build(); + + // Check if an authorization condition can ever be met + CelAbstractSyntaxTree ast = + compiler.compile("role == 'editor' && port > 1024 && port < 65535").getAst(); + + CelVerifier verifier = CelVerifierFactory.newVerifier().build(); + CelVerificationResult result = verifier.isSatisfiable(ast); + + switch (result.status()) { + case VERIFIED: + System.out.println("Condition is satisfiable!"); + System.out.println(result.message()); + // Output: + // Condition is satisfiable. Satisfying input: + // port = 1025 + // role = "editor" + break; + case VIOLATED: + System.out.println("Condition is completely unsatisfiable."); + break; + case INCONCLUSIVE: + System.out.println("Verification was inconclusive: " + result.message()); + break; + } + } +} +``` + +### 4. Policy Invariants Verification + +You can verify that custom invariants (`assume` preconditions and `assert` +clauses) declared on a `CelPolicy` hold mathematically across all possible +input states. The reserved `rule.result` identifier matches the return +value of the policy. + +```java +import com.google.common.collect.ImmutableMap; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelFactory; +import dev.cel.common.types.SimpleType; +import dev.cel.policy.CelPolicy; +import dev.cel.policy.CelPolicyCompiler; +import dev.cel.policy.CelPolicyCompilerFactory; +import dev.cel.policy.CelPolicyParser; +import dev.cel.policy.CelPolicyParserFactory; +import dev.cel.verifier.CelPolicyVerifier; +import dev.cel.verifier.CelPolicyVerifierFactory; +import dev.cel.verifier.CelVerificationResult; +import dev.cel.verifier.CelVerificationResult.VerificationStatus; +import dev.cel.verifier.CelVerifier; +import dev.cel.verifier.CelVerifierFactory; + +public class InvariantsExample { + private static final Cel CEL = CelFactory.plannerCelBuilder() + .addVar("port", SimpleType.INT) + .build(); + + private static final CelPolicyParser PARSER = CelPolicyParserFactory.newYamlParserBuilder().build(); + + private static final CelPolicyCompiler POLICY_COMPILER = + CelPolicyCompilerFactory.newPolicyCompiler(CEL).build(); + + private static final CelVerifier AST_VERIFIER = CelVerifierFactory.newVerifier().build(); + + private static final CelPolicyVerifier POLICY_VERIFIER = + CelPolicyVerifierFactory.newVerifier(POLICY_COMPILER, AST_VERIFIER).build(); + + public static void main(String[] args) throws Exception { + String yamlPolicy = """ + name: secure_access_policy + rule: + match: + - condition: port == 80 + output: 'true' + - output: 'false' + verification: + invariants: + - id: always_secure + assert: + - rule.result == false + """; + + CelPolicy policy = PARSER.parse(yamlPolicy); + ImmutableMap results = POLICY_VERIFIER.verifyInvariants(policy); + + CelVerificationResult result = results.get("always_secure"); + switch (result.status()) { + case VERIFIED: + System.out.println("Invariant proven!"); + break; + case VIOLATED: + System.out.println("Invariant violated!"); + System.out.println(result.message()); + // Output: + // Implication violation detected. Counterexample input: + // port = 80 + break; + case INCONCLUSIVE: + System.out.println("Verification was inconclusive: " + result.message()); + break; } } } diff --git a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel index 6cdb793db..792ac5d2d 100644 --- a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel +++ b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel @@ -50,12 +50,14 @@ java_library( ":verifier", "//policy", "//policy:validation_exception", + "@maven//:com_google_guava_guava", ], ) java_library( name = "policy_verifier_factory", srcs = ["CelPolicyVerifierFactory.java"], + compatible_with = [], tags = [ ], deps = [ @@ -69,15 +71,23 @@ java_library( java_library( name = "policy_verifier_impl", srcs = ["CelPolicyVerifierImpl.java"], + compatible_with = [], tags = [ ], deps = [ ":policy_verifier", ":verifier", + ":z3_impl", + "//bundle:cel", "//common:cel_ast", + "//common:cel_source", + "//common:compiler_common", + "//common/formats:value_string", "//policy", + "//policy:compiled_rule", "//policy:compiler", "//policy:validation_exception", + "@maven//:com_google_guava_guava", ], ) diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java index 54049a1b5..255658136 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java @@ -116,6 +116,14 @@ BoolExpr isTrue(Expr celValue) { return ctx.mkAnd(typeSystem.isBool(celValue), (BoolExpr) typeSystem.unwrapBool(celValue)); } + /** + * Binds an identifier name in the symbol table to an already-translated value. Used to bind + * reserved policy symbols such as 'rule.result' to the composed policy graph. + */ + void bindSymbol(String varName, TranslatedValue value) { + symbolTable.put(varName, value); + } + TranslatedValue translate(CelAbstractSyntaxTree ast) { TranslatedValue result; CelBlock celBlock = CelBlock.extract(ast).orElse(null); @@ -261,7 +269,7 @@ private TranslatedValue translateIdent(CelExpr celExpr, CelAbstractSyntaxTree as /* isApproximate= */ ctx.mkFalse()); }); - return TranslatedValue.create(tv.z3Expr(), celExpr, typeSystem, ctx.mkFalse()); + return TranslatedValue.create(tv.z3Expr(), celExpr, typeSystem, tv.isApproximate()); } private TranslatedValue translateList(CelExpr celExpr, CelAbstractSyntaxTree ast) { diff --git a/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifier.java b/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifier.java index f9ad77c93..70e1494f1 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifier.java +++ b/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifier.java @@ -14,6 +14,7 @@ package dev.cel.verifier; +import com.google.common.collect.ImmutableMap; import dev.cel.policy.CelPolicy; import dev.cel.policy.CelPolicyValidationException; @@ -27,4 +28,15 @@ public interface CelPolicyVerifier { */ CelVerificationResult verifyEquivalence(CelPolicy policyA, CelPolicy policyB) throws CelPolicyValidationException, CelVerificationException; + + /** + * Verifies all custom invariants defined in the policy against all possible input states. + * + * @return A map of invariant IDs to their verification results. + * @throws CelPolicyValidationException if the policy or any invariant fails compilation. + * @throws CelVerificationException if the verification check encounters an internal solver error. + */ + ImmutableMap verifyInvariants(CelPolicy policy) + throws CelPolicyValidationException, CelVerificationException; } + diff --git a/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierImpl.java b/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierImpl.java index 8cc145117..885201190 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierImpl.java +++ b/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierImpl.java @@ -14,7 +14,17 @@ package dev.cel.verifier; +import com.google.common.collect.ImmutableMap; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelBuilder; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelIssue; +import dev.cel.common.CelSource; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelVarDecl; +import dev.cel.common.formats.ValueString; +import dev.cel.policy.CelCompiledRule; +import dev.cel.policy.CelCompiledRule.CelCompiledVariable; import dev.cel.policy.CelPolicy; import dev.cel.policy.CelPolicyCompiler; import dev.cel.policy.CelPolicyValidationException; @@ -22,6 +32,8 @@ /** Implementation of CelPolicyVerifier using a CelVerifier. */ final class CelPolicyVerifierImpl implements CelPolicyVerifier { + private static final String INVARIANTS_RESULT_IDENTIFIER = "rule.result"; + private final CelPolicyCompiler compiler; private final CelVerifier astVerifier; @@ -56,4 +68,89 @@ public CelVerificationResult verifyEquivalence(CelPolicy policyA, CelPolicy poli CelAbstractSyntaxTree astB = compiler.compile(policyB); return astVerifier.verifyEquivalence(astA, astB); } + + @Override + public ImmutableMap verifyInvariants(CelPolicy policy) + throws CelPolicyValidationException, CelVerificationException { + if (policy.invariants().isEmpty()) { + return ImmutableMap.of(); + } + + CelCompiledRule compiledRule = compiler.compileRule(policy); + CelAbstractSyntaxTree composedPolicyAst = compiler.compose(policy, compiledRule); + + CelBuilder celBuilder = compiledRule.cel().toCelBuilder(); + ImmutableMap.Builder boundSymbolsBuilder = + ImmutableMap.builder(); + for (CelCompiledVariable var : compiledRule.variables()) { + celBuilder.addVarDeclarations(var.celVarDecl()); + boundSymbolsBuilder.put(var.celVarDecl().name(), var.ast()); + } + boundSymbolsBuilder.put(INVARIANTS_RESULT_IDENTIFIER, composedPolicyAst); + celBuilder.addVarDeclarations( + CelVarDecl.newVarDeclaration( + INVARIANTS_RESULT_IDENTIFIER, composedPolicyAst.getResultType())); + + Cel localCel = celBuilder.build(); + for (CelPolicy.Variable variable : policy.verificationVariables()) { + ValueString expression = variable.expression(); + CelAbstractSyntaxTree varAst; + try { + varAst = localCel.compile(expression.value()).getAst(); + } catch (CelValidationException e) { + throw new CelPolicyValidationException( + CelIssue.toDisplayString( + e.getErrors(), + CelSource.newBuilder(expression.value()) + .setDescription("verification.variables." + variable.name().value()) + .build())); + } + String variableName = variable.name().value(); + CelVarDecl newVariable = + CelVarDecl.newVarDeclaration("variables." + variableName, varAst.getResultType()); + celBuilder.addVarDeclarations(newVariable); + boundSymbolsBuilder.put("variables." + variableName, varAst); + localCel = localCel.toCelBuilder().addVarDeclarations(newVariable).build(); + } + Cel enrichedCel = celBuilder.build(); + ImmutableMap boundSymbols = boundSymbolsBuilder.buildOrThrow(); + + ImmutableMap.Builder resultsBuilder = ImmutableMap.builder(); + for (CelPolicy.Invariant invariant : policy.invariants()) { + CelAbstractSyntaxTree assumeAst; + try { + assumeAst = enrichedCel.compile(invariant.assumeSourceString()).getAst(); + } catch (CelValidationException e) { + throw new CelPolicyValidationException( + CelIssue.toDisplayString( + e.getErrors(), + CelSource.newBuilder(invariant.assumeSourceString()) + .setDescription("invariant." + invariant.invariantId().value() + ".assume") + .build())); + } + + CelAbstractSyntaxTree assertAst; + try { + assertAst = enrichedCel.compile(invariant.assertSourceString()).getAst(); + } catch (CelValidationException e) { + throw new CelPolicyValidationException( + CelIssue.toDisplayString( + e.getErrors(), + CelSource.newBuilder(invariant.assertSourceString()) + .setDescription("invariant." + invariant.invariantId().value() + ".assert") + .build())); + } + + if (!(astVerifier instanceof CelVerifierZ3Impl)) { + throw new UnsupportedOperationException( + "Invariants verification requires Z3 verifier implementation."); + } + CelVerificationResult result = + ((CelVerifierZ3Impl) astVerifier) + .verifyImplication(assumeAst, assertAst, boundSymbols); + resultsBuilder.put(invariant.invariantId().value(), result); + } + + return resultsBuilder.buildOrThrow(); + } } diff --git a/verifier/src/main/java/dev/cel/verifier/CelVerifier.java b/verifier/src/main/java/dev/cel/verifier/CelVerifier.java index 32bffa1d3..0e80424f2 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelVerifier.java +++ b/verifier/src/main/java/dev/cel/verifier/CelVerifier.java @@ -46,3 +46,5 @@ public interface CelVerifier { CelVerificationResult verifyEquivalence(CelAbstractSyntaxTree astA, CelAbstractSyntaxTree astB) throws CelVerificationException; } + + diff --git a/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java b/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java index 9d8b87e07..ce2705b56 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java +++ b/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java @@ -33,7 +33,10 @@ import dev.cel.verifier.axioms.CelZ3FunctionAxiom; import dev.cel.verifier.axioms.CelZ3StandardAxioms; import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.Map; import java.util.Optional; import org.jspecify.annotations.Nullable; @@ -221,6 +224,95 @@ public CelVerificationResult verifyEquivalence( } } + CelVerificationResult verifyImplication( + CelAbstractSyntaxTree assumeAst, + CelAbstractSyntaxTree assertAst, + Map boundSymbols) + throws CelVerificationException { + Preconditions.checkArgument(assumeAst.isChecked(), "assumeAst must be type-checked."); + Preconditions.checkArgument(assertAst.isChecked(), "assertAst must be type-checked."); + for (Map.Entry entry : boundSymbols.entrySet()) { + Preconditions.checkArgument( + entry.getValue().isChecked(), + "boundSymbol AST for '%s' must be type-checked.", + entry.getKey()); + } + + try (Context ctx = new Context(ImmutableMap.of("model", "true"))) { + CelAstToZ3Translator translator = + new CelAstToZ3Translator( + ctx, comprehensionUnrollLimit, unknownIdentifiers, functionRegistry, typeProvider); + + List taints = new ArrayList<>(); + for (Map.Entry entry : boundSymbols.entrySet()) { + TranslatedValue tv = translator.translate(entry.getValue()); + translator.bindSymbol(entry.getKey(), tv); + } + + TranslatedValue assumeTv = translator.translate(assumeAst); + TranslatedValue assertTv = translator.translate(assertAst); + taints.add(assumeTv.isApproximate()); + taints.add(assertTv.isApproximate()); + + BoolExpr assumeCondition = translator.isTrue(assumeTv.z3Expr()); + BoolExpr assertCondition = translator.isTrue(assertTv.z3Expr()); + BoolExpr violationCondition = ctx.mkAnd(assumeCondition, ctx.mkNot(assertCondition)); + + BoolExpr combinedTaint = CelZ3TypeSystem.mkOrFlattened(ctx, taints); + BoolExpr unknownCondition = + ctx.mkOr( + translator.getTypeSystem().isUnknown(assumeTv.z3Expr()), + translator.getTypeSystem().isUnknown(assertTv.z3Expr())); + + Solver solver = newSolver(ctx); + for (BoolExpr constraint : translator.getTypeConstraints()) { + solver.add(constraint); + } + + SolverRunResult result = + runThreePassVerification( + ctx, + solver, + violationCondition, + combinedTaint, + unknownCondition, + translator, + /* checkTruncation= */ true); + + switch (result.outcome) { + case EXACT_MATCH: + return CelVerificationResult.failed( + "Implication violation detected." + + getCounterexampleString( + ctx, + translator.getTypeSystem(), + result.model, + /* isApproximate= */ false, + /* isCounterexample= */ true)); + case APPROXIMATE_MATCH: + return CelVerificationResult.inconclusive( + "Inconclusive: a counterexample may exist, but it depends on approximations, missing" + + " theories, or loop bounds." + + getCounterexampleString( + ctx, + translator.getTypeSystem(), + result.model, + /* isApproximate= */ true, + /* isCounterexample= */ true)); + case TRUNCATED: + return CelVerificationResult.inconclusive( + "Inconclusive: implication holds within the current loop unroll limit, but" + + " may be violated for larger collections."); + case NO_MATCH: + return CelVerificationResult.verified(); + case SOLVER_UNKNOWN: + return CelVerificationResult.inconclusive( + "Inconclusive: the solver returned unknown status (" + result.reason + ")."); + } + throw new AssertionError("Unknown verification outcome: " + result.outcome); + } + } + private CelVerificationResult checkSatisfiability( CelAbstractSyntaxTree ast, boolean searchForCounterexample) throws CelVerificationException { try (Context ctx = new Context(ImmutableMap.of("model", "true"))) { diff --git a/verifier/src/test/java/dev/cel/verifier/BUILD.bazel b/verifier/src/test/java/dev/cel/verifier/BUILD.bazel index 03676eca6..9e7f0ed15 100644 --- a/verifier/src/test/java/dev/cel/verifier/BUILD.bazel +++ b/verifier/src/test/java/dev/cel/verifier/BUILD.bazel @@ -12,6 +12,9 @@ java_library( ["**/*.java"], ), compatible_with = [], + data = [ + "//testing:policy_test_resources", + ], deps = [ "//bundle:cel", "//common:cel_ast", @@ -37,6 +40,8 @@ java_library( "//policy:compiler_factory", "//policy:parser", "//policy:parser_factory", + "//policy:validation_exception", + "@bazel_tools//tools/java/runfiles", "@maven//:junit_junit", "@maven//:com_google_testparameterinjector_test_parameter_injector", "//:java_truth", diff --git a/verifier/src/test/java/dev/cel/verifier/CelPolicyVerifierImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelPolicyVerifierImplTest.java index 2bc500bde..09f205f04 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelPolicyVerifierImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelPolicyVerifierImplTest.java @@ -15,13 +15,18 @@ package dev.cel.verifier; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.assertThrows; +import com.google.common.collect.ImmutableMap; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.bundle.Cel; import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOptions; +import dev.cel.common.CelOverloadDecl; import dev.cel.common.types.SimpleType; import dev.cel.common.types.StructTypeReference; import dev.cel.expr.conformance.proto3.TestAllTypes; @@ -37,6 +42,7 @@ import dev.cel.policy.CelPolicyCompilerFactory; import dev.cel.policy.CelPolicyParser; import dev.cel.policy.CelPolicyParserFactory; +import dev.cel.policy.CelPolicyValidationException; import dev.cel.verifier.CelVerificationResult.VerificationStatus; import org.junit.Before; import org.junit.Test; @@ -49,8 +55,12 @@ public final class CelPolicyVerifierImplTest { CelPolicyParserFactory.newYamlParserBuilder().enableSimpleVariables(true).build(); private static final Cel CEL = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().populateMacroCalls(true).build()) + CelFactory.plannerCelBuilder() + .setOptions( + CelOptions.current() + .populateMacroCalls(true) + .enableHeterogeneousNumericComparisons(true) + .build()) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .addCompilerLibraries(CelExtensions.bindings()) .addMessageTypes(TestAllTypes.getDescriptor()) @@ -308,4 +318,382 @@ public void verifyEquivalence_celBlockSupport_cseOptimizer( assertThat(unparsed).startsWith("cel.@block"); assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); } + + @Test + public void verifyInvariants_emptyInvariants_returnsEmptyMap() throws Exception { + String policySource = + "name: empty_policy\n" // + + "rule:\n" // + + " match:\n" // + + " - output: 'true'"; + CelPolicy policy = PARSER.parse(policySource); + + ImmutableMap results = VERIFIER.verifyInvariants(policy); + + assertThat(results).isEmpty(); + } + + @Test + public void verifyInvariants_flawedPolicy_violationDetected() throws Exception { + CelPolicy policy = + PARSER.parse(VerifierTestHelper.loadVerificationPolicyYaml("flawed_policy.yaml")); + + ImmutableMap results = VERIFIER.verifyInvariants(policy); + + assertThat(results).containsKey("always_secure"); + CelVerificationResult result = results.get("always_secure"); + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()).containsMatch("port = 80"); + } + + @Test + public void verifyInvariants_secureResourceAccess_verified() throws Exception { + Cel extendedCel = + CEL.toCelBuilder() + .addVar("resource", SimpleType.DYN) + .build(); + CelPolicyVerifier verifier = + CelPolicyVerifierFactory.newVerifier( + CelPolicyCompilerFactory.newPolicyCompiler(extendedCel).build(), AST_VERIFIER) + .build(); + + CelPolicy policy = + PARSER.parse(VerifierTestHelper.loadVerificationPolicyYaml("secure_resource_access.yaml")); + + ImmutableMap results = verifier.verifyInvariants(policy); + + assertThat(results).containsKey("no_unprivileged_break_glass"); + assertThat(results.get("no_unprivileged_break_glass").status()) + .isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void verifyInvariants_multipleInvariants_mixedResults() throws Exception { + CelPolicy policy = + PARSER.parse(VerifierTestHelper.loadVerificationPolicyYaml("multi_invariant_policy.yaml")); + + ImmutableMap results = VERIFIER.verifyInvariants(policy); + + assertThat(results).hasSize(2); + assertThat(results.get("admin_granted").status()).isEqualTo(VerificationStatus.VERIFIED); + assertThat(results.get("viewer_granted").status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(results.get("viewer_granted").message()).containsMatch("role = \"viewer\""); + } + + @Test + public void verifyInvariants_invalidAssumeClause_throwsValidationException() throws Exception { + String policySource = + "name: invalid_assume\n" // + + "rule:\n" // + + " match:\n" // + + " - output: 'true'\n" // + + "verification:\n" // + + " invariants:\n" // + + " - id: bad_assume\n" // + + " assume: non_existent_var == 123\n" // + + " assert: rule.result == true"; + CelPolicy policy = PARSER.parse(policySource); + + assertThrows(CelPolicyValidationException.class, () -> VERIFIER.verifyInvariants(policy)); + } + + @Test + public void verifyInvariants_invalidAssertClause_throwsValidationException() throws Exception { + String policySource = + "name: invalid_assert\n" // + + "rule:\n" // + + " match:\n" // + + " - output: 'true'\n" // + + "verification:\n" // + + " invariants:\n" // + + " - id: bad_assert\n" // + + " assert: rule.result + non_existent_var == true"; + CelPolicy policy = PARSER.parse(policySource); + + assertThrows(CelPolicyValidationException.class, () -> VERIFIER.verifyInvariants(policy)); + } + + @Test + public void verifyInvariants_restrictedDestinationsPolicy_verified() throws Exception { + Cel extendedCel = + CEL.toCelBuilder() + .addVar("origin", SimpleType.DYN) + .addVar("destination", SimpleType.DYN) + .addVar("spec", SimpleType.DYN) + .addVar("resource", SimpleType.DYN) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "locationCode", + CelOverloadDecl.newGlobalOverload( + "locationCode_string", SimpleType.STRING, SimpleType.STRING))) + .build(); + CelPolicyVerifier verifier = + CelPolicyVerifierFactory.newVerifier( + CelPolicyCompilerFactory.newPolicyCompiler(extendedCel).build(), AST_VERIFIER) + .build(); + + CelPolicy policy = + PARSER.parse( + VerifierTestHelper.loadVerificationPolicyYaml("restricted_destinations_policy.yaml")); + + ImmutableMap results = verifier.verifyInvariants(policy); + + assertThat(results) + .containsExactly( + "restricted_by_nationality_prohibited", CelVerificationResult.verified(), + "restricted_by_origin_ip_prohibited", CelVerificationResult.verified(), + "unrestricted_destination_allowed", CelVerificationResult.verified()); + } + + @Test + public void verifyInvariants_invariantReferencesPolicyVariable_verified() throws Exception { + String policySource = + "name: variable_reference_policy\n" // + + "rule:\n" // + + " variables:\n" // + + " - is_admin: role == 'admin'\n" // + + " match:\n" // + + " - condition: variables.is_admin\n" // + + " output: 'true'\n" // + + " - output: 'false'\n" // + + "verification:\n" // + + " invariants:\n" // + + " - id: admin_always_true\n" // + + " assume: variables.is_admin\n" // + + " assert: rule.result == true"; + CelPolicy policy = PARSER.parse(policySource); + + ImmutableMap results = VERIFIER.verifyInvariants(policy); + + assertThat(results).containsKey("admin_always_true"); + assertThat(results.get("admin_always_true").status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void verifyInvariants_verificationVariablesAndMultiClauseLists_verified() + throws Exception { + String policySource = + "name: verification_ergonomics_policy\n" // + + "rule:\n" // + + " variables:\n" // + + " - rule_admin: role == 'admin'\n" // + + " match:\n" // + + " - condition: variables.rule_admin\n" // + + " output: 'true'\n" // + + " - output: 'false'\n" // + + "verification:\n" // + + " variables:\n" // + + " - ver_admin: variables.rule_admin && true\n" // + + " invariants:\n" // + + " - id: ergonomic_inv\n" // + + " assume:\n" // + + " - variables.ver_admin\n" // + + " - role != 'editor'\n" // + + " assert:\n" // + + " - rule.result == true\n" // + + " - role == 'admin'"; + CelPolicy policy = PARSER.parse(policySource); + + ImmutableMap results = VERIFIER.verifyInvariants(policy); + + assertThat(results).containsExactly("ergonomic_inv", CelVerificationResult.verified()); + } + + @Test + public void verifyInvariants_workloadAdmissionFlawed_violationsDetected() throws Exception { + Cel extendedCel = + CEL.toCelBuilder() + .addVar("is_admin", SimpleType.BOOL) + .addVar("is_owner", SimpleType.BOOL) + .addVar("is_privileged", SimpleType.BOOL) + .addVar("is_prod", SimpleType.BOOL) + .addVar("has_approval", SimpleType.BOOL) + .addVar("spec", SimpleType.DYN) + .build(); + CelPolicyVerifier verifier = + CelPolicyVerifierFactory.newVerifier( + CelPolicyCompilerFactory.newPolicyCompiler(extendedCel).build(), AST_VERIFIER) + .build(); + + CelPolicy policy = + PARSER.parse( + VerifierTestHelper.loadVerificationPolicyYaml("workload_admission_flawed.yaml")); + + ImmutableMap results = verifier.verifyInvariants(policy); + + assertThat(results).containsKey("universal_no_unapproved_privileged_prod"); + assertThat(results.get("universal_no_unapproved_privileged_prod").status()) + .isEqualTo(VerificationStatus.VIOLATED); + assertThat(results.get("universal_no_unapproved_privileged_prod").message()) + .isEqualTo( + "Implication violation detected. Counterexample input:\n" + + " is_owner = false\n" + + " is_privileged = true\n" + + " is_prod = true\n" + + " has_approval = false\n" + + " is_admin = true"); + } + + @Test + public void verifyInvariants_workloadAdmissionFixed_verified() throws Exception { + Cel extendedCel = + CEL.toCelBuilder() + .addVar("is_admin", SimpleType.BOOL) + .addVar("is_owner", SimpleType.BOOL) + .addVar("is_privileged", SimpleType.BOOL) + .addVar("is_prod", SimpleType.BOOL) + .addVar("has_approval", SimpleType.BOOL) + .addVar("spec", SimpleType.DYN) + .build(); + CelPolicyVerifier verifier = + CelPolicyVerifierFactory.newVerifier( + CelPolicyCompilerFactory.newPolicyCompiler(extendedCel).build(), AST_VERIFIER) + .build(); + + CelPolicy policy = + PARSER.parse( + VerifierTestHelper.loadVerificationPolicyYaml("workload_admission_fixed.yaml")); + + ImmutableMap results = verifier.verifyInvariants(policy); + + assertThat(results).containsKey("universal_no_unapproved_privileged_prod"); + assertWithMessage(results.get("universal_no_unapproved_privileged_prod").message()) + .that(results.get("universal_no_unapproved_privileged_prod").status()) + .isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void verifyInvariants_invariantSyntaxError_formatsSnippetAndDescription() throws Exception { + String yamlPolicy = + "name: syntax_error_policy\n" + + "rule:\n" + + " match:\n" + + " - output: 'true'\n" + + "verification:\n" + + " invariants:\n" + + " - id: bad_invariant\n" + + " assume:\n" + + " - 'port == 80 &&+ port == 90'\n" + + " assert:\n" + + " - 'rule.result == false'\n"; + CelPolicy policy = PARSER.parse(yamlPolicy); + + CelPolicyValidationException e = + assertThrows(CelPolicyValidationException.class, () -> VERIFIER.verifyInvariants(policy)); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "ERROR: invariant.bad_invariant.assume:1:14: extraneous input '+' expecting {'['," + + " '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT," + + " NUM_UINT, STRING, BYTES, IDENTIFIER}\n" + + " | port == 80 &&+ port == 90\n" + + " | .............^"); + } + + @Test + public void verifyInvariants_verificationVariableSyntaxError_formatsSnippetAndDescription() + throws Exception { + String yamlPolicy = + "name: syntax_error_policy\n" + + "rule:\n" + + " match:\n" + + " - output: 'true'\n" + + "verification:\n" + + " variables:\n" + + " - bad_var: 'port == 80 &&+ port == 90'\n" + + " invariants:\n" + + " - id: always_secure\n" + + " assert:\n" + + " - 'rule.result == false'\n"; + CelPolicy policy = PARSER.parse(yamlPolicy); + + CelPolicyValidationException e = + assertThrows(CelPolicyValidationException.class, () -> VERIFIER.verifyInvariants(policy)); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "ERROR: verification.variables.bad_var:1:14: extraneous input '+' expecting {'['," + + " '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT," + + " NUM_UINT, STRING, BYTES, IDENTIFIER}\n" + + " | port == 80 &&+ port == 90\n" + + " | .............^"); + } + + @Test + public void verifyInvariants_unrelatedBoundedSymbolApproximate_doesNotForceInconclusive() + throws Exception { + String yamlPolicy = + "name: unrelated_approx_policy\n" + + "rule:\n" + + " variables:\n" + + " - unrelated_approx: 'request.matches(\"a\") == true'\n" + + " match:\n" + + " - condition: 'port == 80'\n" + + " output: 'true'\n" + + " - output: 'false'\n" + + "verification:\n" + + " invariants:\n" + + " - id: port_check\n" + + " assume:\n" + + " - 'port == 80'\n" + + " assert:\n" + + " - 'rule.result == true'\n"; + CelPolicy policy = PARSER.parse(yamlPolicy); + + ImmutableMap results = VERIFIER.verifyInvariants(policy); + + assertThat(results).containsKey("port_check"); + assertThat(results.get("port_check").status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void verifyInvariants_verificationVariablesCanReferenceRuleResult() throws Exception { + String yamlPolicy = + "name: rule_result_in_ver_var_policy\n" + + "rule:\n" + + " match:\n" + + " - condition: 'port == 80'\n" + + " output: 'true'\n" + + " - output: 'false'\n" + + "verification:\n" + + " variables:\n" + + " - is_denied: 'rule.result == false'\n" + + " invariants:\n" + + " - id: port_check\n" + + " assume:\n" + + " - 'port == 80'\n" + + " assert:\n" + + " - 'variables.is_denied == false'\n"; + CelPolicy policy = PARSER.parse(yamlPolicy); + + ImmutableMap results = VERIFIER.verifyInvariants(policy); + + assertThat(results).containsKey("port_check"); + assertThat(results.get("port_check").status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void verifyInvariants_boundedSymbolWithApproximation_returnsInconclusive() + throws Exception { + String yamlPolicy = + "name: approx_symbol_policy\n" + + "rule:\n" + + " variables:\n" + + " - approx_var: 'request.matches(\"^[a-z]+$\") == true'\n" + + " match:\n" + + " - condition: 'variables.approx_var'\n" + + " output: 'true'\n" + + " - output: 'false'\n" + + "verification:\n" + + " invariants:\n" + + " - id: check_approx\n" + + " assert:\n" + + " - 'variables.approx_var == true'\n"; + CelPolicy policy = PARSER.parse(yamlPolicy); + + ImmutableMap results = VERIFIER.verifyInvariants(policy); + + assertThat(results).containsKey("check_approx"); + assertThat(results.get("check_approx").status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + } } diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index 6d046b6b9..4ab1d050f 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; @@ -2454,4 +2455,21 @@ public void verifyEquivalence_timeoutReached_throwsCelVerificationException() th CelVerificationException.class, () -> timeoutVerifier.verifyEquivalence(astA, astB)); assertThat(e).hasMessageThat().containsMatch("timeout|canceled"); } + + @Test + public void verifyImplication_loopExceedsLimit_returnsTruncatedInconclusive() throws Exception { + CelAbstractSyntaxTree assumeAst = + CEL.compile("size(int_list) <= 2 && int_list[0] > 0 && int_list[1] > 0").getAst(); + CelAbstractSyntaxTree assertAst = CEL.compile("int_list.all(x, x > 0)").getAst(); + + CelVerifier verifier = + CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(2).build(); + CelVerificationResult result = + ((CelVerifierZ3Impl) verifier) + .verifyImplication(assumeAst, assertAst, ImmutableMap.of()); + + assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + assertThat(result.message()) + .contains("implication holds within the current loop unroll limit"); + } } diff --git a/verifier/src/test/java/dev/cel/verifier/VerifierTestHelper.java b/verifier/src/test/java/dev/cel/verifier/VerifierTestHelper.java new file mode 100644 index 000000000..b54d01d44 --- /dev/null +++ b/verifier/src/test/java/dev/cel/verifier/VerifierTestHelper.java @@ -0,0 +1,58 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.base.Ascii; +import com.google.common.io.Files; +import com.google.devtools.build.runfiles.AutoBazelRepository; +import com.google.devtools.build.runfiles.Runfiles; +import java.io.File; +import java.io.IOException; + +/** Package-private class to assist with verifier testing and runfiles resolution. */ +@AutoBazelRepository +final class VerifierTestHelper { + + private static final Runfiles runfiles = createRunfiles(); + + static String loadVerificationPolicyYaml(String filename) throws IOException { + String rlocationPath = + "cel_java/testing/src/test/resources/policy/verification/" + filename; + String resolvedPath = runfiles.rlocation(Ascii.toLowerCase(rlocationPath)); + if (resolvedPath == null) { + throw new IOException("Unmapped runfile path: " + rlocationPath); + } + File file = new File(resolvedPath); + if (!file.exists()) { + throw new IOException( + String.format( + "Runfile not found on disk at '%s' (unresolved path: '%s')", + resolvedPath, rlocationPath)); + } + return Files.asCharSource(file, UTF_8).read(); + } + + private static Runfiles createRunfiles() { + try { + return Runfiles.preload().withSourceRepository(AutoBazelRepository_VerifierTestHelper.NAME); + } catch (IOException e) { + throw new RuntimeException("Failed to initialize Runfiles", e); + } + } + + private VerifierTestHelper() {} +} From c11a0f5565649a9783151800d92fece284919529 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Tue, 21 Jul 2026 14:18:47 -0700 Subject: [PATCH 132/204] Preserve the original CelExpr from symbol table for comprehensions in verifier PiperOrigin-RevId: 951695444 --- .../main/java/dev/cel/verifier/CelAstToZ3Translator.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java index 255658136..4a0086b16 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java @@ -644,6 +644,12 @@ private T withScope(String varName, TranslatedValue value, Supplier actio private TranslatedValue translateComprehension(CelExpr celExpr, CelAbstractSyntaxTree ast) { CelComprehension comp = celExpr.comprehension(); CelExpr iterRangeExpr = comp.iterRange(); + if (iterRangeExpr.exprKind().getKind() == ExprKind.Kind.IDENT) { + TranslatedValue boundTv = symbolTable.get(iterRangeExpr.ident().name()); + if (boundTv != null) { + iterRangeExpr = boundTv.celExpr().orElse(iterRangeExpr); + } + } List iterationElements = new ArrayList<>(); List taints = new ArrayList<>(); List> allRangeElems = new ArrayList<>(); From 250bff39ee8ec69004d64853e3c5af9a2671813d Mon Sep 17 00:00:00 2001 From: Stanley Cheung Date: Wed, 22 Jul 2026 02:47:44 -0700 Subject: [PATCH 133/204] Port CEL-Go basic Protobuf constant folding syntax to CEL-Java PiperOrigin-RevId: 951988825 --- .../main/java/dev/cel/bundle/CelBuilder.java | 3 + .../src/main/java/dev/cel/bundle/CelImpl.java | 5 + .../dev/cel/optimizer/optimizers/BUILD.bazel | 5 + .../optimizers/ConstantFoldingOptimizer.java | 81 +++++- .../dev/cel/optimizer/optimizers/BUILD.bazel | 2 + .../ConstantFoldingOptimizerTest.java | 252 ++++++++++++++++++ .../dev/cel/runtime/CelRuntimeBuilder.java | 3 + .../java/dev/cel/runtime/CelRuntimeImpl.java | 4 +- .../dev/cel/runtime/CelRuntimeLegacyImpl.java | 5 + 9 files changed, 348 insertions(+), 12 deletions(-) diff --git a/bundle/src/main/java/dev/cel/bundle/CelBuilder.java b/bundle/src/main/java/dev/cel/bundle/CelBuilder.java index f603b479f..a45f846e4 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelBuilder.java +++ b/bundle/src/main/java/dev/cel/bundle/CelBuilder.java @@ -211,6 +211,9 @@ public interface CelBuilder { @CanIgnoreReturnValue CelBuilder setValueProvider(CelValueProvider celValueProvider); + /** Returns the configured {@link CelValueProvider}, or null if not set. */ + CelValueProvider valueProvider(); + /** * Set the {@code typeProvider} for use with type-checking expressions. * diff --git a/bundle/src/main/java/dev/cel/bundle/CelImpl.java b/bundle/src/main/java/dev/cel/bundle/CelImpl.java index f0db128c1..999f1573a 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelImpl.java +++ b/bundle/src/main/java/dev/cel/bundle/CelImpl.java @@ -317,6 +317,11 @@ public CelBuilder setValueProvider(CelValueProvider celValueProvider) { return this; } + @Override + public CelValueProvider valueProvider() { + return runtimeBuilder.valueProvider(); + } + @Override @Deprecated public Builder setTypeProvider(TypeProvider typeProvider) { diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel index 35476a792..da722d521 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -31,6 +31,10 @@ java_library( "//common/navigation:common", "//common/navigation:mutable_navigation", "//common/types", + "//common/types:type_providers", + "//common/values", + "//common/values:cel_value", + "//common/values:cel_value_provider", "//extensions:optional_library", "//optimizer:ast_optimizer", "//optimizer:mutable_ast", @@ -40,6 +44,7 @@ java_library( "//runtime:unknown_attributes", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", ], ) diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java index 35d181905..0fcbb497c 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CanIgnoreReturnValue; import dev.cel.bundle.Cel; +import dev.cel.bundle.CelBuilder; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelMutableAst; import dev.cel.common.CelSource; @@ -42,7 +43,13 @@ import dev.cel.common.navigation.CelNavigableMutableAst; import dev.cel.common.navigation.CelNavigableMutableExpr; import dev.cel.common.navigation.TraversalOrder; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypeProvider; import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructType; +import dev.cel.common.values.CelValue; +import dev.cel.common.values.CelValueProvider; +import dev.cel.common.values.StructValue; import dev.cel.extensions.CelOptionalLibrary.Function; import dev.cel.optimizer.AstMutator; import dev.cel.optimizer.CelAstOptimizer; @@ -59,6 +66,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import org.jspecify.annotations.Nullable; /** * Performs optimization for inlining constant scalar and aggregate literal values within function @@ -95,8 +103,16 @@ private static CelMutableExpr newOptionalNoneExpr() { @Override public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) throws CelOptimizationException { + CelBuilder builder = cel.toCelBuilder(); + CelValueProvider valueProvider; + try { + valueProvider = builder.valueProvider(); + } catch (UnsupportedOperationException e) { + // Legacy runtime does not support valueProvider and may throw. + valueProvider = null; + } // Override the environment's expected type to generally allow all subtrees to be folded. - Cel optimizerEnv = cel.toCelBuilder().setResultType(SimpleType.DYN).build(); + Cel optimizerEnv = builder.setResultType(SimpleType.DYN).build(); CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); int iterCount = 0; @@ -123,7 +139,7 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) if (!mutatedResult.isPresent()) { // Evaluate the call then fold try { - mutatedResult = maybeFold(optimizerEnv, mutableAst, foldableExpr); + mutatedResult = maybeFold(optimizerEnv, valueProvider, mutableAst, foldableExpr); } catch (CelEvaluationException e) { throw new CelOptimizationException( "Constant folding failure. Failed to evaluate subtree due to: " + e.getMessage(), @@ -290,7 +306,10 @@ private static boolean isNestedComprehension(CelNavigableMutableExpr expr) { } private Optional maybeFold( - Cel cel, CelMutableAst mutableAst, CelNavigableMutableExpr node) + Cel cel, + CelValueProvider valueProvider, + CelMutableAst mutableAst, + CelNavigableMutableExpr node) throws CelOptimizationException, CelEvaluationException { Object result; try { @@ -305,10 +324,12 @@ private Optional maybeFold( // ex2: optional.ofNonZeroValue(5) -> optional.of(5) if (result instanceof Optional) { Optional optResult = ((Optional) result); - return maybeRewriteOptional(optResult, mutableAst, node.expr()); + return maybeRewriteOptional( + cel.getTypeProvider(), valueProvider, optResult, mutableAst, node.expr()); } - CelMutableExpr adaptedResult = maybeAdaptEvaluatedResult(result).orElse(null); + CelMutableExpr adaptedResult = + maybeAdaptEvaluatedResult(cel.getTypeProvider(), valueProvider, result).orElse(null); if (adaptedResult == null) { return Optional.empty(); } @@ -316,14 +337,20 @@ private Optional maybeFold( return Optional.of(astMutator.replaceSubtree(mutableAst, adaptedResult, node.id())); } - private Optional maybeAdaptEvaluatedResult(Object result) { + private Optional maybeAdaptEvaluatedResult( + CelTypeProvider typeProvider, @Nullable CelValueProvider valueProvider, Object result) { + if (valueProvider != null && !(result instanceof CelValue)) { + result = valueProvider.celValueConverter().toRuntimeValue(result); + } + if (CelConstant.isConstantValue(result)) { return Optional.of(CelMutableExpr.ofConstant(CelConstant.ofObjectValue(result))); } else if (result instanceof Collection) { Collection collection = (Collection) result; List listElements = new ArrayList<>(); for (Object evaluatedElement : collection) { - CelMutableExpr adaptedExpr = maybeAdaptEvaluatedResult(evaluatedElement).orElse(null); + CelMutableExpr adaptedExpr = + maybeAdaptEvaluatedResult(typeProvider, valueProvider, evaluatedElement).orElse(null); if (adaptedExpr == null) { return Optional.empty(); } @@ -335,11 +362,13 @@ private Optional maybeAdaptEvaluatedResult(Object result) { Map map = (Map) result; List mapEntries = new ArrayList<>(); for (Map.Entry entry : map.entrySet()) { - CelMutableExpr adaptedKey = maybeAdaptEvaluatedResult(entry.getKey()).orElse(null); + CelMutableExpr adaptedKey = + maybeAdaptEvaluatedResult(typeProvider, valueProvider, entry.getKey()).orElse(null); if (adaptedKey == null) { return Optional.empty(); } - CelMutableExpr adaptedValue = maybeAdaptEvaluatedResult(entry.getValue()).orElse(null); + CelMutableExpr adaptedValue = + maybeAdaptEvaluatedResult(typeProvider, valueProvider, entry.getValue()).orElse(null); if (adaptedValue == null) { return Optional.empty(); } @@ -364,6 +393,31 @@ private Optional maybeAdaptEvaluatedResult(Object result) { CelMutableExpr.ofConstant(CelConstant.ofValue(timestampStrArg))); return Optional.of(CelMutableExpr.ofCall(timestampCall)); + } else if (result instanceof StructValue) { + @SuppressWarnings("unchecked") // Unchecked: StructValue only supports String keys. + StructValue structValue = (StructValue) result; + List structEntries = new ArrayList<>(); + + String typeName = structValue.celType().name(); + CelType optType = typeProvider.findType(typeName).orElse(null); + if (!(optType instanceof StructType)) { + return Optional.empty(); + } + StructType structType = (StructType) optType; + for (String fieldName : structType.fieldNames()) { + Optional fieldOpt = structValue.find(fieldName); + if (!fieldOpt.isPresent()) { + continue; + } + CelMutableExpr adaptedFieldExpr = + maybeAdaptEvaluatedResult(typeProvider, valueProvider, fieldOpt.get()).orElse(null); + if (adaptedFieldExpr == null) { + return Optional.empty(); + } + structEntries.add(CelMutableStruct.Entry.create(0, fieldName, adaptedFieldExpr)); + } + return Optional.of( + CelMutableExpr.ofStruct(CelMutableStruct.create(structType.name(), structEntries))); } // Evaluated result cannot be folded (e.g: unknowns) @@ -371,7 +425,11 @@ private Optional maybeAdaptEvaluatedResult(Object result) { } private Optional maybeRewriteOptional( - Optional optResult, CelMutableAst mutableAst, CelMutableExpr expr) { + CelTypeProvider typeProvider, + CelValueProvider valueProvider, + Optional optResult, + CelMutableAst mutableAst, + CelMutableExpr expr) { Object unwrappedResult = optResult.orElse(null); if (unwrappedResult == null) { if (isCallToFunction(expr, Function.OPTIONAL_NONE.getFunction())) { @@ -387,7 +445,8 @@ private Optional maybeRewriteOptional( return Optional.empty(); } - CelMutableExpr adaptedResult = maybeAdaptEvaluatedResult(unwrappedResult).orElse(null); + CelMutableExpr adaptedResult = + maybeAdaptEvaluatedResult(typeProvider, valueProvider, unwrappedResult).orElse(null); if (adaptedResult == null) { // Evaluated result is not an adaptable constant. Leave the optional as is. return Optional.empty(); diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel index 53d72de67..c912d9570 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -42,8 +42,10 @@ java_library( "@maven//:junit_junit", "@maven//:com_google_testparameterinjector_test_parameter_injector", "//:java_truth", + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", ], ) diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java index ec4ffd6bc..3cb388408 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java @@ -18,6 +18,8 @@ import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; @@ -31,6 +33,9 @@ import dev.cel.common.types.ListType; import dev.cel.common.types.MapType; import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.expr.conformance.proto2.TestAllTypes.NestedMessage; +import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.extensions.CelExtensions; import dev.cel.extensions.CelOptionalLibrary; @@ -91,6 +96,7 @@ private static Cel setupEnv(CelBuilder celBuilder) { .addFunctionBindings( CelFunctionBinding.from("get_true_overload", ImmutableList.of(), unused -> true)) .addMessageTypes(TestAllTypes.getDescriptor()) + .addMessageTypes(dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor()) .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .setOptions(CEL_OPTIONS) .addCompilerLibraries( @@ -196,6 +202,8 @@ private static Cel setupEnv(CelBuilder celBuilder) { @TestParameters( "{source: 'TestAllTypes{single_nested_message: TestAllTypes.NestedMessage{bb:" + " 42}}.single_nested_message.bb', expected: '42'}") + @TestParameters("{source: 'TestAllTypes{single_int64: 1 + 2 + 3}.single_int64', expected: '6'}") + @TestParameters("{source: 'TestAllTypes{single_int64: 3}.single_int64', expected: '3'}") @TestParameters("{source: '{\"a\": 1}[\"a\"]', expected: '1'}") @TestParameters("{source: '{\"a\": {\"b\": 2}}[\"a\"][\"b\"]', expected: '2'}") @TestParameters("{source: '{\"hello\": \"world\"}.hello == x', expected: '\"world\" == x'}") @@ -301,6 +309,52 @@ public void constantFold_success(String source, String expected) throws Exceptio assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo(expected); } + @Test + @TestParameters( + "{source: 'TestAllTypes{single_int32: 3}', " + + " expected: 'cel.expr.conformance.proto3.TestAllTypes{single_int32: 3}'}") + @TestParameters( + "{source: 'TestAllTypes{single_float: 1.5}', " + + " expected: 'cel.expr.conformance.proto3.TestAllTypes{single_float: 1.5}'}") + @TestParameters( + "{source: 'TestAllTypes{single_nested_message: TestAllTypes.NestedMessage{bb: 42}}', " + + " expected: 'cel.expr.conformance.proto3.TestAllTypes{single_nested_message:" + + " cel.expr.conformance.proto3.TestAllTypes.NestedMessage{bb: 42}}'}") + @TestParameters( + "{source: 'TestAllTypes{repeated_int32: [1, 2, 3]}', " + + " expected: 'cel.expr.conformance.proto3.TestAllTypes{repeated_int32: [1, 2, 3]}'}") + @TestParameters( + "{source: 'TestAllTypes{map_int32_int64: {1: 2}}', " + + " expected: 'cel.expr.conformance.proto3.TestAllTypes{map_int32_int64: {1: 2}}'}") + @TestParameters( + "{source: 'TestAllTypes{single_any: google.protobuf.Any{type_url:" + + " \"type.googleapis.com/google.protobuf.Int32Value\", value: b\"\\010\\001\"}}', " + + " expected: 'cel.expr.conformance.proto3.TestAllTypes{single_any:" + + " google.protobuf.Any{type_url:" + + " \"type.googleapis.com/google.protobuf.Int32Value\", value: b\"\\010\\001\"}}'}") + @TestParameters( + "{source: '[TestAllTypes{single_int32: 42}][0]', " + + " expected: 'cel.expr.conformance.proto3.TestAllTypes{single_int32: 42}'}") + @TestParameters( + "{source: 'TestAllTypes{single_bytes: b\"\\010\\001\"}', " + + " expected: 'cel.expr.conformance.proto3.TestAllTypes{single_bytes: b\"\\010\\001\"}'}") + @TestParameters( + "{source: 'TestAllTypes{standalone_enum: 1}', " + + " expected: 'cel.expr.conformance.proto3.TestAllTypes{standalone_enum: 1}'}") + public void constantFold_protoMessageLiteral_success(String source, String expected) + throws Exception { + // Legacy runtime does not support adapting protobuf messages into CelValue (via + // CelValueProvider). + if (runtimeFlavor.equals(CelRuntimeFlavor.LEGACY)) { + return; + } + CelAbstractSyntaxTree ast = cel.compile(source).getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo(expected); + } + @Test @TestParameters("{source: '[1 + 1, 1 + 2].exists(i, i < 10)', expected: 'true'}") @TestParameters("{source: '[1, 1 + 1, 1 + 2, 2 + 3].exists(i, i < 10)', expected: 'true'}") @@ -467,6 +521,204 @@ public void constantFold_addFoldableFunction_success() throws Exception { assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo("true"); } + @Test + public void constantFold_protoMessage_success() throws Exception { + // Legacy runtime does not support adapting protobuf messages into CelValue (via + // CelValueProvider). + if (runtimeFlavor.equals(CelRuntimeFlavor.LEGACY)) { + return; + } + Cel customCel = + cel.toCelBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "get_test_all_types", + CelOverloadDecl.newGlobalOverload( + "get_test_all_types_overload", + StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")))) + .addFunctionBindings( + CelFunctionBinding.from( + "get_test_all_types_overload", + ImmutableList.of(), + unused -> + TestAllTypes.newBuilder() + .setSingleInt32(1) + .setSingleString("hello") + .build())) + .build(); + CelAbstractSyntaxTree ast = customCel.compile("get_test_all_types()").getAst(); + ConstantFoldingOptions options = + ConstantFoldingOptions.newBuilder().addFoldableFunctions("get_test_all_types").build(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(customCel) + .addAstOptimizers(ConstantFoldingOptimizer.newInstance(options)) + .build(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo( + "cel.expr.conformance.proto3.TestAllTypes{single_int32: 1, single_string: \"hello\"}"); + } + + @Test + public void constantFold_protoMessage_complexFields_success() throws Exception { + // Legacy runtime does not support adapting protobuf messages into CelValue (via + // CelValueProvider). + if (runtimeFlavor.equals(CelRuntimeFlavor.LEGACY)) { + return; + } + Cel customCel = + cel.toCelBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "get_test_all_types_complex", + CelOverloadDecl.newGlobalOverload( + "get_test_all_types_complex_overload", + StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")))) + .addFunctionBindings( + CelFunctionBinding.from( + "get_test_all_types_complex_overload", + ImmutableList.of(), + unused -> + TestAllTypes.newBuilder() + .setSingleUint32(123) + .setSingleUint64(456L) + .setSingleDuration(Duration.newBuilder().setSeconds(10).build()) + .setSingleTimestamp(Timestamp.newBuilder().setSeconds(10).build()) + .addRepeatedNestedMessage( + TestAllTypes.NestedMessage.newBuilder().setBb(99).build()) + .build())) + .build(); + CelAbstractSyntaxTree ast = customCel.compile("get_test_all_types_complex()").getAst(); + ConstantFoldingOptions options = + ConstantFoldingOptions.newBuilder() + .addFoldableFunctions("get_test_all_types_complex") + .build(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(customCel) + .addAstOptimizers(ConstantFoldingOptimizer.newInstance(options)) + .build(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)).contains("123u"); + } + + @Test + public void constantFold_proto2Message_success() throws Exception { + // Legacy runtime does not support adapting protobuf messages into CelValue (via + // CelValueProvider). + if (runtimeFlavor.equals(CelRuntimeFlavor.LEGACY)) { + return; + } + Cel customCel = + cel.toCelBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "get_test_all_types_proto2", + CelOverloadDecl.newGlobalOverload( + "get_test_all_types_proto2_overload", + StructTypeReference.create("cel.expr.conformance.proto2.TestAllTypes")))) + .addFunctionBindings( + CelFunctionBinding.from( + "get_test_all_types_proto2_overload", + ImmutableList.of(), + unused -> + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() + .setSingleInt32(2) + .setExtension(TestAllTypesExtensions.int32Ext, 3) + .build())) + .build(); + CelAbstractSyntaxTree ast = customCel.compile("get_test_all_types_proto2()").getAst(); + ConstantFoldingOptions options = + ConstantFoldingOptions.newBuilder() + .addFoldableFunctions("get_test_all_types_proto2") + .build(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(customCel) + .addAstOptimizers(ConstantFoldingOptimizer.newInstance(options)) + .build(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo("cel.expr.conformance.proto2.TestAllTypes{single_int32: 2}"); + } + + @Test + public void constantFold_proto2Message_complexFields_success() throws Exception { + // Legacy runtime does not support adapting protobuf messages into CelValue (via + // CelValueProvider). + if (runtimeFlavor.equals(CelRuntimeFlavor.LEGACY)) { + return; + } + Cel customCel = + cel.toCelBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "get_test_all_types_proto2_complex", + CelOverloadDecl.newGlobalOverload( + "get_test_all_types_proto2_complex_overload", + StructTypeReference.create("cel.expr.conformance.proto2.TestAllTypes")))) + .addFunctionBindings( + CelFunctionBinding.from( + "get_test_all_types_proto2_complex_overload", + ImmutableList.of(), + unused -> + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() + .setSingleUint32(123) + .setSingleUint64(456L) + .addRepeatedNestedMessage(NestedMessage.newBuilder().setBb(99).build()) + .build())) + .build(); + CelAbstractSyntaxTree ast = customCel.compile("get_test_all_types_proto2_complex()").getAst(); + ConstantFoldingOptions options = + ConstantFoldingOptions.newBuilder() + .addFoldableFunctions("get_test_all_types_proto2_complex") + .build(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(customCel) + .addAstOptimizers(ConstantFoldingOptimizer.newInstance(options)) + .build(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)).contains("123u"); + } + + @Test + public void constantFold_functionReturningUnregisteredMessage_doesNotFold() throws Exception { + Cel customCel = + runtimeFlavor + .builder() + .addVar("x", SimpleType.DYN) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "get_unregistered_message", + CelOverloadDecl.newGlobalOverload( + "get_unregistered_message_overload", SimpleType.ANY))) + .addFunctionBindings( + CelFunctionBinding.from( + "get_unregistered_message_overload", + ImmutableList.of(), + unused -> TestAllTypes.getDefaultInstance())) + .build(); + ConstantFoldingOptions options = + ConstantFoldingOptions.newBuilder() + .addFoldableFunctions("get_unregistered_message") + .build(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(customCel) + .addAstOptimizers(ConstantFoldingOptimizer.newInstance(options)) + .build(); + CelAbstractSyntaxTree ast = customCel.compile("get_unregistered_message()").getAst(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo("get_unregistered_message()"); + } + @Test public void constantFold_withExpectedResultTypeSet_success() throws Exception { Cel cel = runtimeFlavor.builder().setResultType(SimpleType.STRING).build(); diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java index 87f11fde2..e284b374c 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java @@ -167,6 +167,9 @@ public interface CelRuntimeBuilder { @CanIgnoreReturnValue CelRuntimeBuilder setValueProvider(CelValueProvider celValueProvider); + /** Returns the configured {@link CelValueProvider}, or null if not set. */ + CelValueProvider valueProvider(); + /** Enable or disable the standard CEL library functions and variables. */ @CanIgnoreReturnValue CelRuntimeBuilder setStandardEnvironmentEnabled(boolean value); diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index b02f64b61..f934108e0 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -286,7 +286,8 @@ public abstract static class Builder implements CelRuntimeBuilder { abstract CelTypeProvider typeProvider(); - abstract CelValueProvider valueProvider(); + @Override + public abstract CelValueProvider valueProvider(); abstract CelStandardFunctions standardFunctions(); @@ -503,6 +504,7 @@ public CelRuntime build() { protoMessageValueProvider = CombinedCelValueProvider.combine(protoMessageValueProvider, valueProvider()); } + setValueProvider(protoMessageValueProvider); CelValueConverter celValueConverter = protoMessageValueProvider.celValueConverter(); CelTypeProvider messageTypeProvider = diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java index c5e06d013..cad7e74f8 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java @@ -207,6 +207,11 @@ public CelRuntimeBuilder setValueProvider(CelValueProvider celValueProvider) { "setValueProvider is not supported for legacy runtime"); } + @Override + public CelValueProvider valueProvider() { + throw new UnsupportedOperationException("valueProvider is not supported for legacy runtime"); + } + @Override public CelRuntimeBuilder setTypeFactory(Function typeFactory) { this.customTypeFactory = typeFactory; From f4ee04978e376fad0868341299140ac593ae8c12 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Wed, 22 Jul 2026 15:23:19 -0700 Subject: [PATCH 134/204] Add optional pruning operator (?foo) handling to CEL Java's verifier PiperOrigin-RevId: 952362510 --- .../dev/cel/verifier/CelAstAlphaHasher.java | 4 +- .../cel/verifier/CelAstToZ3Translator.java | 57 +++++++++++++++---- .../cel/verifier/CelZ3OperatorTranslator.java | 13 ++++- .../cel/verifier/CelVerifierZ3ImplTest.java | 14 ++++- 4 files changed, 74 insertions(+), 14 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstAlphaHasher.java b/verifier/src/main/java/dev/cel/verifier/CelAstAlphaHasher.java index 7991290b0..c0491085f 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelAstAlphaHasher.java +++ b/verifier/src/main/java/dev/cel/verifier/CelAstAlphaHasher.java @@ -118,7 +118,9 @@ private static void hashAst(CelExpr expr, @Nullable Scope scope, HasherContext c break; case LIST: context.hasher.putInt(expr.list().elements().size()); - for (CelExpr elem : expr.list().elements()) { + for (int i = 0; i < expr.list().elements().size(); i++) { + CelExpr elem = expr.list().elements().get(i); + context.hasher.putBoolean(expr.list().optionalIndices().contains(i)); hashAst(elem, scope, context); } break; diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java index 4a0086b16..e47f176ca 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java @@ -284,11 +284,24 @@ private TranslatedValue translateList(CelExpr celExpr, CelAbstractSyntaxTree ast // check to a trivial identity check (e.g., `list_ref_0 == list_ref_0`). if (listRef == null) { SeqExpr seq = ctx.mkEmptySeq(ctx.mkSeqSort(typeSystem.celValueSort())); - for (CelExpr element : createList.elements()) { + ImmutableList optionalIndices = createList.optionalIndices(); + ImmutableList elements = createList.elements(); + for (int i = 0; i < elements.size(); i++) { + CelExpr element = elements.get(i); TranslatedValue elem = translateExpr(element, ast); elementsTv.add(elem); - seq = typeSystem.mkConcatSafe(seq, ctx.mkUnit(elem.z3Expr())); + if (optionalIndices.contains(i)) { + Expr optRef = typeSystem.getOptionalRef(elem.z3Expr()); + seq = + (SeqExpr) + ctx.mkITE( + typeSystem.optHasValue(optRef), + typeSystem.mkConcatSafe(seq, ctx.mkUnit(typeSystem.getOptionalValue(optRef))), + seq); + } else { + seq = typeSystem.mkConcatSafe(seq, ctx.mkUnit(elem.z3Expr())); + } } listRef = typeSystem.mkListRefConst(LIST_REF_PREFIX); typeConstraints.add(ctx.mkEq(typeSystem.getSeq(listRef), seq)); @@ -318,12 +331,24 @@ private TranslatedValue translateMap(CelExpr celExpr, CelAbstractSyntaxTree ast) Expr value = valueTv.z3Expr(); elementsTv.add(valueTv); + Expr finalValue = value; + BoolExpr finalPresence = ctx.mkTrue(); + if (entryAst.optionalEntry()) { + Expr optRef = typeSystem.getOptionalRef(value); + finalPresence = typeSystem.optHasValue(optRef); + finalValue = typeSystem.getOptionalValue(optRef); + } + BoolExpr keyAlreadyPresent = (BoolExpr) ctx.mkSelect(mapPresence, key); + BoolExpr shouldInsertKey = ctx.mkAnd(ctx.mkNot(keyAlreadyPresent), finalPresence); keysSeq = - ctx.mkITE(keyAlreadyPresent, keysSeq, typeSystem.mkConcatSafe(keysSeq, ctx.mkUnit(key))); + ctx.mkITE(shouldInsertKey, typeSystem.mkConcatSafe(keysSeq, ctx.mkUnit(key)), keysSeq); - mapValues = ctx.mkStore(mapValues, key, value); - mapPresence = ctx.mkStore(mapPresence, key, ctx.mkTrue()); + mapValues = + (ArrayExpr) ctx.mkITE(finalPresence, ctx.mkStore(mapValues, key, finalValue), mapValues); + mapPresence = + (ArrayExpr) + ctx.mkITE(finalPresence, ctx.mkStore(mapPresence, key, ctx.mkTrue()), mapPresence); } typeConstraints.add(ctx.mkEq(typeSystem.getMapValues(mapRef), mapValues)); @@ -371,6 +396,14 @@ private TranslatedValue translateStruct(CelExpr celExpr, CelAbstractSyntaxTree a .orElseGet(() -> extractAstTypeOrDefault(ast, entryAst.value().id())); Expr defaultVal = getDefaultValueForType(fieldType); + Expr finalValue = value; + BoolExpr optionalHasValue = ctx.mkTrue(); + if (entryAst.optionalEntry()) { + Expr optRef = typeSystem.getOptionalRef(value); + optionalHasValue = typeSystem.optHasValue(optRef); + finalValue = typeSystem.getOptionalValue(optRef); + } + // Canonicalization Trick: // // We avoid storing explicit default values (e.g. `single_int32: 0`) @@ -379,11 +412,13 @@ private TranslatedValue translateStruct(CelExpr celExpr, CelAbstractSyntaxTree a // (`msg1 == msg2`) to work without using quantifiers (which avoids MBQI loops). // Because proto3 singular primitives do not have field presence, we also skip setting // `msgPresence`. - BoolExpr shouldBypass = - fieldType.kind().isPrimitive() ? ctx.mkEq(value, defaultVal) : ctx.mkFalse(); + BoolExpr isDefaultPrimitive = + fieldType.kind().isPrimitive() ? ctx.mkEq(finalValue, defaultVal) : ctx.mkFalse(); + + BoolExpr shouldBypass = ctx.mkOr(ctx.mkNot(optionalHasValue), isDefaultPrimitive); msgValues = - (ArrayExpr) ctx.mkITE(shouldBypass, msgValues, ctx.mkStore(msgValues, key, value)); + (ArrayExpr) ctx.mkITE(shouldBypass, msgValues, ctx.mkStore(msgValues, key, finalValue)); msgPresence = (ArrayExpr) @@ -655,7 +690,8 @@ private TranslatedValue translateComprehension(CelExpr celExpr, CelAbstractSynta List> allRangeElems = new ArrayList<>(); // For statically known list/map literals, unroll them exactly. - if (iterRangeExpr.exprKind().getKind() == ExprKind.Kind.LIST) { + if (iterRangeExpr.exprKind().getKind() == ExprKind.Kind.LIST + && iterRangeExpr.list().optionalIndices().isEmpty()) { ImmutableList elements = iterRangeExpr.list().elements(); for (int i = 0; i < elements.size(); i++) { TranslatedValue valueTv = translateExpr(elements.get(i), ast); @@ -664,7 +700,8 @@ private TranslatedValue translateComprehension(CelExpr celExpr, CelAbstractSynta iterationElements.add(new IterationElement(typeSystem.mkInt(i), value)); allRangeElems.add(value); } - } else if (iterRangeExpr.exprKind().getKind() == ExprKind.Kind.MAP) { + } else if (iterRangeExpr.exprKind().getKind() == ExprKind.Kind.MAP + && iterRangeExpr.map().entries().stream().noneMatch(CelExpr.CelMap.Entry::optionalEntry)) { for (CelExpr.CelMap.Entry entry : iterRangeExpr.map().entries()) { TranslatedValue keyTv = translateExpr(entry.key(), ast); Expr key = keyTv.z3Expr(); diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java index f13411f28..dca62bc11 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java @@ -497,6 +497,11 @@ private BoolExpr getDynamicNumericEquality(Expr z3Expr0, Expr z3Expr1) { .build(ctx.mkFalse()); } + private boolean hasOptionalElements(TranslatedValue arg) { + return arg.isLiteral(ExprKind.Kind.LIST) + && !arg.celExpr().get().list().optionalIndices().isEmpty(); + } + private BoolExpr unrollListEquality( TranslatedValue listA, TranslatedValue listB, CelAbstractSyntaxTree ast) { CelExpr literalListAst = @@ -544,7 +549,9 @@ private TranslatedValue translateEquality( equality = getNumericEquality(arg0, arg1, ast); } else if (type0.kind() == CelKind.LIST && type1.kind() == CelKind.LIST - && (arg0.isLiteral(ExprKind.Kind.LIST) || arg1.isLiteral(ExprKind.Kind.LIST))) { + && (arg0.isLiteral(ExprKind.Kind.LIST) || arg1.isLiteral(ExprKind.Kind.LIST)) + && !hasOptionalElements(arg0) + && !hasOptionalElements(arg1)) { equality = unrollListEquality(arg0, arg1, ast); } else if (isStaticallyKnown(type0) && isStaticallyKnown(type1)) { equality = typeSystem.getStructuralEquality(z3Arg0, z3Arg1); @@ -554,7 +561,9 @@ private TranslatedValue translateEquality( // Check if one side is an explicit LIST that we can unroll BoolExpr structuralEq = typeSystem.getStructuralEquality(z3Arg0, z3Arg1); - if (arg0.isLiteral(ExprKind.Kind.LIST) || arg1.isLiteral(ExprKind.Kind.LIST)) { + if ((arg0.isLiteral(ExprKind.Kind.LIST) || arg1.isLiteral(ExprKind.Kind.LIST)) + && !hasOptionalElements(arg0) + && !hasOptionalElements(arg1)) { structuralEq = (BoolExpr) ctx.mkITE( diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index 4ab1d050f..a95157891 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -42,6 +42,7 @@ import dev.cel.common.ast.CelExpr.CelCall; import dev.cel.common.types.ListType; import dev.cel.common.types.MapType; +import dev.cel.common.types.OptionalType; import dev.cel.common.types.ProtoMessageTypeProvider; import dev.cel.common.types.SimpleType; import dev.cel.common.types.StructTypeReference; @@ -100,6 +101,7 @@ public final class CelVerifierZ3ImplTest { .addVar("dyn_map", MapType.create(SimpleType.DYN, SimpleType.DYN)) .addVar("dyn_var", SimpleType.DYN) .addVar("dyn_var2", SimpleType.DYN) + .addVar("opt_var", OptionalType.create(SimpleType.INT)) .addVar("string_int_map", MapType.create(SimpleType.STRING, SimpleType.INT)) .addVar("bytes_val", SimpleType.BYTES) .addVar( @@ -1420,7 +1422,15 @@ private enum EquivalenceTestCase { "has(dyn({'a': 1}).a) && has(dyn(TestAllTypes{single_int32: 1}).single_int32)"), DYNAMIC_INDEXING_TYPE_MISMATCH( "type(request) == type(1) && request[1] == 1 && request[2] == 2", - "type(request) == type(1) && 1 / 0 == 1 && request[2] == 2"); + "type(request) == type(1) && 1 / 0 == 1 && request[2] == 2"), + OPTIONAL_PRUNE_LIST_LITERAL("[1, ?optional.of(3)]", "[1,3]"), + OPTIONAL_PRUNE_LIST_NONE("[?optional.none(), ?opt_var]", "[?opt_var]"), + OPTIONAL_PRUNE_MAP_NONE("{?1: optional.none()}", "{}"), + OPTIONAL_PRUNE_STRUCT_LIST( + "TestAllTypes{?repeated_int32: optional.of([1, 2])}", + "cel.expr.conformance.proto3.TestAllTypes{repeated_int32: [1, 2]}"), + OPTIONAL_PRUNE_LIST_EQUALITY("[?optional.none(), 1] == [1]", "true"), + OPTIONAL_PRUNE_LIST_COMPREHENSION("[1, ?optional.none()].all(x, x > 0)", "true"); private final String exprA; private final String exprB; @@ -1458,11 +1468,13 @@ private enum EquivalenceViolationTestCase { HETEROGENEOUS_FIELD_SELECTION( "test_all_types.single_int32 == 10", "test_all_types.single_int64 == 10"), STRUCT_VARIABLE_NOT_EQUIVALENT_TO_DEFAULT("test_all_types == TestAllTypes{}", "true"), + OPTIONAL_INVALID_PRUNE_OPT_VAR("[1, ?opt_var]", "[1]"), CROSS_TYPE_NUMERIC_INEQUALITY_INT_DOUBLE("request == 1.0", "request == 2.0 || request == 1"), CROSS_TYPE_SYMBOLIC_INEQUALITY_INT_UINT("dyn(x) == dyn(u)", "false"), CROSS_TYPE_SYMBOLIC_INEQUALITY_UINT_INT("dyn(u) == dyn(x)", "false"), OPTIONAL_OR_VALUE_VIOLATION("optional.of(x).orValue(y)", "y"), OPTIONAL_VALUE_VIOLATION("optional.of(x).value()", "y"), + LIST_OPTIONAL_ELEMENTS_COLLISION("[1, ?opt_var]", "[1, opt_var]"), CROSS_NUMERIC_EQUALITY_INT_DYN_VIOLATION("1 == request", "false"); final String exprA; From 59cd48f58fe19bf5749c6bd926ccae0af7e06109 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Wed, 22 Jul 2026 15:50:14 -0700 Subject: [PATCH 135/204] Replace quantifiers with array extensionality for map bijection in verifier PiperOrigin-RevId: 952376623 --- .../cel/verifier/CelAstToZ3Translator.java | 38 +++++-------------- .../cel/verifier/CelVerifierZ3ImplTest.java | 5 ++- 2 files changed, 12 insertions(+), 31 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java index e47f176ca..9b7ef0341 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java @@ -23,7 +23,6 @@ import com.microsoft.z3.Expr; import com.microsoft.z3.FuncDecl; import com.microsoft.z3.IntExpr; -import com.microsoft.z3.Pattern; import com.microsoft.z3.Quantifier; import com.microsoft.z3.SeqExpr; import com.microsoft.z3.Sort; @@ -79,7 +78,6 @@ final class CelAstToZ3Translator { private static final String EMPTY_MSG_REF_PREFIX = "!empty_msg_ref_"; private static final String EMPTY_LIST_PREFIX = "!empty_list"; private static final String EMPTY_MAP_PREFIX = "!empty_map"; - private static final String MAP_BIJECTION_PREFIX = "k_map_bijection"; private final Context ctx; private final CelZ3TypeSystem typeSystem; private final CelZ3OperatorTranslator operatorTranslator; @@ -819,36 +817,18 @@ private void applyBoundedMapBijection( } } - Expr kVar = ctx.mkFreshConst(MAP_BIJECTION_PREFIX, typeSystem.celValueSort()); - BoolExpr isValidKey = - ctx.mkOr( - typeSystem.isInt(kVar), typeSystem.isUint(kVar), - typeSystem.isBool(kVar), typeSystem.isString(kVar)); - BoolExpr inMap = (BoolExpr) ctx.mkSelect(mapPresence, kVar); + BoolExpr isNotTruncated = ctx.mkLe(lengthExpr, ctx.mkInt(comprehensionUnrollLimit)); - List inSeqMatches = new ArrayList<>(); + ArrayExpr seqMap = ctx.mkConstArray(typeSystem.celValueSort(), ctx.mkFalse()); for (int i = 0; i < comprehensionUnrollLimit; i++) { - BoolExpr match = - ctx.mkAnd( - ctx.mkLt(ctx.mkInt(i), lengthExpr), ctx.mkEq(kVar, ctx.mkNth(seq, ctx.mkInt(i)))); - inSeqMatches.add(match); + seqMap = + (ArrayExpr) + ctx.mkITE( + ctx.mkLt(ctx.mkInt(i), lengthExpr), + ctx.mkStore(seqMap, ctx.mkNth(seq, ctx.mkInt(i)), ctx.mkTrue()), + seqMap); } - BoolExpr inSeq = CelZ3TypeSystem.mkOrFlattened(ctx, inSeqMatches); - - BoolExpr isNotTruncated = ctx.mkLe(lengthExpr, ctx.mkInt(comprehensionUnrollLimit)); - - Pattern inMapPattern = ctx.mkPattern(inMap); - - BoolExpr completeness = - ctx.mkForall( - new Expr[] {kVar}, - ctx.mkImplies(ctx.mkAnd(isNotTruncated, isValidKey, inMap), inSeq), - 1, - new Pattern[] {inMapPattern}, - null, - null, - null); - typeConstraints.add(completeness); + typeConstraints.add(ctx.mkImplies(isNotTruncated, ctx.mkEq(mapPresence, seqMap))); } private TranslatedValue[] evaluateLoopCondAndStep( diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index a95157891..a8edf15eb 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -1430,8 +1430,9 @@ private enum EquivalenceTestCase { "TestAllTypes{?repeated_int32: optional.of([1, 2])}", "cel.expr.conformance.proto3.TestAllTypes{repeated_int32: [1, 2]}"), OPTIONAL_PRUNE_LIST_EQUALITY("[?optional.none(), 1] == [1]", "true"), - OPTIONAL_PRUNE_LIST_COMPREHENSION("[1, ?optional.none()].all(x, x > 0)", "true"); - + OPTIONAL_PRUNE_LIST_COMPREHENSION("[1, ?optional.none()].all(x, x > 0)", "true"), + MAP_COMPREHENSION( + "{'a': 1, 'b': 2}.exists(k, k == 'a')", "{'a': 1, 'b': 2}.exists(k, k == 'a')"); private final String exprA; private final String exprB; From 63f47511137a73d2dfe477f8c684b107d4587229 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Wed, 22 Jul 2026 20:14:29 -0700 Subject: [PATCH 136/204] Minor fix to readme.md PiperOrigin-RevId: 952483022 --- verifier/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/verifier/README.md b/verifier/README.md index bf98643af..c8d838e95 100644 --- a/verifier/README.md +++ b/verifier/README.md @@ -47,17 +47,17 @@ properties about your expressions. counterexample showing the inputs that caused the violation. When checking satisfiability (`isSatisfiable`), it generates concrete variable assignments (satisfying model / witness) showing the inputs that satisfy the condition. -* **Partial Evaluation (Unknowns) Support:** Define variables that are - permitted to evaluate to `Unknown` during verification, mirroring CEL's - runtime partial evaluation. * **Custom Invariants Verification:** Allows policy authors to define safety invariants (e.g., "port must always be secure if external access is allowed") and mathematically prove that the policy never violates them across all possible input states. +* **Partial Evaluation (Unknowns) Support:** Define variables that are + permitted to evaluate to `Unknown` during verification, mirroring CEL's + runtime partial evaluation. ```java CelVerifier verifier = CelVerifierFactory.newVerifier() - .addUnknownIdentifier("request.headers") // Exclude dynamic fields from failure paths + .addUnknownIdentifier("request.headers") // Exclude unknown fields from failure paths .build(); ``` From 46e1933cbd90984d59eaa37d717c78daceb6a5d7 Mon Sep 17 00:00:00 2001 From: Stephen Roberts Date: Wed, 22 Jul 2026 23:02:06 -0700 Subject: [PATCH 137/204] Internal change PiperOrigin-RevId: 952538565 --- verifier/BUILD.bazel | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/verifier/BUILD.bazel b/verifier/BUILD.bazel index 1c2e5adfa..a7d620d1d 100644 --- a/verifier/BUILD.bazel +++ b/verifier/BUILD.bazel @@ -7,6 +7,9 @@ package( java_library( name = "verifier", + visibility = [ + "//:internal", + ], exports = ["//verifier/src/main/java/dev/cel/verifier"], ) @@ -24,6 +27,9 @@ java_library( java_library( name = "verifier_factory", compatible_with = [], + visibility = [ + "//:internal", + ], exports = ["//verifier/src/main/java/dev/cel/verifier:verifier_factory"], ) From 62d37c8d80dfa2c90a6348137497524026a479eb Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Thu, 23 Jul 2026 13:38:32 -0700 Subject: [PATCH 138/204] Add variable bounds for optionals in verifier PiperOrigin-RevId: 952930226 --- .../cel/verifier/CelAstToZ3Translator.java | 46 ++++- .../cel/verifier/CelVerifierZ3ImplTest.java | 164 ++++++++++++------ 2 files changed, 149 insertions(+), 61 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java index 9b7ef0341..bc9ad676a 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java @@ -40,6 +40,7 @@ import dev.cel.common.types.ListType; import dev.cel.common.types.MapType; import dev.cel.common.types.NullableType; +import dev.cel.common.types.OptionalType; import dev.cel.common.types.SimpleType; import dev.cel.common.types.StructType; import dev.cel.common.types.StructTypeReference; @@ -287,9 +288,13 @@ private TranslatedValue translateList(CelExpr celExpr, CelAbstractSyntaxTree ast for (int i = 0; i < elements.size(); i++) { CelExpr element = elements.get(i); TranslatedValue elem = translateExpr(element, ast); - elementsTv.add(elem); if (optionalIndices.contains(i)) { + Expr checkedValue = + typeSystem.withRuntimeError( + elem.z3Expr(), ctx.mkNot(typeSystem.isOptional(elem.z3Expr()))); + elem = TranslatedValue.create(checkedValue, element, typeSystem, elem.isApproximate()); + Expr optRef = typeSystem.getOptionalRef(elem.z3Expr()); seq = (SeqExpr) @@ -300,6 +305,7 @@ private TranslatedValue translateList(CelExpr celExpr, CelAbstractSyntaxTree ast } else { seq = typeSystem.mkConcatSafe(seq, ctx.mkUnit(elem.z3Expr())); } + elementsTv.add(elem); } listRef = typeSystem.mkListRefConst(LIST_REF_PREFIX); typeConstraints.add(ctx.mkEq(typeSystem.getSeq(listRef), seq)); @@ -327,15 +333,20 @@ private TranslatedValue translateMap(CelExpr celExpr, CelAbstractSyntaxTree ast) elementsTv.add(keyTv); TranslatedValue valueTv = translateExpr(entryAst.value(), ast); Expr value = valueTv.z3Expr(); - elementsTv.add(valueTv); Expr finalValue = value; BoolExpr finalPresence = ctx.mkTrue(); if (entryAst.optionalEntry()) { - Expr optRef = typeSystem.getOptionalRef(value); + Expr checkedValue = + typeSystem.withRuntimeError(value, ctx.mkNot(typeSystem.isOptional(value))); + valueTv = + TranslatedValue.create( + checkedValue, entryAst.value(), typeSystem, valueTv.isApproximate()); + Expr optRef = typeSystem.getOptionalRef(checkedValue); finalPresence = typeSystem.optHasValue(optRef); finalValue = typeSystem.getOptionalValue(optRef); } + elementsTv.add(valueTv); BoolExpr keyAlreadyPresent = (BoolExpr) ctx.mkSelect(mapPresence, key); BoolExpr shouldInsertKey = ctx.mkAnd(ctx.mkNot(keyAlreadyPresent), finalPresence); @@ -382,7 +393,6 @@ private TranslatedValue translateStruct(CelExpr celExpr, CelAbstractSyntaxTree a Expr key = ctx.mkString(entryAst.fieldKey()); TranslatedValue valueTv = translateExpr(entryAst.value(), ast); Expr value = valueTv.z3Expr(); - elementsTv.add(valueTv); CelType fieldType = typeProvider @@ -397,10 +407,16 @@ private TranslatedValue translateStruct(CelExpr celExpr, CelAbstractSyntaxTree a Expr finalValue = value; BoolExpr optionalHasValue = ctx.mkTrue(); if (entryAst.optionalEntry()) { - Expr optRef = typeSystem.getOptionalRef(value); + Expr checkedValue = + typeSystem.withRuntimeError(value, ctx.mkNot(typeSystem.isOptional(value))); + valueTv = + TranslatedValue.create( + checkedValue, entryAst.value(), typeSystem, valueTv.isApproximate()); + Expr optRef = typeSystem.getOptionalRef(checkedValue); optionalHasValue = typeSystem.optHasValue(optRef); finalValue = typeSystem.getOptionalValue(optRef); } + elementsTv.add(valueTv); // Canonicalization Trick: // @@ -436,6 +452,9 @@ private Expr getDefaultValueForType(CelType type) { if (type instanceof NullableType) { return typeSystem.mkNull(); } + if (type instanceof OptionalType) { + return typeSystem.mkOptionalNone(); + } if (type.equals(SimpleType.INT)) { return typeSystem.mkInt(0); } @@ -1147,6 +1166,23 @@ private BoolExpr createTypeConstraint(Expr val, long exprId, CelAbstractSynta } private BoolExpr createTypeConstraintForType(Expr val, CelType type) { + if (type instanceof NullableType) { + NullableType nullableType = (NullableType) type; + return ctx.mkOr( + typeSystem.isNull(val), createTypeConstraintForType(val, nullableType.targetType())); + } + if (type instanceof OptionalType) { + BoolExpr isOpt = typeSystem.isOptional(val); + CelType paramType = type.parameters().get(0); + if (paramType.kind().isDyn() || paramType.kind().isTypeParam()) { + return isOpt; + } + Expr optRef = typeSystem.getOptionalRef(val); + BoolExpr hasValue = typeSystem.optHasValue(optRef); + BoolExpr valConstraint = + createTypeConstraintForType(typeSystem.getOptionalValue(optRef), paramType); + return ctx.mkAnd(isOpt, ctx.mkImplies(hasValue, valConstraint)); + } if (type.equals(SimpleType.BOOL)) { return (BoolExpr) ctx.mkApp(typeSystem.boolCons().getTesterDecl(), val); } diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index a8edf15eb..bd218674a 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -42,6 +42,7 @@ import dev.cel.common.ast.CelExpr.CelCall; import dev.cel.common.types.ListType; import dev.cel.common.types.MapType; +import dev.cel.common.types.NullableType; import dev.cel.common.types.OptionalType; import dev.cel.common.types.ProtoMessageTypeProvider; import dev.cel.common.types.SimpleType; @@ -102,6 +103,8 @@ public final class CelVerifierZ3ImplTest { .addVar("dyn_var", SimpleType.DYN) .addVar("dyn_var2", SimpleType.DYN) .addVar("opt_var", OptionalType.create(SimpleType.INT)) + .addVar("opt_dyn_var", OptionalType.create(SimpleType.DYN)) + .addVar("nullable_int", NullableType.create(SimpleType.INT)) .addVar("string_int_map", MapType.create(SimpleType.STRING, SimpleType.INT)) .addVar("bytes_val", SimpleType.BYTES) .addVar( @@ -147,7 +150,8 @@ private enum IsSatisfiableTestCase { "timestamp('2023-01-01T00:00:00Z') == timestamp('2023-01-01T00:00:00Z')"), CROSS_NUMERIC_EQUALITY_INT_DYN_EXACT("1 == request"), MACRO_LIMIT("dyn_list.all(x, x == 1)"), - STRUCT_FIELD_MISSING_APPROXIMATE_SATISFIABLE("dyn_var.unknown_field"); + STRUCT_FIELD_MISSING_APPROXIMATE_SATISFIABLE("dyn_var.unknown_field"), + NULLABLE_INT_SATISFIABLE("nullable_int == 123"); final String expr; @@ -463,6 +467,8 @@ private enum IsAlwaysTrueTestCase { OPTIONAL_OR_NONE_IS_NONE("optional.none().or(optional.none()) == optional.none()"), OPTIONAL_VALUE_VAR("optional.of(x).value() == x"), OPTIONAL_HAS_VALUE_VAR("optional.of(x).hasValue()"), + OPTIONAL_VAR_HAS_VALUE_IMPLIES_INT("opt_var.hasValue() ? type(opt_var.value()) == int : true"), + IEEE_754_PROTO_NEG_ZERO_NEQ( "TestAllTypes{single_double: -0.0} != TestAllTypes{single_double: 0.0}"), IEEE_754_ROUND_NEAREST_TIES_TO_EVEN_DOWN("1.0 + 1.1102230246251565e-16 == 1.0"), @@ -584,6 +590,7 @@ private enum IsAlwaysTrueTestCase { "dyn_map == {'a': 1, 1: 'b'} ? dyn_map.exists(k, v, type(k) == string && type(v) == int &&" + " k == 'a' && v == 1) : true"), TYPE_AXIOM_BOOL("type(true) == bool"), + NULLABLE_INT_IS_NULL_OR_INT("type(nullable_int) == int || type(nullable_int) == null_type"), TYPE_AXIOM_INT("type(1) == int"), TYPE_AXIOM_UINT("type(1u) == uint"), TYPE_AXIOM_DOUBLE("type(1.0) == double"), @@ -637,6 +644,7 @@ private enum IsAlwaysTrueTestCase { WRAPPER_SET_NULL_IS_NULL( "TestAllTypes{single_int64_wrapper: null}.single_int64_wrapper == null"), WRAPPER_SET_NULL_EQ_UNSET("TestAllTypes{single_int64_wrapper: null} == TestAllTypes{}"), + WRAPPER_SET_NON_NULL_EQ("TestAllTypes{single_int64_wrapper: 123}.single_int64_wrapper == 123"), STRING_CONTAINS_EMPTY("role.contains('')"), STRING_STARTS_WITH_EMPTY("role.startsWith('')"), STRING_ENDS_WITH_EMPTY("role.endsWith('')"), @@ -1013,140 +1021,184 @@ public void verifyEquivalence_infinityConstants_notEquivalent() throws Exception } private enum IsAlwaysTrueViolationTestCase { - NOT_ALWAYS_TRUE("x > 5", "Condition is not always true.", "Counterexample input:", "x ="), + NOT_ALWAYS_TRUE( + "x > 5", "Condition is not always true\\.", "Counterexample input:", "x = -?\\d+"), LAW_OF_EXCLUDED_MIDDLE_FAILS_WITH_ERRORS( - "(1 / 0 == 5) || !(1 / 0 == 5)", "Condition is not always true."), + "(1 / 0 == 5) || !(1 / 0 == 5)", "Condition is not always true\\."), INTEGER_OVERFLOW_FAILS_WITH_ERRORS( - "(x + 1) - 1 == x", "Condition is not always true.", "Counterexample input:", "x ="), + "(x + 1) - 1 == x", + "Condition is not always true\\.", + "Counterexample input:", + "x = -?\\d+"), UINT_SUBTRACT_UNDERFLOW_FAILS_WITH_ERRORS( - "(u - 1u) + 1u == u", "Condition is not always true.", "Counterexample input:", "u ="), + "(u - 1u) + 1u == u", + "Condition is not always true\\.", + "Counterexample input:", + "u = \\d+u?"), NEGATE_MIN_INT_FAILS_WITH_ERRORS( - "-(-x) == x", "Condition is not always true.", "Counterexample input:", "x ="), - HETEROGENEOUS_ARITHMETIC_FAILS("dyn(1) + 1u == 2u", "Condition is not always true."), + "-(-x) == x", "Condition is not always true\\.", "Counterexample input:", "x = -?\\d+"), + HETEROGENEOUS_ARITHMETIC_FAILS("dyn(1) + 1u == 2u", "Condition is not always true\\."), CROSS_TYPE_SYMBOLIC_EQUALITY_NOT_ALWAYS_UNEQUAL_INT_UINT( - "dyn(x) != dyn(u)", "Condition is not always true."), + "dyn(x) != dyn(u)", + "Condition is not always true\\.", + "Counterexample input:", + "x = -?\\d+", + "u = \\d+u?"), CROSS_TYPE_SYMBOLIC_EQUALITY_NOT_ALWAYS_UNEQUAL_UINT_INT( - "dyn(u) != dyn(x)", "Condition is not always true."), + "dyn(u) != dyn(x)", + "Condition is not always true\\.", + "Counterexample input:", + "x = -?\\d+", + "u = \\d+u?"), CROSS_TYPE_DYNAMIC_EQUALITY_NOT_ALWAYS_UNEQUAL_INT_DOUBLE( "!(request == unknown_var && type(request) == int && type(unknown_var) == double)", - "Condition is not always true.", - "Counterexample input:"), + "Condition is not always true\\.", + "Counterexample input:", + "unknown_var = -?\\d+\\.\\d+", + "request = -?\\d+"), + OPTIONAL_DYN_VAR_HAS_VALUE_NOT_IMPLIES_INT( + "opt_dyn_var.hasValue() ? type(opt_dyn_var.value()) == int : true", + "Condition is not always true\\.", + "Counterexample input:", + "opt_dyn_var = \\(Optional OptionalRef!val!\\d+\\)"), + OPTIONAL_ENTRY_DYN_VAR_TYPE_MISMATCH( + "[?dyn_var] == [?dyn_var] ? true : true", + "Condition is not always true\\.", + "Counterexample input:", + "dyn_var = b\\\"![01]!\\\""), + OPTIONAL_MAP_ENTRY_DYN_VAR_TYPE_MISMATCH( + "{?1: dyn_var} == {?1: dyn_var} ? true : true", + "Condition is not always true\\.", + "Counterexample input:", + "dyn_var = b\\\"![01]!\\\""), + OPTIONAL_STRUCT_ENTRY_DYN_VAR_TYPE_MISMATCH( + "cel.expr.conformance.proto3.TestAllTypes{?single_int32: dyn_var} ==" + + " cel.expr.conformance.proto3.TestAllTypes{?single_int32: dyn_var} ? true : true", + "Condition is not always true\\.", + "Counterexample input:", + "dyn_var = b\\\"(!0!|i)\\\""), DYNAMIC_MAP_ALL_VIOLATION( "string_int_map == {'a': 1, 'b': 2} ? string_int_map.all(k, k == 'a') : true", - "Condition is not always true.", + "Condition is not always true\\.", "Counterexample input:", - "string_int_map = {", + "string_int_map = \\{", "\"a\": 1", "\"b\": 2"), DYNAMIC_MAP_EXISTS_VIOLATION( "string_int_map == {'a': 1, 'b': 2} ? string_int_map.exists(k, k == 'c') : true", - "Condition is not always true.", + "Condition is not always true\\.", "Counterexample input:", - "string_int_map = {", + "string_int_map = \\{", "\"a\": 1", "\"b\": 2"), LITERAL_MAP_KEY_ERROR_PROPAGATES( "{1/0: 1}.all(k, true) == true", - "Condition is not always true.", - "(The expression fails unconditionally, regardless of input state)"), + "Condition is not always true\\.", + "\\(The expression fails unconditionally, regardless of input state\\)"), DYNAMIC_LIST_EXISTS_ONE_UNKNOWN_MATH( "int_list == [1, 2] ? int_list.exists_one(x, x == 1 || unknown_var) : true", - "Condition is not always true.", + "Condition is not always true\\.", "Counterexample input:", - "unknown_var =", - "int_list = [1, 2]"), + "unknown_var = (true|false|\\d+)", + "int_list = \\[1, 2\\]"), DYNAMIC_LIST_EXISTS_ONE_UNKNOWN_POISONING( "int_list == [1, 2] ? !(int_list.exists_one(x, x == 1 || unknown_var) == true ||" + " int_list.exists_one(x, x == 1 || unknown_var) == false) : true", - "Condition is not always true.", + "Condition is not always true\\.", "Counterexample input:", - "unknown_var =", - "int_list = [1, 2]"), + "unknown_var = (true|false)", + "int_list = \\[1, 2\\]"), DYNAMIC_ITERATION_OVER_SCALAR_RETURNS_UNKNOWN( "unknown_var == 1 ? !(unknown_var.all(x, false) == true || unknown_var.all(x, false) ==" + " false) : true", - "Condition is not always true.", + "Condition is not always true\\.", "Counterexample input:", "unknown_var = 1"), ERROR_UNKNOWN_PRECEDENCE( "[1, 2].all(x, x == 1 ? unknown_var : 1/0 == 0) == true", - "Condition is not always true.", + "Condition is not always true\\.", "Counterexample input:", - "unknown_var ="), + "unknown_var = (true|false)"), LIST_LITERAL_ELEMENT_ERROR_PROPAGATES( "[true, 1/0].exists(x, x) == true", - "Condition is not always true.", - "(The expression fails unconditionally, regardless of input state)"), + "Condition is not always true\\.", + "\\(The expression fails unconditionally, regardless of input state\\)"), DYNAMIC_STRING_MACRO_TYPE_MISMATCH( "unknown_var == 1 ? !(unknown_var.contains('a') == true || unknown_var.contains('a') ==" + " false) : true", - "Condition is not always true.", + "Condition is not always true\\.", "Counterexample input:", - "unknown_var = 1"), + "unknown_var = \\d+u?"), STRING_CONTAINS_IS_NOT_EQUALITY( "role.contains('admin') ? role == 'admin' : true", - "Condition is not always true.", + "Condition is not always true\\.", "Counterexample input:", - "role ="), + "role = \\\".*\\\""), STRING_OVERLAP_FALLACY( "role.startsWith('A') && role.endsWith('B') ? role == 'AB' : true", - "Condition is not always true.", + "Condition is not always true\\.", "Counterexample input:", - "role ="), + "role = \\\".*\\\""), TYPE_CONVERSION_INT_TO_UINT_UNDERFLOW_ERROR( "uint(-1) == 1u", - "Condition is not always true.", - "(The expression fails unconditionally, regardless of input state)"), + "Condition is not always true\\.", + "\\(The expression fails unconditionally, regardless of input state\\)"), TYPE_CONVERSION_UINT_TO_INT_OVERFLOW_ERROR( "int(9223372036854775808u) == 1", - "Condition is not always true.", - "(The expression fails unconditionally, regardless of input state)"), + "Condition is not always true\\.", + "\\(The expression fails unconditionally, regardless of input state\\)"), STRING_STARTS_VS_ENDS_WITH( "role.startsWith('admin') == role.endsWith('admin')", - "Condition is not always true.", + "Condition is not always true\\.", "Counterexample input:", - "role ="), + "role = \\\".*\\\""), STRING_CONTAINS_VS_STARTS_WITH( "role.contains('admin') == role.startsWith('admin')", - "Condition is not always true.", + "Condition is not always true\\.", "Counterexample input:", - "role ="), + "role = \\\".*\\\""), DYNAMIC_MAP_INDEX_COMPUTATION_VIOLATION( "type(dyn_map[1 + 1]) == list && size(dyn_map[1 + 1]) == 0 " + "? dyn_map[1 + 1] == [] : true", - "Condition is not always true.", + "Condition is not always true\\.", "Counterexample input:", - "dyn_map ="), + "dyn_map = \\{\\}"), DYNAMIC_MAP_COMPREHENSION_NESTED_EQUALITY_VIOLATION( "cel.bind(r, request, r.l == [[1], [2], [3], [4], [5]] && r.m == {1: [1], 2: [2]," + " 3: [3]} ? r.l.all(x, r.m.exists(k, r.m[k] == x)) : true)", - "Condition is not always true.", + "Condition is not always true\\.", "Counterexample input:", - "request ="), + "request = .*"), UNINTERPRETED_EQUALITY_VIOLATION( "request == request", - "Condition is not always true.", + "Condition is not always true\\.", "Counterexample input:", "request = NaN"), CROSS_TYPE_NUMERIC_EQUALITY_APPROXIMATION_VIOLATION( - "dyn_var == 1.0", "Condition is not always true.", "Counterexample input:", "dyn_var ="), + "dyn_var == 1.0", + "Condition is not always true\\.", + "Counterexample input:", + "dyn_var = b\\\"![01]!\\\""), DYNAMIC_NOT_TYPE_MISMATCH( - "!dyn_var", "Condition is not always true.", "Counterexample input:", "dyn_var ="), + "!dyn_var", + "Condition is not always true\\.", + "Counterexample input:", + "dyn_var = (true|false)"), DYNAMIC_CONDITIONAL_TYPE_MISMATCH( "dyn_var ? true : false", - "Condition is not always true.", + "Condition is not always true\\.", "Counterexample input:", - "dyn_var ="), + "dyn_var = b\\\"![01]!\\\""), DYNAMIC_NOT_TYPE_MISMATCH_SURVIVOR( "type(dyn_var) == int ? (!dyn_var == !dyn_var) : true", - "Condition is not always true.", + "Condition is not always true\\.", "Counterexample input:", - "dyn_var ="), + "dyn_var = int\\{\\}"), DYNAMIC_CONDITIONAL_TYPE_MISMATCH_SURVIVOR( "type(dyn_var) == int ? (dyn_var ? true : false) == (dyn_var ? true : false) : true", - "Condition is not always true.", + "Condition is not always true\\.", "Counterexample input:", - "dyn_var ="); + "dyn_var = int\\{\\}"); final String expr; final ImmutableList expectedFragments; @@ -1166,7 +1218,7 @@ public void isAlwaysTrue_violation_returnsFalse( assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); for (String fragment : testCase.expectedFragments) { - assertThat(result.message()).contains(fragment); + assertThat(result.message()).containsMatch(fragment); } } From 22e84f7c65a2858a65e925c7a5c6337943b84f7b Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Thu, 23 Jul 2026 14:12:14 -0700 Subject: [PATCH 139/204] Add counterexample generation for optional values PiperOrigin-RevId: 952950649 --- .../CelZ3CounterexampleGenerator.java | 17 +++++++++++++ .../dev/cel/verifier/CelZ3TypeSystem.java | 4 +++ .../cel/verifier/CelVerifierZ3ImplTest.java | 25 +++++++++++-------- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java index 5cea7468d..f7d47635f 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java @@ -123,6 +123,23 @@ private static String formatExpr( return "Error"; } else if (decl.equals(typeSystem.unknownCons().ConstructorDecl())) { return "Unknown"; + } else if (decl.equals(typeSystem.optionalCons().ConstructorDecl())) { + Expr optRef = expr.getArgs()[0]; + Expr hasValueExpr = + evaluateStrict( + model, + typeSystem.optHasValue(optRef), + String.format("Z3 failed to evaluate optHasValue natively for %s", optRef)); + if (hasValueExpr.isTrue()) { + Expr valueExpr = + evaluateStrict( + model, + typeSystem.getOptionalValue(optRef), + String.format("Z3 failed to evaluate optValue natively for %s", optRef)); + return "optional(" + formatExpr(ctx, typeSystem, model, valueExpr) + ")"; + } else if (hasValueExpr.isFalse()) { + return "optional.none()"; + } } return expr.toString(); diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java index cf29d0207..2913f0f39 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java @@ -256,6 +256,10 @@ Constructor bytesCons() { return bytesCons; } + Constructor optionalCons() { + return optionalCons; + } + /** Creates a CelValue containing a boolean. */ public Expr mkBool(boolean val) { return ctx.mkApp(boolCons.ConstructorDecl(), ctx.mkBool(val)); diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index bd218674a..25a74e58c 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -1060,23 +1060,28 @@ private enum IsAlwaysTrueViolationTestCase { "opt_dyn_var.hasValue() ? type(opt_dyn_var.value()) == int : true", "Condition is not always true\\.", "Counterexample input:", - "opt_dyn_var = \\(Optional OptionalRef!val!\\d+\\)"), + "opt_dyn_var = optional\\(Unknown\\)"), OPTIONAL_ENTRY_DYN_VAR_TYPE_MISMATCH( "[?dyn_var] == [?dyn_var] ? true : true", "Condition is not always true\\.", "Counterexample input:", - "dyn_var = b\\\"![01]!\\\""), + "dyn_var = b\"![01]!\""), OPTIONAL_MAP_ENTRY_DYN_VAR_TYPE_MISMATCH( "{?1: dyn_var} == {?1: dyn_var} ? true : true", "Condition is not always true\\.", "Counterexample input:", - "dyn_var = b\\\"![01]!\\\""), + "dyn_var = b\"![01]!\""), OPTIONAL_STRUCT_ENTRY_DYN_VAR_TYPE_MISMATCH( "cel.expr.conformance.proto3.TestAllTypes{?single_int32: dyn_var} ==" + " cel.expr.conformance.proto3.TestAllTypes{?single_int32: dyn_var} ? true : true", "Condition is not always true\\.", "Counterexample input:", - "dyn_var = b\\\"(!0!|i)\\\""), + "dyn_var = b\"(!0!|i)\""), + OPTIONAL_NONE_COUNTEREXAMPLE( + "opt_dyn_var.hasValue()", + "Condition is not always true\\.", + "Counterexample input:", + "opt_dyn_var = optional\\.none\\(\\)"), DYNAMIC_MAP_ALL_VIOLATION( "string_int_map == {'a': 1, 'b': 2} ? string_int_map.all(k, k == 'a') : true", "Condition is not always true\\.", @@ -1133,12 +1138,12 @@ private enum IsAlwaysTrueViolationTestCase { "role.contains('admin') ? role == 'admin' : true", "Condition is not always true\\.", "Counterexample input:", - "role = \\\".*\\\""), + "role = \".*admin.*\""), STRING_OVERLAP_FALLACY( "role.startsWith('A') && role.endsWith('B') ? role == 'AB' : true", "Condition is not always true\\.", "Counterexample input:", - "role = \\\".*\\\""), + "role = \"A.*B\""), TYPE_CONVERSION_INT_TO_UINT_UNDERFLOW_ERROR( "uint(-1) == 1u", "Condition is not always true\\.", @@ -1151,12 +1156,12 @@ private enum IsAlwaysTrueViolationTestCase { "role.startsWith('admin') == role.endsWith('admin')", "Condition is not always true\\.", "Counterexample input:", - "role = \\\".*\\\""), + "role = \"(admin.*|.*admin)\""), STRING_CONTAINS_VS_STARTS_WITH( "role.contains('admin') == role.startsWith('admin')", "Condition is not always true\\.", "Counterexample input:", - "role = \\\".*\\\""), + "role = \".*admin.*\""), DYNAMIC_MAP_INDEX_COMPUTATION_VIOLATION( "type(dyn_map[1 + 1]) == list && size(dyn_map[1 + 1]) == 0 " + "? dyn_map[1 + 1] == [] : true", @@ -1178,7 +1183,7 @@ private enum IsAlwaysTrueViolationTestCase { "dyn_var == 1.0", "Condition is not always true\\.", "Counterexample input:", - "dyn_var = b\\\"![01]!\\\""), + "dyn_var = b\"![01]!\""), DYNAMIC_NOT_TYPE_MISMATCH( "!dyn_var", "Condition is not always true\\.", @@ -1188,7 +1193,7 @@ private enum IsAlwaysTrueViolationTestCase { "dyn_var ? true : false", "Condition is not always true\\.", "Counterexample input:", - "dyn_var = b\\\"![01]!\\\""), + "dyn_var = b\"![01]!\""), DYNAMIC_NOT_TYPE_MISMATCH_SURVIVOR( "type(dyn_var) == int ? (!dyn_var == !dyn_var) : true", "Condition is not always true\\.", From c97a361effcbe1c1717e8296b79abfab332fa3b4 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Thu, 23 Jul 2026 14:34:56 -0700 Subject: [PATCH 140/204] Add an axiom for optional.ofZeroValue for verifier PiperOrigin-RevId: 952962232 --- .../cel/verifier/axioms/OptionalAxioms.java | 38 +++++++++++++++++++ .../cel/verifier/CelVerifierZ3ImplTest.java | 25 ++++++++++++ 2 files changed, 63 insertions(+) diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/OptionalAxioms.java b/verifier/src/main/java/dev/cel/verifier/axioms/OptionalAxioms.java index 9a76bec36..46975756c 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/OptionalAxioms.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/OptionalAxioms.java @@ -15,13 +15,19 @@ package dev.cel.verifier.axioms; import com.google.common.collect.ImmutableList; +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Context; import com.microsoft.z3.Expr; +import com.microsoft.z3.FPExpr; +import com.microsoft.z3.SeqExpr; import dev.cel.common.CelFunctionDecl; import dev.cel.extensions.CelOptionalLibrary; import dev.cel.extensions.CelOptionalLibrary.Function; +import dev.cel.verifier.CelZ3TypeSystem; import java.util.Optional; /** Axiomatization for CEL's optional library functions. */ +@SuppressWarnings({"unchecked", "rawtypes"}) // Z3 Java API uses raw types. final class OptionalAxioms { static final ImmutableList ALL_AXIOMS = @@ -40,6 +46,17 @@ final class OptionalAxioms { sink.accept(ts.optHasValue(optRef)); return Optional.of(ts.mkOptionalOf(optRef)); }), + createUnaryAxiom( + Function.OPTIONAL_OF_NON_ZERO_VALUE, + "optional_ofNonZeroValue", + (ctx, ts, sink, value) -> { + Expr optRef = ctx.mkApp(ts.optionalOfRefFunc(), value); + BoolExpr isZero = isZeroValue(ctx, ts, value); + sink.accept( + ctx.mkImplies(ctx.mkNot(isZero), ctx.mkEq(ts.getOptionalValue(optRef), value))); + sink.accept(ctx.mkImplies(ctx.mkNot(isZero), ts.optHasValue(optRef))); + return Optional.of(ctx.mkITE(isZero, ts.mkOptionalNone(), ts.mkOptionalOf(optRef))); + }), createUnaryAxiom( Function.HAS_VALUE, "optional_hasValue", @@ -69,6 +86,27 @@ final class OptionalAxioms { return Optional.of(ctx.mkITE(ts.optHasValue(optRef), val, other)); })); + private static BoolExpr isZeroValue(Context ctx, CelZ3TypeSystem ts, Expr val) { + return ctx.mkOr( + ts.isNull(val), + ctx.mkAnd(ts.isBool(val), ctx.mkEq(ts.unwrapBool(val), ctx.mkFalse())), + ctx.mkAnd(ts.isInt(val), ctx.mkEq(ts.getInt(val), ctx.mkInt(0))), + ctx.mkAnd(ts.isUint(val), ctx.mkEq(ts.getUint(val), ctx.mkInt(0))), + ctx.mkAnd(ts.isDouble(val), ctx.mkFPIsZero((FPExpr) ts.getDouble(val))), + ctx.mkAnd(ts.isString(val), ctx.mkEq(ts.getString(val), ctx.mkString(""))), + ctx.mkAnd( + ts.isBytes(val), ctx.mkEq(ctx.mkLength((SeqExpr) ts.getBytes(val)), ctx.mkInt(0))), + ctx.mkAnd( + ts.isList(val), ctx.mkEq(ctx.mkLength(ts.getSeq(ts.getListRef(val))), ctx.mkInt(0))), + ctx.mkAnd( + ts.isMap(val), ctx.mkEq(ctx.mkLength(ts.getMapKeys(ts.getMapRef(val))), ctx.mkInt(0))), + ctx.mkAnd( + ts.isMessage(val), + ctx.mkEq( + ts.getMsgPresence(ts.getMessageRef(val)), + ctx.mkConstArray(ctx.getStringSort(), ctx.mkFalse())))); + } + private static CelFunctionDecl getDecl(Function funcEnum) { return CelOptionalLibrary.INSTANCE.functions().stream() .filter(d -> d.name().equals(funcEnum.getFunction())) diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index 25a74e58c..6489c15e2 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -1393,6 +1393,31 @@ private enum EquivalenceTestCase { OPTIONAL_VALUE_EQUIVALENCE("optional.of(x).value()", "x"), OPTIONAL_HAS_VALUE_EQUIVALENCE("optional.of(x).hasValue()", "true"), OPTIONAL_NONE_HAS_VALUE_EQUIVALENCE("optional.none().hasValue()", "false"), + OPTIONAL_OF_NON_ZERO_VALUE_ARITHMETIC_EQUIVALENCE( + "[optional.ofNonZeroValue(1 + 2 + 3)]", "[optional.of(6)]"), + OPTIONAL_OF_NON_ZERO_VALUE_INT_ZERO_EQUIVALENCE( + "optional.ofNonZeroValue(0)", "optional.none()"), + OPTIONAL_OF_NON_ZERO_VALUE_INT_NON_ZERO_EQUIVALENCE( + "optional.ofNonZeroValue(5)", "optional.of(5)"), + OPTIONAL_OF_NON_ZERO_VALUE_STRING_EMPTY_EQUIVALENCE( + "optional.ofNonZeroValue('')", "optional.none()"), + OPTIONAL_OF_NON_ZERO_VALUE_STRING_NON_EMPTY_EQUIVALENCE( + "optional.ofNonZeroValue('hi')", "optional.of('hi')"), + OPTIONAL_OF_NON_ZERO_VALUE_BOOL_FALSE_EQUIVALENCE( + "optional.ofNonZeroValue(false)", "optional.none()"), + OPTIONAL_OF_NON_ZERO_VALUE_BOOL_TRUE_EQUIVALENCE( + "optional.ofNonZeroValue(true)", "optional.of(true)"), + OPTIONAL_OF_NON_ZERO_VALUE_DOUBLE_ZERO_EQUIVALENCE( + "optional.ofNonZeroValue(0.0)", "optional.none()"), + OPTIONAL_OF_NON_ZERO_VALUE_UINT_ZERO_EQUIVALENCE( + "optional.ofNonZeroValue(0u)", "optional.none()"), + OPTIONAL_OF_NON_ZERO_VALUE_LIST_EMPTY_EQUIVALENCE( + "optional.ofNonZeroValue([])", "optional.none()"), + OPTIONAL_OF_NON_ZERO_VALUE_MAP_EMPTY_EQUIVALENCE( + "optional.ofNonZeroValue({})", "optional.none()"), + OPTIONAL_OF_NON_ZERO_VALUE_BYTES_EMPTY_EQUIVALENCE( + "optional.ofNonZeroValue(b'')", "optional.none()"), + OPTIONAL_OF_NON_ZERO_VALUE_NULL_EQUIVALENCE("optional.ofNonZeroValue(null)", "optional.none()"), FUNCTIONS("size(\"abc\") == size(role)", "size(role) == size(\"abc\")"), NOT_EQUALS("x != y", "!(x == y)"), LESS("x < y", "y > x"), From db6432f063b70f240cb5d7ac1496925d0ecaa281 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Thu, 23 Jul 2026 14:44:10 -0700 Subject: [PATCH 141/204] Fix overflow handlings for Uint, add more test cases around IntSort overflows PiperOrigin-RevId: 952966513 --- .../dev/cel/verifier/CelZ3TypeSystem.java | 14 +++--- .../cel/verifier/CelVerifierZ3ImplTest.java | 45 ++++++++++++++++++- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java index 2913f0f39..af1a4688b 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java @@ -16,6 +16,7 @@ import com.google.common.collect.Lists; import com.google.common.collect.ObjectArrays; +import com.google.common.primitives.UnsignedLongs; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.microsoft.z3.ArithExpr; import com.microsoft.z3.ArrayExpr; @@ -300,9 +301,14 @@ public Expr mkInt(long val) { return ctx.mkApp(intCons.ConstructorDecl(), ctx.mkInt(val)); } + /** Creates a CelValue containing an unsigned integer from a string representation. */ + public Expr mkUint(String val) { + return ctx.mkApp(uintCons.ConstructorDecl(), ctx.mkInt(val)); + } + /** Creates a CelValue containing an unsigned integer. */ public Expr mkUint(long val) { - return ctx.mkApp(uintCons.ConstructorDecl(), ctx.mkInt(val)); + return mkUint(UnsignedLongs.toString(val)); } /** Creates a CelValue containing a double. */ @@ -859,10 +865,8 @@ public static BoolExpr mkNotFlattened(Context ctx, BoolExpr arg) { this.boolCons = ctx.mkConstructor( CONS_BOOL, IS_BOOL, new String[] {GET_BOOL}, new Sort[] {ctx.getBoolSort()}, null); - // Note: Z3's IntSort models unbounded mathematical integers. We do not currently use - // BitVecSort(64), which means CEL integer overflow semantics are not natively modeled, - // and bitwise operations are unsupported. We enforce 64-bit value bounds explicitly - // during variable constraint generation instead. + // We use Z3's IntSort instead of BitVecSort(64) for faster arithmetic solving without + // bit-blasting, explicitly enforcing 64-bit range bounds and overflow errors. this.intCons = ctx.mkConstructor( CONS_INT, IS_INT, new String[] {GET_INT}, new Sort[] {ctx.getIntSort()}, null); diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index 6489c15e2..20b410a5c 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -84,6 +84,8 @@ public final class CelVerifierZ3ImplTest { .addMessageTypes(TestAllTypes.getDescriptor(), TestAllTypes.NestedMessage.getDescriptor()) .addVar("x", SimpleType.INT) .addVar("u", SimpleType.UINT) + .addVar("u1", SimpleType.UINT) + .addVar("u2", SimpleType.UINT) .addVar("d", SimpleType.DOUBLE) .addVar("by", SimpleType.BYTES) .addVar("y", SimpleType.INT) @@ -705,6 +707,10 @@ private enum IsAlwaysTrueTestCase { DYNAMIC_NUMERIC_EQUALITY_CROSS_TYPE_DYN_DOUBLE( "type(dyn_var) == double && dyn_var == 5.0 && dyn_var2 == 5.0 && type(dyn_var2) == double ?" + " dyn_var == dyn_var2 : true"), + INT64_BOUNDS_ALWAYS_TRUE("x <= 9223372036854775807 && x >= -9223372036854775808"), + UINT64_BOUNDS_ALWAYS_TRUE("u <= 18446744073709551615u && u >= 0u"), + MODULO_INT64_MIN_INT_BY_NEG_ONE_ALWAYS_ZERO( + "x == -9223372036854775808 && y == -1 ? x % y == 0 : true"), ; final String expr; @@ -1203,7 +1209,44 @@ private enum IsAlwaysTrueViolationTestCase { "type(dyn_var) == int ? (dyn_var ? true : false) == (dyn_var ? true : false) : true", "Condition is not always true\\.", "Counterexample input:", - "dyn_var = int\\{\\}"); + "dyn_var = int\\{\\}"), + ADD_INT64_OVERFLOW_FAILS_WITH_ERRORS( + "x > 0 && y > 0 ? x + y > x : true", + "Condition is not always true\\.", + "Counterexample input:", + "x = (1|9223372036854775807)", + "y = (1|9223372036854775807)"), + ADD_UINT64_OVERFLOW_FAILS_WITH_ERRORS( + "u1 > 0u && u2 > 0u ? u1 + u2 >= u1 : true", + "Condition is not always true\\.", + "Counterexample input:", + "u1 = (1u|18446744073709551615u)", + "u2 = (1u|18446744073709551615u)"), + SUBTRACT_INT64_UNDERFLOW_FAILS_WITH_ERRORS( + "x < 0 && y > 0 ? x - y < x : true", + "Condition is not always true\\.", + "Counterexample input:", + "x = (-2|9223372036854775807)", + "y = (-2|9223372036854775807)"), + MULTIPLY_INT64_OVERFLOW_FAILS_WITH_ERRORS( + "x > 1000000000 && y > 1000000000 ? x * y > 0 : true", + "Condition is not always true\\.", + "Counterexample input:", + "x = [0-9]+", + "y = [0-9]+"), + MULTIPLY_UINT64_OVERFLOW_FAILS_WITH_ERRORS( + "u1 > 1000000000u && u2 > 1000000000u ? u1 * u2 > 0u : true", + "Condition is not always true\\.", + "Counterexample input:", + "u1 = [0-9]+u", + "u2 = [0-9]+u"), + DIVIDE_INT64_OVERFLOW_MIN_INT_BY_NEG_ONE_FAILS_WITH_ERRORS( + "x == -9223372036854775808 && y == -1 ? x / y == -x : true", + "Condition is not always true\\.", + "Counterexample input:", + "x = -9223372036854775808", + "y = -1"), + ; final String expr; final ImmutableList expectedFragments; From d4c891326ebcccb632a813c4a3b3f08ca032889b Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Mon, 27 Jul 2026 09:21:59 -0700 Subject: [PATCH 142/204] Make invariant violation messages policy-specific PiperOrigin-RevId: 954667997 --- verifier/BUILD.bazel | 26 ++++-- verifier/README.md | 2 +- .../cel/verifier/CelPolicyVerifierImpl.java | 9 +- .../cel/verifier/CelVerificationResult.java | 38 ++++++-- .../dev/cel/verifier/CelVerifierZ3Impl.java | 92 ++++++++++--------- .../verifier/CelPolicyVerifierImplTest.java | 3 +- .../cel/verifier/CelVerifierZ3ImplTest.java | 2 +- 7 files changed, 105 insertions(+), 67 deletions(-) diff --git a/verifier/BUILD.bazel b/verifier/BUILD.bazel index a7d620d1d..cc2f01810 100644 --- a/verifier/BUILD.bazel +++ b/verifier/BUILD.bazel @@ -2,14 +2,25 @@ load("@rules_java//java:defs.bzl", "java_library") package( default_applicable_licenses = ["//:license"], - default_visibility = ["//:internal"], + default_visibility = [":verifier_allow_list"], +) + +VERIFIER_ALLOW_LIST = [ + "//...", +] + +package_group( + name = "verifier_allow_list", + packages = VERIFIER_ALLOW_LIST, +) + +package_group( + name = "verifier_internal", + packages = ["//verifier/..."], ) java_library( name = "verifier", - visibility = [ - "//:internal", - ], exports = ["//verifier/src/main/java/dev/cel/verifier"], ) @@ -27,22 +38,19 @@ java_library( java_library( name = "verifier_factory", compatible_with = [], - visibility = [ - "//:internal", - ], exports = ["//verifier/src/main/java/dev/cel/verifier:verifier_factory"], ) java_library( name = "type_system", compatible_with = [], - visibility = ["//:internal"], + visibility = [":verifier_internal"], exports = ["//verifier/src/main/java/dev/cel/verifier:type_system"], ) java_library( name = "z3_impl", compatible_with = [], - visibility = ["//:internal"], + visibility = [":verifier_internal"], exports = ["//verifier/src/main/java/dev/cel/verifier:z3_impl"], ) diff --git a/verifier/README.md b/verifier/README.md index c8d838e95..db797a283 100644 --- a/verifier/README.md +++ b/verifier/README.md @@ -353,7 +353,7 @@ public class InvariantsExample { System.out.println("Invariant violated!"); System.out.println(result.message()); // Output: - // Implication violation detected. Counterexample input: + // Invariant 'always_secure' violation detected. Counterexample input: // port = 80 break; case INCONCLUSIVE: diff --git a/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierImpl.java b/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierImpl.java index 885201190..96473c16f 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierImpl.java +++ b/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierImpl.java @@ -145,10 +145,15 @@ public ImmutableMap verifyInvariants(CelPolicy po throw new UnsupportedOperationException( "Invariants verification requires Z3 verifier implementation."); } + String invariantId = invariant.invariantId().value(); CelVerificationResult result = ((CelVerifierZ3Impl) astVerifier) - .verifyImplication(assumeAst, assertAst, boundSymbols); - resultsBuilder.put(invariant.invariantId().value(), result); + .verifyImplication( + assumeAst, + assertAst, + boundSymbols, + String.format("Invariant '%s'", invariantId)); + resultsBuilder.put(invariantId, result); } return resultsBuilder.buildOrThrow(); diff --git a/verifier/src/main/java/dev/cel/verifier/CelVerificationResult.java b/verifier/src/main/java/dev/cel/verifier/CelVerificationResult.java index f243537e1..5a4c7ada6 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelVerificationResult.java +++ b/verifier/src/main/java/dev/cel/verifier/CelVerificationResult.java @@ -33,26 +33,48 @@ public enum VerificationStatus { /** Returns the status of the verification check. */ public abstract VerificationStatus status(); + /** + * Returns the primary reason for the verification outcome. + */ + public abstract String reason(); + + /** + * Returns a detailed counterexample or satisfying model assignment, if one was found. + */ + public abstract String counterexample(); + /** * Returns a message detailing the outcome of the verification check, such as a counterexample * input, satisfying model assignments, or truncation reason. May be empty if status is VERIFIED * and no model inputs apply (e.g., when verifying isAlwaysTrue without counterexamples). */ - public abstract String message(); + public String message() { + return reason() + counterexample(); + } static CelVerificationResult verified() { - return new AutoValue_CelVerificationResult(VerificationStatus.VERIFIED, ""); + return new AutoValue_CelVerificationResult(VerificationStatus.VERIFIED, "", ""); + } + + static CelVerificationResult verified(String reason) { + return new AutoValue_CelVerificationResult(VerificationStatus.VERIFIED, reason, ""); + } + + static CelVerificationResult failed(String reason) { + return new AutoValue_CelVerificationResult(VerificationStatus.VIOLATED, reason, ""); } - static CelVerificationResult verified(String message) { - return new AutoValue_CelVerificationResult(VerificationStatus.VERIFIED, message); + static CelVerificationResult failed(String reason, String counterexample) { + return new AutoValue_CelVerificationResult( + VerificationStatus.VIOLATED, reason, counterexample); } - static CelVerificationResult failed(String message) { - return new AutoValue_CelVerificationResult(VerificationStatus.VIOLATED, message); + static CelVerificationResult inconclusive(String reason) { + return new AutoValue_CelVerificationResult(VerificationStatus.INCONCLUSIVE, reason, ""); } - static CelVerificationResult inconclusive(String message) { - return new AutoValue_CelVerificationResult(VerificationStatus.INCONCLUSIVE, message); + static CelVerificationResult inconclusive(String reason, String counterexample) { + return new AutoValue_CelVerificationResult( + VerificationStatus.INCONCLUSIVE, reason, counterexample); } } diff --git a/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java b/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java index ce2705b56..510d88ec0 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java +++ b/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java @@ -36,6 +36,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import org.jspecify.annotations.Nullable; @@ -193,23 +194,23 @@ public CelVerificationResult verifyEquivalence( switch (result.outcome) { case EXACT_MATCH: return CelVerificationResult.failed( - "Equivalence violation detected." - + getCounterexampleString( - ctx, - translator.getTypeSystem(), - result.model, - /* isApproximate= */ false, - /* isCounterexample= */ true)); + "Equivalence violation detected.", + getCounterexampleString( + ctx, + translator.getTypeSystem(), + result.model, + /* isApproximate= */ false, + /* isCounterexample= */ true)); case APPROXIMATE_MATCH: return CelVerificationResult.inconclusive( "Inconclusive: a divergence may exist, but it depends on approximations, missing" - + " theories, or loop bounds." - + getCounterexampleString( - ctx, - translator.getTypeSystem(), - result.model, - /* isApproximate= */ true, - /* isCounterexample= */ true)); + + " theories, or loop bounds.", + getCounterexampleString( + ctx, + translator.getTypeSystem(), + result.model, + /* isApproximate= */ true, + /* isCounterexample= */ true)); case TRUNCATED: return CelVerificationResult.inconclusive( "Inconclusive: expressions are equivalent within the current loop unroll limit, but" @@ -227,7 +228,8 @@ public CelVerificationResult verifyEquivalence( CelVerificationResult verifyImplication( CelAbstractSyntaxTree assumeAst, CelAbstractSyntaxTree assertAst, - Map boundSymbols) + Map boundSymbols, + String subjectName) throws CelVerificationException { Preconditions.checkArgument(assumeAst.isChecked(), "assumeAst must be type-checked."); Preconditions.checkArgument(assertAst.isChecked(), "assertAst must be type-checked."); @@ -282,27 +284,27 @@ CelVerificationResult verifyImplication( switch (result.outcome) { case EXACT_MATCH: return CelVerificationResult.failed( - "Implication violation detected." - + getCounterexampleString( - ctx, - translator.getTypeSystem(), - result.model, - /* isApproximate= */ false, - /* isCounterexample= */ true)); + String.format("%s violation detected.", subjectName), + getCounterexampleString( + ctx, + translator.getTypeSystem(), + result.model, + /* isApproximate= */ false, + /* isCounterexample= */ true)); case APPROXIMATE_MATCH: return CelVerificationResult.inconclusive( "Inconclusive: a counterexample may exist, but it depends on approximations, missing" - + " theories, or loop bounds." - + getCounterexampleString( - ctx, - translator.getTypeSystem(), - result.model, - /* isApproximate= */ true, - /* isCounterexample= */ true)); + + " theories, or loop bounds.", + getCounterexampleString( + ctx, + translator.getTypeSystem(), + result.model, + /* isApproximate= */ true, + /* isCounterexample= */ true)); case TRUNCATED: return CelVerificationResult.inconclusive( - "Inconclusive: implication holds within the current loop unroll limit, but" - + " may be violated for larger collections."); + String.format("Inconclusive: %s holds within the current loop unroll limit, but" + + " may be violated for larger collections.", subjectName.toLowerCase(Locale.US))); case NO_MATCH: return CelVerificationResult.verified(); case SOLVER_UNKNOWN: @@ -345,13 +347,13 @@ private CelVerificationResult checkSatisfiability( case EXACT_MATCH: return searchForCounterexample ? CelVerificationResult.failed( - "Condition is not always true." - + getCounterexampleString( - ctx, - translator.getTypeSystem(), - result.model, - /* isApproximate= */ false, - /* isCounterexample= */ true)) + "Condition is not always true.", + getCounterexampleString( + ctx, + translator.getTypeSystem(), + result.model, + /* isApproximate= */ false, + /* isCounterexample= */ true)) : CelVerificationResult.verified( "Condition is satisfiable." + getCounterexampleString( @@ -369,13 +371,13 @@ private CelVerificationResult checkSatisfiability( : "Inconclusive: a satisfying model may exist, but it depends on" + " approximations, missing theories, or loop bounds."; return CelVerificationResult.inconclusive( - prefix - + getCounterexampleString( - ctx, - translator.getTypeSystem(), - result.model, - /* isApproximate= */ true, - /* isCounterexample= */ searchForCounterexample)); + prefix, + getCounterexampleString( + ctx, + translator.getTypeSystem(), + result.model, + /* isApproximate= */ true, + /* isCounterexample= */ searchForCounterexample)); case TRUNCATED: return CelVerificationResult.inconclusive( diff --git a/verifier/src/test/java/dev/cel/verifier/CelPolicyVerifierImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelPolicyVerifierImplTest.java index 09f205f04..5a9eaea02 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelPolicyVerifierImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelPolicyVerifierImplTest.java @@ -526,7 +526,8 @@ public void verifyInvariants_workloadAdmissionFlawed_violationsDetected() throws .isEqualTo(VerificationStatus.VIOLATED); assertThat(results.get("universal_no_unapproved_privileged_prod").message()) .isEqualTo( - "Implication violation detected. Counterexample input:\n" + "Invariant 'universal_no_unapproved_privileged_prod' violation detected." + + " Counterexample input:\n" + " is_owner = false\n" + " is_privileged = true\n" + " is_prod = true\n" diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index 20b410a5c..7f9f61520 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -2604,7 +2604,7 @@ public void verifyImplication_loopExceedsLimit_returnsTruncatedInconclusive() th CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(2).build(); CelVerificationResult result = ((CelVerifierZ3Impl) verifier) - .verifyImplication(assumeAst, assertAst, ImmutableMap.of()); + .verifyImplication(assumeAst, assertAst, ImmutableMap.of(), "Implication"); assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); assertThat(result.message()) From f502672911cc2c980e0d93a3b4e16b463e51382e Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Mon, 27 Jul 2026 15:55:21 -0700 Subject: [PATCH 143/204] Implement optional field traversal semantics in verifier PiperOrigin-RevId: 954876702 --- .../cel/checker/CelStandardDeclarations.java | 11 +- .../java/dev/cel/common/CelFunctionDecl.java | 6 + .../main/java/dev/cel/extensions/BUILD.bazel | 4 +- .../cel/extensions/CelOptionalLibrary.java | 207 +++++++++++------- .../cel/verifier/CelZ3OperatorTranslator.java | 37 +++- .../cel/verifier/axioms/OptionalAxioms.java | 135 +++++++++--- .../cel/verifier/CelVerifierZ3ImplTest.java | 51 ++++- 7 files changed, 324 insertions(+), 127 deletions(-) diff --git a/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java b/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java index 0efcd4c65..3d5175cb5 100644 --- a/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java +++ b/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java @@ -55,7 +55,7 @@ public final class CelStandardDeclarations { private final ImmutableSet celIdentDecls; /** Enumeration of Standard Functions. */ - public enum StandardFunction { + public enum StandardFunction implements CelFunctionDecl.Declarer { // Deprecated - use {@link #IN} OLD_IN( true, @@ -1504,6 +1504,7 @@ private CelFunctionDecl withOverloads(Iterable overloads) { return newCelFunctionDecl(functionName, ImmutableSet.copyOf(overloads)); } + @Override public CelFunctionDecl functionDecl() { return celFunctionDecl; } @@ -1579,8 +1580,14 @@ public CelIdentDecl identDecl() { /** General interface for defining a standard function overload. */ @Immutable - public interface StandardOverload { + public interface StandardOverload extends CelFunctionDecl.Declarer { CelOverloadDecl celOverloadDecl(); + + @Override + default CelFunctionDecl functionDecl() { + // TODO: Remove default keyword by implementing this for all standard overloads + throw new UnsupportedOperationException("Unimplemented"); + } } /** Set of all standard function names. */ diff --git a/common/src/main/java/dev/cel/common/CelFunctionDecl.java b/common/src/main/java/dev/cel/common/CelFunctionDecl.java index 12beb53d7..ea10366ff 100644 --- a/common/src/main/java/dev/cel/common/CelFunctionDecl.java +++ b/common/src/main/java/dev/cel/common/CelFunctionDecl.java @@ -38,6 +38,12 @@ public abstract class CelFunctionDecl { /** Required. List of function overloads. Must contain at least one overload. */ public abstract ImmutableSet overloads(); + /** General interface for defining an extension function overload or standard declaration. */ + @Immutable + public interface Declarer { + CelFunctionDecl functionDecl(); + } + /** Builder for configuring the {@link CelFunctionDecl}. */ @AutoValue.Builder public abstract static class Builder { diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel index b25fdf16d..ba57a07c3 100644 --- a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel @@ -13,7 +13,9 @@ package( java_library( name = "extension_library", - srcs = ["CelExtensionLibrary.java"], + srcs = [ + "CelExtensionLibrary.java", + ], tags = [ ], deps = [ diff --git a/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java b/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java index 87a31341f..8b67d5c79 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java +++ b/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java @@ -97,95 +97,148 @@ public String getFunction() { } } + private static final class Types { + private static final TypeParamType PARAM_TYPE_K = TypeParamType.create("K"); + private static final TypeParamType PARAM_TYPE_V = TypeParamType.create("V"); + private static final OptionalType OPTIONAL_TYPE_V = OptionalType.create(PARAM_TYPE_V); + private static final ListType LIST_TYPE_V = ListType.create(PARAM_TYPE_V); + private static final MapType MAP_TYPE_KV = MapType.create(PARAM_TYPE_K, PARAM_TYPE_V); + } + + /** Declarations for the optional extension library. */ + public enum OptionalDeclaration implements CelFunctionDecl.Declarer { + OPTIONAL_OF( + CelFunctionDecl.newFunctionDeclaration( + Function.OPTIONAL_OF.getFunction(), + CelOverloadDecl.newGlobalOverload( + "optional_of", Types.OPTIONAL_TYPE_V, Types.PARAM_TYPE_V))), + OPTIONAL_OF_NON_ZERO_VALUE( + CelFunctionDecl.newFunctionDeclaration( + Function.OPTIONAL_OF_NON_ZERO_VALUE.getFunction(), + CelOverloadDecl.newGlobalOverload( + "optional_ofNonZeroValue", Types.OPTIONAL_TYPE_V, Types.PARAM_TYPE_V))), + OPTIONAL_NONE( + CelFunctionDecl.newFunctionDeclaration( + Function.OPTIONAL_NONE.getFunction(), + CelOverloadDecl.newGlobalOverload("optional_none", Types.OPTIONAL_TYPE_V))), + OPTIONAL_VALUE( + CelFunctionDecl.newFunctionDeclaration( + Function.VALUE.getFunction(), + CelOverloadDecl.newMemberOverload( + "optional_value", Types.PARAM_TYPE_V, Types.OPTIONAL_TYPE_V))), + OPTIONAL_HAS_VALUE( + CelFunctionDecl.newFunctionDeclaration( + Function.HAS_VALUE.getFunction(), + CelOverloadDecl.newMemberOverload( + "optional_hasValue", SimpleType.BOOL, Types.OPTIONAL_TYPE_V))), + OPTIONAL_UNWRAP( + CelFunctionDecl.newFunctionDeclaration( + Function.OPTIONAL_UNWRAP.getFunction(), + CelOverloadDecl.newGlobalOverload( + "optional_unwrap_list", + Types.LIST_TYPE_V, + ListType.create(Types.OPTIONAL_TYPE_V)))), + OPTIONAL_OR( + CelFunctionDecl.newFunctionDeclaration( + "or", + CelOverloadDecl.newMemberOverload( + "optional_or_optional", + Types.OPTIONAL_TYPE_V, + Types.OPTIONAL_TYPE_V, + Types.OPTIONAL_TYPE_V))), + OPTIONAL_OR_VALUE( + CelFunctionDecl.newFunctionDeclaration( + "orValue", + CelOverloadDecl.newMemberOverload( + "optional_orValue_value", + Types.PARAM_TYPE_V, + Types.OPTIONAL_TYPE_V, + Types.PARAM_TYPE_V))), + OPTIONAL_SELECT( + CelFunctionDecl.newFunctionDeclaration( + Operator.OPTIONAL_SELECT.getFunction(), + CelOverloadDecl.newGlobalOverload( + "select_optional_field", + Types.OPTIONAL_TYPE_V, + SimpleType.DYN, + SimpleType.STRING))), + OPTIONAL_INDEX( + CelFunctionDecl.newFunctionDeclaration( + Operator.OPTIONAL_INDEX.getFunction(), + CelOverloadDecl.newGlobalOverload( + "list_optindex_optional_int", + Types.OPTIONAL_TYPE_V, + Types.LIST_TYPE_V, + SimpleType.INT), + CelOverloadDecl.newGlobalOverload( + "optional_list_optindex_optional_int", + Types.OPTIONAL_TYPE_V, + OptionalType.create(Types.LIST_TYPE_V), + SimpleType.INT), + CelOverloadDecl.newGlobalOverload( + "map_optindex_optional_value", + Types.OPTIONAL_TYPE_V, + Types.MAP_TYPE_KV, + Types.PARAM_TYPE_K), + CelOverloadDecl.newGlobalOverload( + "optional_map_optindex_optional_value", + Types.OPTIONAL_TYPE_V, + OptionalType.create(Types.MAP_TYPE_KV), + Types.PARAM_TYPE_K))), + OPTIONAL_INDEX_OPERAND( + CelFunctionDecl.newFunctionDeclaration( + Operator.INDEX.getFunction(), + CelOverloadDecl.newGlobalOverload( + "optional_list_index_int", + Types.OPTIONAL_TYPE_V, + OptionalType.create(Types.LIST_TYPE_V), + SimpleType.INT), + CelOverloadDecl.newGlobalOverload( + "optional_map_index_value", + Types.OPTIONAL_TYPE_V, + OptionalType.create(Types.MAP_TYPE_KV), + Types.PARAM_TYPE_K))); + + private final CelFunctionDecl celFunctionDecl; + + OptionalDeclaration(CelFunctionDecl celFunctionDecl) { + this.celFunctionDecl = celFunctionDecl; + } + + @Override + public CelFunctionDecl functionDecl() { + return celFunctionDecl; + } + } + private static final CelExtensionLibrary LIBRARY = new CelExtensionLibrary() { - final TypeParamType paramTypeK = TypeParamType.create("K"); - final TypeParamType paramTypeV = TypeParamType.create("V"); - final OptionalType optionalTypeV = OptionalType.create(paramTypeV); - final ListType listTypeV = ListType.create(paramTypeV); - final MapType mapTypeKv = MapType.create(paramTypeK, paramTypeV); - private final CelOptionalLibrary version0 = new CelOptionalLibrary( 0, ImmutableSet.of( - CelFunctionDecl.newFunctionDeclaration( - OPTIONAL_OF.getFunction(), - CelOverloadDecl.newGlobalOverload( - "optional_of", optionalTypeV, paramTypeV)), - CelFunctionDecl.newFunctionDeclaration( - OPTIONAL_OF_NON_ZERO_VALUE.getFunction(), - CelOverloadDecl.newGlobalOverload( - "optional_ofNonZeroValue", optionalTypeV, paramTypeV)), - CelFunctionDecl.newFunctionDeclaration( - OPTIONAL_NONE.getFunction(), - CelOverloadDecl.newGlobalOverload("optional_none", optionalTypeV)), - CelFunctionDecl.newFunctionDeclaration( - VALUE.getFunction(), - CelOverloadDecl.newMemberOverload( - "optional_value", paramTypeV, optionalTypeV)), - CelFunctionDecl.newFunctionDeclaration( - HAS_VALUE.getFunction(), - CelOverloadDecl.newMemberOverload( - "optional_hasValue", SimpleType.BOOL, optionalTypeV)), - CelFunctionDecl.newFunctionDeclaration( - OPTIONAL_UNWRAP.getFunction(), - CelOverloadDecl.newGlobalOverload( - "optional_unwrap_list", listTypeV, ListType.create(optionalTypeV))), + OptionalDeclaration.OPTIONAL_OF.functionDecl(), + OptionalDeclaration.OPTIONAL_OF_NON_ZERO_VALUE.functionDecl(), + OptionalDeclaration.OPTIONAL_NONE.functionDecl(), + OptionalDeclaration.OPTIONAL_VALUE.functionDecl(), + OptionalDeclaration.OPTIONAL_HAS_VALUE.functionDecl(), + OptionalDeclaration.OPTIONAL_UNWRAP.functionDecl(), // Note: Implementation of "or" and "orValue" are special-cased inside the // interpreter. Hence, their bindings are not provided here. - CelFunctionDecl.newFunctionDeclaration( - "or", - CelOverloadDecl.newMemberOverload( - "optional_or_optional", optionalTypeV, optionalTypeV, optionalTypeV)), - CelFunctionDecl.newFunctionDeclaration( - "orValue", - CelOverloadDecl.newMemberOverload( - "optional_orValue_value", paramTypeV, optionalTypeV, paramTypeV)), + OptionalDeclaration.OPTIONAL_OR.functionDecl(), + OptionalDeclaration.OPTIONAL_OR_VALUE.functionDecl(), // Note: Function bindings for optional field selection and indexer is defined // in {@code StandardFunctions}. - CelFunctionDecl.newFunctionDeclaration( - Operator.OPTIONAL_SELECT.getFunction(), - CelOverloadDecl.newGlobalOverload( - "select_optional_field", - optionalTypeV, - SimpleType.DYN, - SimpleType.STRING)), - CelFunctionDecl.newFunctionDeclaration( - Operator.OPTIONAL_INDEX.getFunction(), - CelOverloadDecl.newGlobalOverload( - "list_optindex_optional_int", optionalTypeV, listTypeV, SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "optional_list_optindex_optional_int", - optionalTypeV, - OptionalType.create(listTypeV), - SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "map_optindex_optional_value", optionalTypeV, mapTypeKv, paramTypeK), - CelOverloadDecl.newGlobalOverload( - "optional_map_optindex_optional_value", - optionalTypeV, - OptionalType.create(mapTypeKv), - paramTypeK)), + OptionalDeclaration.OPTIONAL_SELECT.functionDecl(), + OptionalDeclaration.OPTIONAL_INDEX.functionDecl(), // Index overloads to accommodate using an optional value as the operand - CelFunctionDecl.newFunctionDeclaration( - Operator.INDEX.getFunction(), - CelOverloadDecl.newGlobalOverload( - "optional_list_index_int", - optionalTypeV, - OptionalType.create(listTypeV), - SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "optional_map_index_value", - optionalTypeV, - OptionalType.create(mapTypeKv), - paramTypeK))), + OptionalDeclaration.OPTIONAL_INDEX_OPERAND.functionDecl()), ImmutableSet.of( CelMacro.newReceiverMacro("optMap", 2, CelOptionalLibrary::expandOptMap)), ImmutableSet.of( // Type declaration for optional_type -> type(optional_type(V)) CelVarDecl.newVarDeclaration( - OptionalType.NAME, TypeType.create(optionalTypeV)))); + OptionalType.NAME, TypeType.create(Types.OPTIONAL_TYPE_V)))); private final CelOptionalLibrary version1 = new CelOptionalLibrary( @@ -211,16 +264,16 @@ public String getFunction() { "optional_list_first", "Return the first value in a list if present, otherwise" + " optional.none()", - optionalTypeV, - listTypeV)), + Types.OPTIONAL_TYPE_V, + Types.LIST_TYPE_V)), CelFunctionDecl.newFunctionDeclaration( LAST.functionName, CelOverloadDecl.newMemberOverload( "optional_list_last", "Return the last value in a list if present, otherwise" + " optional.none()", - optionalTypeV, - listTypeV))) + Types.OPTIONAL_TYPE_V, + Types.LIST_TYPE_V))) .build(), version1.macros, version1.variables); diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java index dca62bc11..955c3d530 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java @@ -243,7 +243,9 @@ private TranslatedValue translateOperatorCall( // by our axioms return TranslatedValue.propagateStrict(ctx, typeSystem, typeSystem.mkError(), args); case INDEX: - return translateIndex(args, ast); + return translateIndex(args, ast, false); + case OPTIONAL_INDEX: + return translateIndex(args, ast, true); case CONDITIONAL: return translateConditional(args, ast); case NOT_STRICTLY_FALSE: @@ -600,7 +602,8 @@ private TranslatedValue translateEquality( .withApproximation(ctx.mkFalse()); } - private Expr buildListIndex(Expr lhsTrans, Expr rhsTrans, BoolExpr typeGuard) { + private Expr buildListIndex( + Expr lhsTrans, Expr rhsTrans, BoolExpr typeGuard, boolean isOptional) { Expr listRef = typeSystem.getListRef(lhsTrans); SeqExpr seq = typeSystem.getSeq(listRef); Expr index = typeSystem.getInt(rhsTrans); @@ -617,6 +620,14 @@ private Expr buildListIndex(Expr lhsTrans, Expr rhsTrans, BoolExpr type constraintSink.accept(ctx.mkImplies(ctx.mkAnd(typeGuard, inBounds), valNotUnknown)); } + if (isOptional) { + Expr resultOptRef = ctx.mkApp(typeSystem.optionalOfRefFunc(), val); + constraintSink.accept(ctx.mkEq(typeSystem.getOptionalValue(resultOptRef), val)); + constraintSink.accept(typeSystem.optHasValue(resultOptRef)); + return ctx.mkITE( + inBounds, typeSystem.mkOptionalOf(resultOptRef), typeSystem.mkOptionalNone()); + } + return ctx.mkITE(inBounds, val, typeSystem.mkError()); } @@ -677,7 +688,8 @@ private ProbeResult createProbeResult( return new ProbeResult(altInMap, altVal); } - private Expr buildMapIndex(Expr lhsTrans, Expr rhsTrans, BoolExpr typeGuard) { + private Expr buildMapIndex( + Expr lhsTrans, Expr rhsTrans, BoolExpr typeGuard, boolean isOptional) { Expr mapRef = typeSystem.getMapRef(lhsTrans); ArrayExpr mapValues = (ArrayExpr) typeSystem.getMapValues(mapRef); ArrayExpr mapPresence = (ArrayExpr) typeSystem.getMapPresence(mapRef); @@ -780,10 +792,19 @@ private Expr buildMapIndex(Expr lhsTrans, Expr rhsTrans, BoolExpr typeG constraintSink.accept(ctx.mkImplies(ctx.mkAnd(typeGuard, finalInMap), valNotUnknown)); } + if (isOptional) { + Expr resultOptRef = ctx.mkApp(typeSystem.optionalOfRefFunc(), finalVal); + constraintSink.accept(ctx.mkEq(typeSystem.getOptionalValue(resultOptRef), finalVal)); + constraintSink.accept(typeSystem.optHasValue(resultOptRef)); + return ctx.mkITE( + finalInMap, typeSystem.mkOptionalOf(resultOptRef), typeSystem.mkOptionalNone()); + } + return ctx.mkITE(finalInMap, finalVal, typeSystem.mkError()); } - private TranslatedValue translateIndex(List args, CelAbstractSyntaxTree ast) { + private TranslatedValue translateIndex( + List args, CelAbstractSyntaxTree ast, boolean isOptional) { Expr lhsTrans = args.get(0).z3Expr(); Expr rhsTrans = args.get(1).z3Expr(); @@ -794,13 +815,13 @@ private TranslatedValue translateIndex(List args, CelAbstractSy Expr actualValue; if (lhsType.kind() == CelKind.LIST && rhsType.kind() == CelKind.INT) { - actualValue = buildListIndex(lhsTrans, rhsTrans, ctx.mkTrue()); + actualValue = buildListIndex(lhsTrans, rhsTrans, ctx.mkTrue(), isOptional); constraintSink.accept( ctx.mkImplies( ctx.mkNot(typeSystem.isError(actualValue)), typeConstraintGenerator.apply(actualValue, ((ListType) lhsType).elemType()))); } else if (lhsType.kind() == CelKind.MAP) { - actualValue = buildMapIndex(lhsTrans, rhsTrans, ctx.mkTrue()); + actualValue = buildMapIndex(lhsTrans, rhsTrans, ctx.mkTrue(), isOptional); constraintSink.accept( ctx.mkImplies( ctx.mkNot(typeSystem.isError(actualValue)), @@ -810,8 +831,8 @@ private TranslatedValue translateIndex(List args, CelAbstractSy BoolExpr isMapGuard = typeSystem.isMap(lhsTrans); actualValue = CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) - .addCase(isListGuard, buildListIndex(lhsTrans, rhsTrans, isListGuard)) - .addCase(isMapGuard, buildMapIndex(lhsTrans, rhsTrans, isMapGuard)) + .addCase(isListGuard, buildListIndex(lhsTrans, rhsTrans, isListGuard, isOptional)) + .addCase(isMapGuard, buildMapIndex(lhsTrans, rhsTrans, isMapGuard, isOptional)) .build(typeSystem.mkError()); } diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/OptionalAxioms.java b/verifier/src/main/java/dev/cel/verifier/axioms/OptionalAxioms.java index 46975756c..49f2d7450 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/OptionalAxioms.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/OptionalAxioms.java @@ -14,15 +14,25 @@ package dev.cel.verifier.axioms; +import static dev.cel.extensions.CelOptionalLibrary.OptionalDeclaration.OPTIONAL_HAS_VALUE; +import static dev.cel.extensions.CelOptionalLibrary.OptionalDeclaration.OPTIONAL_NONE; +import static dev.cel.extensions.CelOptionalLibrary.OptionalDeclaration.OPTIONAL_OF; +import static dev.cel.extensions.CelOptionalLibrary.OptionalDeclaration.OPTIONAL_OF_NON_ZERO_VALUE; +import static dev.cel.extensions.CelOptionalLibrary.OptionalDeclaration.OPTIONAL_OR; +import static dev.cel.extensions.CelOptionalLibrary.OptionalDeclaration.OPTIONAL_OR_VALUE; +import static dev.cel.extensions.CelOptionalLibrary.OptionalDeclaration.OPTIONAL_SELECT; +import static dev.cel.extensions.CelOptionalLibrary.OptionalDeclaration.OPTIONAL_VALUE; + +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.microsoft.z3.ArrayExpr; import com.microsoft.z3.BoolExpr; import com.microsoft.z3.Context; import com.microsoft.z3.Expr; import com.microsoft.z3.FPExpr; import com.microsoft.z3.SeqExpr; import dev.cel.common.CelFunctionDecl; -import dev.cel.extensions.CelOptionalLibrary; -import dev.cel.extensions.CelOptionalLibrary.Function; +import dev.cel.common.CelOverloadDecl; import dev.cel.verifier.CelZ3TypeSystem; import java.util.Optional; @@ -33,13 +43,11 @@ final class OptionalAxioms { static final ImmutableList ALL_AXIOMS = ImmutableList.of( createAxiom( - Function.OPTIONAL_NONE, - "optional_none", + OPTIONAL_NONE, (ctx, ts, sink, args, argApproximations) -> Optional.of(CelZ3OverloadResult.create(ts.mkOptionalNone(), ctx.mkFalse()))), createUnaryAxiom( - Function.OPTIONAL_OF, - "optional_of", + OPTIONAL_OF, (ctx, ts, sink, value) -> { Expr optRef = ctx.mkApp(ts.optionalOfRefFunc(), value); sink.accept(ctx.mkEq(ts.getOptionalValue(optRef), value)); @@ -47,8 +55,7 @@ final class OptionalAxioms { return Optional.of(ts.mkOptionalOf(optRef)); }), createUnaryAxiom( - Function.OPTIONAL_OF_NON_ZERO_VALUE, - "optional_ofNonZeroValue", + OPTIONAL_OF_NON_ZERO_VALUE, (ctx, ts, sink, value) -> { Expr optRef = ctx.mkApp(ts.optionalOfRefFunc(), value); BoolExpr isZero = isZeroValue(ctx, ts, value); @@ -58,32 +65,85 @@ final class OptionalAxioms { return Optional.of(ctx.mkITE(isZero, ts.mkOptionalNone(), ts.mkOptionalOf(optRef))); }), createUnaryAxiom( - Function.HAS_VALUE, - "optional_hasValue", + OPTIONAL_HAS_VALUE, (ctx, ts, sink, val) -> Optional.of(ts.wrapBool(ts.optHasValue(ts.getOptionalRef(val))))), createUnaryAxiom( - Function.VALUE, - "optional_value", + OPTIONAL_VALUE, (ctx, ts, sink, val) -> { Expr optRef = ts.getOptionalRef(val); return Optional.of( ctx.mkITE(ts.optHasValue(optRef), ts.getOptionalValue(optRef), ts.mkError())); }), createBinaryAxiom( - Function.OR_VALUE, - "optional_orValue_value", + OPTIONAL_OR_VALUE, (ctx, ts, sink, val, other) -> { Expr optRef = ts.getOptionalRef(val); return Optional.of( ctx.mkITE(ts.optHasValue(optRef), ts.getOptionalValue(optRef), other)); }), createBinaryAxiom( - Function.OR, - "optional_or_optional", + OPTIONAL_OR, (ctx, ts, sink, val, other) -> { Expr optRef = ts.getOptionalRef(val); return Optional.of(ctx.mkITE(ts.optHasValue(optRef), val, other)); + }), + createBinaryAxiom( + OPTIONAL_SELECT, + (ctx, ts, sink, operand, field) -> { + Expr optRef = ts.getOptionalRef(operand); + BoolExpr isOpt = ts.isOptional(operand); + BoolExpr hasValue = ts.optHasValue(optRef); + Expr actualOperand = ctx.mkITE(isOpt, ts.getOptionalValue(optRef), operand); + + BoolExpr isMap = ts.isMap(actualOperand); + BoolExpr isMsg = ts.isMessage(actualOperand); + BoolExpr isValidTarget = ctx.mkOr(isMap, isMsg); + + Expr msgFieldZ3Str = ts.getString(field); + Expr mapFieldCelVal = field; + + Expr msgRef = ts.getMessageRef(actualOperand); + Expr mapRef = ts.getMapRef(actualOperand); + + Expr presence = + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase( + isMsg, + ctx.mkSelect((ArrayExpr) ts.getMsgPresence(msgRef), msgFieldZ3Str)) + .addCase( + isMap, + ctx.mkSelect((ArrayExpr) ts.getMapPresence(mapRef), mapFieldCelVal)) + .build(ctx.mkFalse()); + + Expr value = + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase( + isMsg, ctx.mkSelect((ArrayExpr) ts.getMsgValues(msgRef), msgFieldZ3Str)) + .addCase( + isMap, + ctx.mkSelect((ArrayExpr) ts.getMapValues(mapRef), mapFieldCelVal)) + .build(ts.mkError()); + + BoolExpr valNotError = ctx.mkNot(ctx.mkEq(value, ts.mkError())); + BoolExpr shouldEvaluate = (BoolExpr) ctx.mkITE(isOpt, hasValue, ctx.mkTrue()); + sink.accept( + ctx.mkImplies( + CelZ3TypeSystem.mkAndFlattened( + ctx, shouldEvaluate, isValidTarget, (BoolExpr) presence), + valNotError)); + + Expr resultOptRef = ctx.mkApp(ts.optionalOfRefFunc(), value); + sink.accept(ctx.mkEq(ts.getOptionalValue(resultOptRef), value)); + sink.accept(ts.optHasValue(resultOptRef)); + + Expr optionalResult = + ctx.mkITE( + (BoolExpr) presence, ts.mkOptionalOf(resultOptRef), ts.mkOptionalNone()); + + Expr result = ctx.mkITE(isValidTarget, optionalResult, ts.mkError()); + return Optional.of( + ctx.mkITE(ctx.mkAnd(isOpt, ctx.mkNot(hasValue)), ts.mkOptionalNone(), result)); })); private static BoolExpr isZeroValue(Context ctx, CelZ3TypeSystem ts, Expr val) { @@ -107,32 +167,37 @@ private static BoolExpr isZeroValue(Context ctx, CelZ3TypeSystem ts, Expr val ctx.mkConstArray(ctx.getStringSort(), ctx.mkFalse())))); } - private static CelFunctionDecl getDecl(Function funcEnum) { - return CelOptionalLibrary.INSTANCE.functions().stream() - .filter(d -> d.name().equals(funcEnum.getFunction())) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Unknown function: " + funcEnum)); - } - private static CelZ3FunctionAxiom createAxiom( - Function funcEnum, String overloadId, CelZ3OverloadTranslator translator) { - return CelZ3FunctionAxiom.newBuilder(getDecl(funcEnum)) - .addOverloadTranslator(overloadId, translator) - .build(); + CelFunctionDecl.Declarer declarer, CelZ3OverloadTranslator translator) { + CelFunctionDecl functionDecl = declarer.functionDecl(); + CelZ3FunctionAxiom.Builder builder = CelZ3FunctionAxiom.newBuilder(functionDecl); + builder.addOverloadTranslator(getSingleOverloadOrThrow(functionDecl), translator); + return builder.build(); } private static CelZ3FunctionAxiom createUnaryAxiom( - Function funcEnum, String overloadId, CelZ3FunctionAxiom.UnaryTranslator translator) { - return CelZ3FunctionAxiom.newBuilder(getDecl(funcEnum)) - .addUnaryOverloadTranslator(overloadId, translator) - .build(); + CelFunctionDecl.Declarer declarer, CelZ3FunctionAxiom.UnaryTranslator translator) { + CelFunctionDecl functionDecl = declarer.functionDecl(); + CelZ3FunctionAxiom.Builder builder = CelZ3FunctionAxiom.newBuilder(functionDecl); + builder.addUnaryOverloadTranslator(getSingleOverloadOrThrow(functionDecl), translator); + return builder.build(); } private static CelZ3FunctionAxiom createBinaryAxiom( - Function funcEnum, String overloadId, CelZ3FunctionAxiom.BinaryTranslator translator) { - return CelZ3FunctionAxiom.newBuilder(getDecl(funcEnum)) - .addBinaryOverloadTranslator(overloadId, translator) - .build(); + CelFunctionDecl.Declarer declarer, CelZ3FunctionAxiom.BinaryTranslator translator) { + CelFunctionDecl functionDecl = declarer.functionDecl(); + CelZ3FunctionAxiom.Builder builder = CelZ3FunctionAxiom.newBuilder(functionDecl); + builder.addBinaryOverloadTranslator(getSingleOverloadOrThrow(functionDecl), translator); + return builder.build(); + } + + private static CelOverloadDecl getSingleOverloadOrThrow(CelFunctionDecl functionDecl) { + Preconditions.checkArgument( + functionDecl.overloads().size() == 1, + "Expected 1 overload for function %s, but found %s.", + functionDecl.name(), + functionDecl.overloads().size()); + return functionDecl.overloads().iterator().next(); } private OptionalAxioms() {} diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index 7f9f61520..8a975f37a 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -1557,7 +1557,41 @@ private enum EquivalenceTestCase { OPTIONAL_PRUNE_LIST_EQUALITY("[?optional.none(), 1] == [1]", "true"), OPTIONAL_PRUNE_LIST_COMPREHENSION("[1, ?optional.none()].all(x, x > 0)", "true"), MAP_COMPREHENSION( - "{'a': 1, 'b': 2}.exists(k, k == 'a')", "{'a': 1, 'b': 2}.exists(k, k == 'a')"); + "{'a': 1, 'b': 2}.exists(k, k == 'a')", "{'a': 1, 'b': 2}.exists(k, k == 'a')"), + OPTIONAL_FIELD_SELECTION_HAS_EQUIVALENCE( + "dyn_map.?field.orValue('default')", "has(dyn_map.field) ? dyn_map.field : 'default'"), + OPTIONAL_FIELD_SELECTION_MACRO_EQUIVALENCE( + "dyn_map.?field.hasValue() ? dyn_map.?field.value() : 'default'", + "has(dyn_map.field) ? dyn_map.field : 'default'"), + OPTIONAL_FIELD_SELECTION_CHAINED("{\"a\": {\"b\": 42}}.?a.?b", "optional.of(42)"), + OPTIONAL_INDEX_LIST_PRESENT("[1, 2, 3][?0]", "optional.of(1)"), + OPTIONAL_INDEX_LIST_MISSING("[1, 2, 3][?5]", "optional.none()"), + OPTIONAL_INDEX_MAP_MISSING("{'a': 1}[?'missing_key']", "optional.none()"), + OPTIONAL_FIELD_SELECTION_PROTO3_PRIMITIVE_ZERO( + "TestAllTypes{single_int32: 0}.?single_int32", "optional.none()"), + OPTIONAL_FIELD_SELECTION_PROTO3_PRIMITIVE_NONZERO( + "TestAllTypes{single_int32: 5}.?single_int32", "optional.of(5)"), + OPTIONAL_FIELD_SELECTION_PROTO3_MESSAGE_EMPTY( + "TestAllTypes{}.?standalone_message", "optional.none()"), + OPTIONAL_FIELD_SELECTION_PROTO3_MESSAGE_PRESENT( + "TestAllTypes{standalone_message:" + + " TestAllTypes.NestedMessage{}}.?standalone_message.hasValue()", + "true"), + OPTIONAL_FIELD_SELECTION_PROTO3_WRAPPER_NULL( + "TestAllTypes{}.?single_int64_wrapper", "optional.none()"), + OPTIONAL_FIELD_SELECTION_PROTO3_WRAPPER_EXPLICIT_NULL( + "TestAllTypes{single_int64_wrapper: null}.?single_int64_wrapper", "optional.none()"), + OPTIONAL_FIELD_SELECTION_PROTO3_WRAPPER_PRESENT( + "TestAllTypes{single_int64_wrapper: 42}.?single_int64_wrapper", "optional.of(42)"), + OPTIONAL_FIELD_SELECTION_DYNAMIC_MISS( + "dyn_map == {'a': 1} ? dyn_map.?b : optional.none()", "optional.none()"), + OPTIONAL_FIELD_SELECTION_TYPE_GUARDING( + "type(dyn_var) == map ? dyn_var.?key == optional.none() || dyn_var.?key.hasValue() : true", + "true"), + OPTIONAL_FIELD_SELECTION_MAP_COMPREHENSION( + "{'a': 1, 'b': 2}.transformMap(k, v, v > 1, v).?b", "optional.of(2)"), + OPTIONAL_FIELD_SELECTION_BINDER("cel.bind(m, {'a': 1}, m.?a)", "optional.of(1)"); + private final String exprA; private final String exprB; @@ -1601,7 +1635,16 @@ private enum EquivalenceViolationTestCase { OPTIONAL_OR_VALUE_VIOLATION("optional.of(x).orValue(y)", "y"), OPTIONAL_VALUE_VIOLATION("optional.of(x).value()", "y"), LIST_OPTIONAL_ELEMENTS_COLLISION("[1, ?opt_var]", "[1, opt_var]"), - CROSS_NUMERIC_EQUALITY_INT_DYN_VIOLATION("1 == request", "false"); + CROSS_NUMERIC_EQUALITY_INT_DYN_VIOLATION("1 == request", "false"), + OPTIONAL_SELECTION_VS_DIRECT_ERROR( + "{'a': 1}.?missing_key", "optional.of({'a': 1}.missing_key)"), + OPTIONAL_NESTED_NONE_VS_FLAT_NONE("{'a': optional.none()}.?a", "optional.none()"), + OPTIONAL_NULL_VALUE_VS_MISSING("{'a': null}.?a", "optional.none()"), + OPTIONAL_PROTO3_PRIMITIVE_ZERO_VS_OF_ZERO( + "TestAllTypes{single_int32: 0}.?single_int32", "optional.of(0)"), + OPTIONAL_PROTO3_WRAPPER_ZERO_VS_UNSET( + "TestAllTypes{single_int64_wrapper: 0}.?single_int64_wrapper", + "TestAllTypes{}.?single_int64_wrapper"); final String exprA; final String exprB; @@ -1961,8 +2004,8 @@ public void isSatisfiable_timeoutReached_throwsCelVerificationException() throws CelAbstractSyntaxTree ast = customCel .compile( - "d1 * d2 * d3 * d4 * d1 * d2 * d3 * d4 == 9429185123491285.0 && d1 > 100000.0 &&" - + " d2 > 100000.0 && d3 > 100000.0 && d4 > 100000.0") + "d1 * d2 * d3 * d4 * d1 * d2 * d3 * d4 * d1 * d2 * d3 * d4 * d1 * d2 * d3 * d4 ==" + + " 9429185123491285.0 && d1 > 1.0 && d2 > 1.0 && d3 > 1.0 && d4 > 1.0") .getAst(); CelVerificationException e = From 0bd9173ac05b8fb416eb1fa13c75462f0b57998d Mon Sep 17 00:00:00 2001 From: CEL Dev Team Date: Mon, 27 Jul 2026 21:24:22 -0700 Subject: [PATCH 144/204] No public description PiperOrigin-RevId: 955005982 --- testing/testrunner/BUILD.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/testrunner/BUILD.bazel b/testing/testrunner/BUILD.bazel index 043d62c88..44bc57cb3 100644 --- a/testing/testrunner/BUILD.bazel +++ b/testing/testrunner/BUILD.bazel @@ -93,7 +93,7 @@ java_library( java_library( name = "proto_descriptor_utils", - visibility = ["//:internal"], + visibility = ["//visibility:private"], exports = ["//testing/src/main/java/dev/cel/testing/utils:proto_descriptor_utils"], ) From 6b019ecb8aea6b1d5c1912ad878c14623776671b Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Tue, 28 Jul 2026 15:17:37 -0700 Subject: [PATCH 145/204] Implement JSON value unwrapping capability in verifier PiperOrigin-RevId: 955499463 --- .../main/java/dev/cel/verifier/BUILD.bazel | 1 + .../cel/verifier/CelAstToZ3Translator.java | 60 +++++++++++++ .../cel/verifier/CelZ3OperatorTranslator.java | 87 +++++++++++++----- .../cel/verifier/CelVerifierZ3ImplTest.java | 88 ++++++++++++++++--- 4 files changed, 202 insertions(+), 34 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel index 792ac5d2d..b9ac88687 100644 --- a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel +++ b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel @@ -129,6 +129,7 @@ java_library( "//common/ast", "//common/ast:cel_block", "//common/types", + "//common/types:cel_types", "//common/types:type_providers", "//verifier/axioms", "@maven//:com_google_errorprone_error_prone_annotations", diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java index bc9ad676a..8c0239efd 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java @@ -16,6 +16,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import com.microsoft.z3.ArithExpr; import com.microsoft.z3.ArrayExpr; import com.microsoft.z3.BoolExpr; @@ -37,6 +38,7 @@ import dev.cel.common.types.CelKind; import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.CelTypes; import dev.cel.common.types.ListType; import dev.cel.common.types.MapType; import dev.cel.common.types.NullableType; @@ -79,6 +81,7 @@ final class CelAstToZ3Translator { private static final String EMPTY_MSG_REF_PREFIX = "!empty_msg_ref_"; private static final String EMPTY_LIST_PREFIX = "!empty_list"; private static final String EMPTY_MAP_PREFIX = "!empty_map"; + private static final String NULL_VALUE_FIELD = "null_value"; private final Context ctx; private final CelZ3TypeSystem typeSystem; private final CelZ3OperatorTranslator operatorTranslator; @@ -370,6 +373,10 @@ private TranslatedValue translateMap(CelExpr celExpr, CelAbstractSyntaxTree ast) private TranslatedValue translateStruct(CelExpr celExpr, CelAbstractSyntaxTree ast) { CelExpr.CelStruct createStruct = celExpr.struct(); + if (isJsonWkt(createStruct.messageName())) { + return translateJsonWktStruct(celExpr, createStruct, ast); + } + // Bypass SMT when the struct is empty (return the cached SMT default pointer) if (createStruct.entries().isEmpty()) { return TranslatedValue.create( @@ -448,6 +455,59 @@ private TranslatedValue translateStruct(CelExpr celExpr, CelAbstractSyntaxTree a return TranslatedValue.propagateStrict(ctx, typeSystem, result, celExpr, elementsTv); } + private static boolean isJsonWkt(String messageName) { + return messageName.equals(CelTypes.VALUE_MESSAGE) + || messageName.equals(CelTypes.LIST_VALUE_MESSAGE) + || messageName.equals(CelTypes.STRUCT_MESSAGE); + } + + // Concretize JSON WKT unwrapping directly into native Z3 primitives to avoid + // sort incompatibilities (Message == String) and solver performance penalties (quantifiers). + private TranslatedValue translateJsonWktStruct( + CelExpr celExpr, CelExpr.CelStruct createStruct, CelAbstractSyntaxTree ast) { + Expr fallback; + if (createStruct.messageName().equals(CelTypes.VALUE_MESSAGE)) { + fallback = typeSystem.mkNull(); + } else if (createStruct.messageName().equals(CelTypes.LIST_VALUE_MESSAGE)) { + fallback = getDefaultValueForType(ListType.create(SimpleType.DYN)); + } else { + fallback = getDefaultValueForType(MapType.create(SimpleType.STRING, SimpleType.DYN)); + } + + if (createStruct.entries().isEmpty()) { + return TranslatedValue.create(fallback, celExpr, typeSystem, ctx.mkFalse()); + } + + // JSON WKT messages (gp.Struct, gp.Value, gp.ListValue) can only have a single top-level + // field entry in non-empty creation literals (e.g., 'fields' for Struct, 'values' for + // ListValue, or a single 'oneof' field for Value). + CelExpr.CelStruct.Entry entry = Iterables.getOnlyElement(createStruct.entries()); + + // Translate the value to properly capture approximations and Optionals + TranslatedValue entryTv = translateExpr(entry.value(), ast); + Expr finalVal = entryTv.z3Expr(); + + boolean isNullValueField = + createStruct.messageName().equals(CelTypes.VALUE_MESSAGE) + && entry.fieldKey().equals(NULL_VALUE_FIELD); + + if (entry.optionalEntry()) { + Expr optRef = typeSystem.getOptionalRef(finalVal); + BoolExpr hasValue = typeSystem.optHasValue(optRef); + + Expr unpackedVal = typeSystem.getOptionalValue(optRef); + if (isNullValueField) { + unpackedVal = typeSystem.mkNull(); + } + finalVal = ctx.mkITE(hasValue, unpackedVal, fallback); + } else if (isNullValueField) { + finalVal = typeSystem.mkNull(); + } + + return TranslatedValue.propagateStrict( + ctx, typeSystem, finalVal, celExpr, ImmutableList.of(entryTv)); + } + private Expr getDefaultValueForType(CelType type) { if (type instanceof NullableType) { return typeSystem.mkNull(); diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java index 955c3d530..effa91c2b 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java @@ -805,38 +805,83 @@ private Expr buildMapIndex( private TranslatedValue translateIndex( List args, CelAbstractSyntaxTree ast, boolean isOptional) { - Expr lhsTrans = args.get(0).z3Expr(); - Expr rhsTrans = args.get(1).z3Expr(); - TranslatedValue lhs = args.get(0); TranslatedValue rhs = args.get(1); CelType lhsType = extractAstTypeOrDefault(lhs, ast); CelType rhsType = extractAstTypeOrDefault(rhs, ast); - Expr actualValue; + Expr lhsTrans = lhs.z3Expr(); + Expr rhsTrans = rhs.z3Expr(); + + BoolExpr isLhsOpt = ctx.mkFalse(); + BoolExpr lhsHasValue = ctx.mkFalse(); + BoolExpr shouldEvaluate = ctx.mkTrue(); + + if (isOptional) { + isLhsOpt = typeSystem.isOptional(lhsTrans); + Expr optRef = typeSystem.getOptionalRef(lhsTrans); + lhsHasValue = typeSystem.optHasValue(optRef); + + lhsTrans = ctx.mkITE(isLhsOpt, typeSystem.getOptionalValue(optRef), lhsTrans); + shouldEvaluate = (BoolExpr) ctx.mkITE(isLhsOpt, lhsHasValue, ctx.mkTrue()); + + if (lhsType instanceof OptionalType) { + lhsType = lhsType.parameters().get(0); + } + } + + Expr actualValue = + buildAndConstrainIndex(lhsTrans, rhsTrans, lhsType, rhsType, shouldEvaluate, isOptional); + + if (isOptional) { + actualValue = + ctx.mkITE( + ctx.mkAnd(isLhsOpt, ctx.mkNot(lhsHasValue)), + typeSystem.mkOptionalNone(), + actualValue); + } + + return TranslatedValue.propagateStrict(ctx, typeSystem, actualValue, args); + } + + private Expr buildAndConstrainIndex( + Expr lhsTrans, + Expr rhsTrans, + CelType lhsType, + CelType rhsType, + BoolExpr shouldEvaluate, + boolean isOptional) { + CelType expectedElemType = null; if (lhsType.kind() == CelKind.LIST && rhsType.kind() == CelKind.INT) { - actualValue = buildListIndex(lhsTrans, rhsTrans, ctx.mkTrue(), isOptional); - constraintSink.accept( - ctx.mkImplies( - ctx.mkNot(typeSystem.isError(actualValue)), - typeConstraintGenerator.apply(actualValue, ((ListType) lhsType).elemType()))); + expectedElemType = ((ListType) lhsType).elemType(); } else if (lhsType.kind() == CelKind.MAP) { - actualValue = buildMapIndex(lhsTrans, rhsTrans, ctx.mkTrue(), isOptional); + expectedElemType = ((MapType) lhsType).valueType(); + } + + if (expectedElemType != null) { + Expr actualValue = + lhsType.kind() == CelKind.LIST + ? buildListIndex(lhsTrans, rhsTrans, shouldEvaluate, isOptional) + : buildMapIndex(lhsTrans, rhsTrans, shouldEvaluate, isOptional); + + CelType finalType = isOptional ? OptionalType.create(expectedElemType) : expectedElemType; + constraintSink.accept( ctx.mkImplies( - ctx.mkNot(typeSystem.isError(actualValue)), - typeConstraintGenerator.apply(actualValue, ((MapType) lhsType).valueType()))); - } else { - BoolExpr isListGuard = ctx.mkAnd(typeSystem.isList(lhsTrans), typeSystem.isInt(rhsTrans)); - BoolExpr isMapGuard = typeSystem.isMap(lhsTrans); - actualValue = - CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) - .addCase(isListGuard, buildListIndex(lhsTrans, rhsTrans, isListGuard, isOptional)) - .addCase(isMapGuard, buildMapIndex(lhsTrans, rhsTrans, isMapGuard, isOptional)) - .build(typeSystem.mkError()); + ctx.mkAnd(shouldEvaluate, ctx.mkNot(typeSystem.isError(actualValue))), + typeConstraintGenerator.apply(actualValue, finalType))); + + return actualValue; } - return TranslatedValue.propagateStrict(ctx, typeSystem, actualValue, args); + BoolExpr isListGuard = + ctx.mkAnd(shouldEvaluate, typeSystem.isList(lhsTrans), typeSystem.isInt(rhsTrans)); + BoolExpr isMapGuard = ctx.mkAnd(shouldEvaluate, typeSystem.isMap(lhsTrans)); + + return CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(isListGuard, buildListIndex(lhsTrans, rhsTrans, isListGuard, isOptional)) + .addCase(isMapGuard, buildMapIndex(lhsTrans, rhsTrans, isMapGuard, isOptional)) + .build(typeSystem.mkError()); } private TranslatedValue translateConditional( diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index 8a975f37a..2b41408d7 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -115,18 +115,21 @@ public final class CelVerifierZ3ImplTest { .addVar( "test_all_types", StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")) + .addVar("json_val", StructTypeReference.create("google.protobuf.Value")) + .addVar("json_list", StructTypeReference.create("google.protobuf.ListValue")) + .addVar("json_struct", StructTypeReference.create("google.protobuf.Struct")) .build(); - private static final CelVerifier VERIFIER = - CelVerifierFactory.newVerifier() - .setTypeProvider( - ProtoMessageTypeProvider.newBuilder() - .addDescriptors( - ImmutableList.of( - TestAllTypes.getDescriptor(), TestAllTypes.NestedMessage.getDescriptor())) - .build()) + private static final ProtoMessageTypeProvider TYPE_PROVIDER = + ProtoMessageTypeProvider.newBuilder() + .addDescriptors( + ImmutableList.of( + TestAllTypes.getDescriptor(), TestAllTypes.NestedMessage.getDescriptor())) .build(); + private static final CelVerifier VERIFIER = + CelVerifierFactory.newVerifier().setTypeProvider(TYPE_PROVIDER).build(); + @Before public void setUp() { System.setProperty("z3.skipLibraryLoad", "true"); @@ -226,9 +229,6 @@ private enum IsSatisfiableInconclusiveTestCase { MASKED_BY_BMC_MAP( "string_int_map == {'a':1, 'b':2, 'c':3, 'd':4, 'e':5, 'f':6} ? string_int_map.exists(k," + " k == 'g') : false"), - MASKED_BY_BMC_NESTED( - "nested_list == [[1, 2, 3, 4, 5, 6]] ? nested_list.exists(row, row.exists(x, x == 42)) :" - + " false"), APPROXIMATED_STRING_TO_INT("int('123') == 123"), APPROXIMATED_DOUBLE_TO_INT("int(1.5) == 1"), APPROXIMATED_INT_TO_STRING("string(123) == '123'"), @@ -252,6 +252,24 @@ public void isSatisfiable_inconclusive(@TestParameter IsSatisfiableInconclusiveT assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); } + @Test + public void isSatisfiable_maskedByBmcNested_inconclusive() throws Exception { + String expr = + "nested_list == [[1, 2, 3, 4, 5, 6]] ? nested_list.exists(row, row.exists(x, x == 42)) :" + + " false"; + CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); + + CelVerifier customVerifier = + CelVerifierFactory.newVerifier() + .setComprehensionUnrollLimit(3) + .setTypeProvider(TYPE_PROVIDER) + .build(); + + CelVerificationResult result = customVerifier.isSatisfiable(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + } + @Test public void isSatisfiable_comprehensionZeroUnrollLimit_inconclusive() throws Exception { String expr = "int_list == [1] ? int_list.exists(x, x == 1) : false"; @@ -1581,6 +1599,9 @@ private enum EquivalenceTestCase { "TestAllTypes{}.?single_int64_wrapper", "optional.none()"), OPTIONAL_FIELD_SELECTION_PROTO3_WRAPPER_EXPLICIT_NULL( "TestAllTypes{single_int64_wrapper: null}.?single_int64_wrapper", "optional.none()"), + OPTIONAL_FIELD_SELECTION_PROTO3_WRAPPER_OPTIONAL_NONE( + "TestAllTypes{?single_int64_wrapper: optional.none()}.?single_int64_wrapper", + "optional.none()"), OPTIONAL_FIELD_SELECTION_PROTO3_WRAPPER_PRESENT( "TestAllTypes{single_int64_wrapper: 42}.?single_int64_wrapper", "optional.of(42)"), OPTIONAL_FIELD_SELECTION_DYNAMIC_MISS( @@ -1590,7 +1611,43 @@ private enum EquivalenceTestCase { "true"), OPTIONAL_FIELD_SELECTION_MAP_COMPREHENSION( "{'a': 1, 'b': 2}.transformMap(k, v, v > 1, v).?b", "optional.of(2)"), - OPTIONAL_FIELD_SELECTION_BINDER("cel.bind(m, {'a': 1}, m.?a)", "optional.of(1)"); + OPTIONAL_FIELD_SELECTION_BINDER("cel.bind(m, {'a': 1}, m.?a)", "optional.of(1)"), + JSON_VALUE_BOOL("google.protobuf.Value{bool_value: true}", "true"), + JSON_VALUE_NUMBER("google.protobuf.Value{number_value: 1.0}", "1.0"), + JSON_VALUE_NULL("google.protobuf.Value{null_value: 0}", "null"), + JSON_VALUE_EMPTY("google.protobuf.Value{}", "null"), + JSON_LIST_VALUE_EMPTY("google.protobuf.ListValue{}", "[]"), + JSON_STRUCT_EMPTY("google.protobuf.Struct{}", "{}"), + JSON_LIST_VALUE("google.protobuf.ListValue{values: [1, 2]}", "[1, 2]"), + JSON_STRUCT("google.protobuf.Struct{fields: {'a': 1}}", "{'a': 1}"), + JSON_STRUCT_MULTIPLE_FIELDS( + "google.protobuf.Struct{fields: {'a': 1, 'b': 'hello'}}", "{'a': 1, 'b': 'hello'}"), + JSON_STRUCT_EXPLICIT_VALUES( + "google.protobuf.Struct{fields: {'a': google.protobuf.Value{number_value: 1.0}, 'b':" + + " google.protobuf.Value{string_value: 'hello'}}}", + "{'a': 1.0, 'b': 'hello'}"), + JSON_STRUCT_OPTIONAL_ENTRIES( + "google.protobuf.Struct{fields: {'a': 1, ?'b': optional.of(2), ?'c': optional.none()}}", + "{'a': 1, 'b': 2}"), + JSON_DEEP_NESTING( + "google.protobuf.ListValue{values: [google.protobuf.Struct{fields: {'a':" + + " google.protobuf.Value{number_value: 1.0}}}]}", + "[{'a': 1.0}]"), + JSON_NUMBER_HETEROGENEOUS_EQUALITY("google.protobuf.Value{number_value: 1.0} == 1", "true"), + JSON_VALUE_OPTIONAL_NONE("google.protobuf.Value{?string_value: optional.none()}", "null"), + JSON_LIST_VALUE_OPTIONAL_NONE("google.protobuf.ListValue{?values: optional.none()}", "[]"), + JSON_STRUCT_OPTIONAL_NONE("google.protobuf.Struct{?fields: optional.none()}", "{}"), + JSON_VALUE_TYPE_REFLECTION("type(google.protobuf.Value{string_value: 'hi'}) == string", "true"), + JSON_STRUCT_TYPE_REFLECTION("type(google.protobuf.Struct{fields: {'a': 1}}) == map", "true"), + JSON_VAR_VALUE_EQUALITY("json_val == 'hi' || json_val != 'hi'", "true"), + JSON_VAR_LIST_EQUALITY("json_list == [1, 2] || json_list != [1, 2]", "true"), + JSON_VAR_MAP_EQUALITY("json_struct == {'a': 1} || json_struct != {'a': 1}", "true"), + JSON_VALUE_OPTIONAL_NULL_VALUE_NONE( + "google.protobuf.Value{?null_value: optional.none()}", "null"), + JSON_VALUE_OPTIONAL_NULL_VALUE_OF("google.protobuf.Value{?null_value: optional.of(0)}", "null"), + OPTIONAL_INDEX_LIST_UNWRAPPING("optional.of([1, 2, 3])[?0]", "optional.of(1)"), + OPTIONAL_INDEX_MAP_UNWRAPPING("optional.of({'a': 1})[?'a']", "optional.of(1)"), + OPTIONAL_INDEX_UNWRAPPING_NONE("optional.none()[?0]", "optional.none()"); private final String exprA; private final String exprB; @@ -1644,7 +1701,12 @@ private enum EquivalenceViolationTestCase { "TestAllTypes{single_int32: 0}.?single_int32", "optional.of(0)"), OPTIONAL_PROTO3_WRAPPER_ZERO_VS_UNSET( "TestAllTypes{single_int64_wrapper: 0}.?single_int64_wrapper", - "TestAllTypes{}.?single_int64_wrapper"); + "TestAllTypes{}.?single_int64_wrapper"), + OPTIONAL_PROTO3_WRAPPER_ZERO_VS_NONE( + "TestAllTypes{single_int64_wrapper: 0}.?single_int64_wrapper", "optional.none()"), + OPTIONAL_DYNAMIC_TARGET_TYPE_MISMATCH("dyn_var.?a == optional.none()", "false"), + OPTIONAL_NESTED_NONE_VS_MISSING( + "{'a': optional.none()}.?a.orValue(optional.of(1))", "optional.of(1)"); final String exprA; final String exprB; From 36b268b6d600c71614b3358aac9e5ecc16541e0e Mon Sep 17 00:00:00 2001 From: Jonathan Tatum Date: Tue, 28 Jul 2026 15:43:42 -0700 Subject: [PATCH 146/204] Add limit for expression nodes during parsing. This is mainly to guard against misbehaving macros that can make a very large AST relative to the input expression size. PiperOrigin-RevId: 955512488 --- .../java/dev/cel/bundle/CelEnvironment.java | 4 +- .../cel/bundle/CelEnvironmentExporter.java | 5 ++ .../bundle/CelEnvironmentExporterTest.java | 4 +- .../dev/cel/bundle/CelEnvironmentTest.java | 25 +++++++++- .../main/java/dev/cel/common/CelOptions.java | 11 ++++ .../src/main/java/dev/cel/parser/Parser.java | 50 ++++++++++++++----- .../dev/cel/parser/CelParserImplTest.java | 48 ++++++++++++++++++ 7 files changed, 131 insertions(+), 16 deletions(-) diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java index ccbaef61b..6b4684b27 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java @@ -85,7 +85,9 @@ public abstract class CelEnvironment { "cel.limit.parse_error_recovery", CelOptions.Builder::maxParseErrorRecoveryLimit, "cel.limit.parse_recursion_depth", - CelOptions.Builder::maxParseRecursionDepth); + CelOptions.Builder::maxParseRecursionDepth, + "cel.limit.expression_node_count", + CelOptions.Builder::maxParseExpressionNodeCount); private static final ImmutableMap FEATURE_HANDLERS = ImmutableMap.of( diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java index d233fd36f..6e10edd92 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentExporter.java @@ -237,6 +237,11 @@ private void addOptions(CelEnvironment.Builder envBuilder, CelOptions options) { CelEnvironment.Limit.create( "cel.limit.parse_recursion_depth", options.maxParseRecursionDepth())); } + if (options.maxParseExpressionNodeCount() != CelOptions.DEFAULT.maxParseExpressionNodeCount()) { + limits.add( + CelEnvironment.Limit.create( + "cel.limit.expression_node_count", options.maxParseExpressionNodeCount())); + } envBuilder.setLimits(limits.build()); } diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java index ae0de2c18..7560a12aa 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java @@ -348,6 +348,7 @@ public void options() { .maxExpressionCodePointSize(100) .maxParseErrorRecoveryLimit(10) .maxParseRecursionDepth(10) + .maxParseExpressionNodeCount(500) .enableQuotedIdentifierSyntax(true) .enableHeterogeneousNumericComparisons(true) .populateMacroCalls(true) @@ -365,6 +366,7 @@ public void options() { .containsExactly( CelEnvironment.Limit.create("cel.limit.expression_code_points", 100), CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10), - CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10)); + CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10), + CelEnvironment.Limit.create("cel.limit.expression_node_count", 500)); } } diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java index a5a2f3e6d..a48ea0ff8 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java @@ -134,7 +134,8 @@ public void extend_allLimits() throws Exception { .setLimits( CelEnvironment.Limit.create("cel.limit.expression_code_points", 20), CelEnvironment.Limit.create("cel.limit.parse_error_recovery", 10), - CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10)) + CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10), + CelEnvironment.Limit.create("cel.limit.expression_node_count", 500)) .build(); Cel cel = @@ -147,6 +148,7 @@ public void extend_allLimits() throws Exception { assertThat(checkerOptions.maxExpressionCodePointSize()).isEqualTo(20); assertThat(checkerOptions.maxParseErrorRecoveryLimit()).isEqualTo(10); assertThat(checkerOptions.maxParseRecursionDepth()).isEqualTo(10); + assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(500); CelAbstractSyntaxTree ast = cel.compile("1 + 2 + 3 + 4 + 5").getAst(); Long result = (Long) cel.createProgram(ast).eval(); @@ -158,6 +160,27 @@ public void extend_allLimits() throws Exception { .contains("expression code point size exceeds limit: size: 21, limit 20"); } + @Test + public void extend_expressionNodeCountLimit() throws Exception { + CelEnvironment environment = + CelEnvironment.newBuilder() + .setLimits(CelEnvironment.Limit.create("cel.limit.expression_node_count", 2)) + .build(); + + Cel cel = + environment.extend( + CelFactory.legacyCelBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .build(), + CelOptions.DEFAULT); + CelOptions checkerOptions = cel.toCheckerBuilder().options(); + assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(2); + + CelValidationResult validationResult = cel.compile("1 + 2 + 3"); + assertThat(validationResult.hasError()).isTrue(); + assertThat(validationResult.getErrorString()).contains("expression node limit (2) exceeded"); + } + @Test public void extend_unsupportedFeatureFlag_throws() throws Exception { CelEnvironment environment = diff --git a/common/src/main/java/dev/cel/common/CelOptions.java b/common/src/main/java/dev/cel/common/CelOptions.java index d9c2dd818..3525e45d7 100644 --- a/common/src/main/java/dev/cel/common/CelOptions.java +++ b/common/src/main/java/dev/cel/common/CelOptions.java @@ -60,6 +60,8 @@ public enum ProtoUnsetFieldOptions { public abstract int maxParseRecursionDepth(); + public abstract int maxParseExpressionNodeCount(); + public abstract boolean populateMacroCalls(); public abstract boolean retainRepeatedUnaryOperators(); @@ -134,6 +136,7 @@ public static Builder newBuilder() { .maxExpressionCodePointSize(100_000) .maxParseErrorRecoveryLimit(30) .maxParseRecursionDepth(250) + .maxParseExpressionNodeCount(1_000_000) .populateMacroCalls(false) .retainRepeatedUnaryOperators(false) .retainUnbalancedLogicalExpressions(false) @@ -223,6 +226,14 @@ public abstract static class Builder { /** Limit the amount of recursion within parse expressions. */ public abstract Builder maxParseRecursionDepth(int value); + /** + * Set a limit on the number of expression nodes in the abstract syntax tree for the expression. + * This prevents cases where macro expansion results in an AST that is larger than expected from + * the source expression. Once exceeded, the parser will record an error and stop expanding + * macros but continue parsing to report other errors. + */ + public abstract Builder maxParseExpressionNodeCount(int value); + /** Populate macro_calls map in source_info with macro calls parsed from the expression. */ public abstract Builder populateMacroCalls(boolean value); diff --git a/parser/src/main/java/dev/cel/parser/Parser.java b/parser/src/main/java/dev/cel/parser/Parser.java index af860e936..0e6849056 100644 --- a/parser/src/main/java/dev/cel/parser/Parser.java +++ b/parser/src/main/java/dev/cel/parser/Parser.java @@ -153,7 +153,8 @@ static CelValidationResult parse(CelParserImpl parser, CelSource source, CelOpti new ExprFactory( antlrParser, sourceInfo, - options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME); + options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME, + options.maxParseExpressionNodeCount()); Parser parserImpl = new Parser(parser, options, sourceInfo, exprFactory); ErrorListener errorListener = new ErrorListener(exprFactory); antlrLexer.removeErrorListeners(); @@ -655,6 +656,12 @@ private Optional visitMacro( ImmutableList args, Optional target, CelMacro macro) { + if (exprFactory.isNodeLimitExceeded()) { + return Optional.of( + exprFactory.reportError( + exprFactory.getPosition(expr.id()), + "could not expand macro: expression node limit exceeded")); + } Optional expandedMacro = expandMacro( @@ -1077,16 +1084,20 @@ private static final class ExprFactory extends CelMacroExprFactory { private final ArrayList issues; private final ArrayDeque positions; private final String accumulatorVarName; + private final int maxExpressionNodeCount; + private boolean nodeLimitExceeded; private ExprFactory( org.antlr.v4.runtime.Parser recognizer, CelSource.Builder sourceInfo, - String accumulatorVarName) { + String accumulatorVarName, + int maxExpressionNodeCount) { this.recognizer = recognizer; this.sourceInfo = sourceInfo; this.issues = new ArrayList<>(); this.positions = new ArrayDeque<>(1); // Currently this usually contains at most 1 position. this.accumulatorVarName = accumulatorVarName; + this.maxExpressionNodeCount = maxExpressionNodeCount; } // Implementation of CelExprFactory. @@ -1110,12 +1121,6 @@ public CelExpr reportError(CelIssue error) { return ERROR; } - @Override - public String getAccumulatorVarName() { - return accumulatorVarName; - } - - // Internal methods used by the parser but not part of the public API. @FormatMethod @CanIgnoreReturnValue private CelExpr reportError( @@ -1133,8 +1138,18 @@ private CelExpr reportError(Token token, String message) { return reportError(CelIssue.formatError(getLocation(token), message)); } + @CanIgnoreReturnValue + private CelExpr reportError(int position, String message) { + return reportError(CelIssue.formatError(getLocation(position), message)); + } + // Implementation of CelExprFactory. + @Override + public String getAccumulatorVarName() { + return accumulatorVarName; + } + @Override protected CelSourceLocation currentSourceLocationForMacro() { checkState(!positions.isEmpty()); // Should only be called while expanding macros. @@ -1143,6 +1158,10 @@ protected CelSourceLocation currentSourceLocationForMacro() { // Internal methods used by the parser but not part of the public API. + private boolean isNodeLimitExceeded() { + return nodeLimitExceeded; + } + private void pushPosition(int position) { positions.addLast(position); } @@ -1159,17 +1178,17 @@ private int peekPosition() { private long nextExprId(int position) { long exprId = super.nextExprId(); + if (exprId > maxExpressionNodeCount && !nodeLimitExceeded) { + nodeLimitExceeded = true; + reportError( + position, String.format("expression node limit (%d) exceeded", maxExpressionNodeCount)); + } if (position != -1) { sourceInfo.addPositions(exprId, position); } return exprId; } - @Override - public long copyExprId(long id) { - return nextExprId(getPosition(id)); - } - @Override public long nextExprId() { checkState(!positions.isEmpty()); // Should only be called while expanding macros. @@ -1177,6 +1196,11 @@ public long nextExprId() { return nextExprId(peekPosition()); } + @Override + public long copyExprId(long id) { + return nextExprId(getPosition(id)); + } + private List getIssuesList() { return issues; } diff --git a/parser/src/test/java/dev/cel/parser/CelParserImplTest.java b/parser/src/test/java/dev/cel/parser/CelParserImplTest.java index 1e7b44fab..756e97d31 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserImplTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserImplTest.java @@ -256,6 +256,54 @@ public void parse_exprUnderMaxRecursionLimit_doesNotThrow( assertThat(parseResult.getAst()).isNotNull(); } + @Test + public void parse_nodeLimitExceeded_throws() { + CelParser parser = + CelParserImpl.newBuilder() + .setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(2).build()) + .build(); + CelValidationResult parseResult = parser.parse("a + b + c"); + + CelValidationException exception = + assertThrows(CelValidationException.class, parseResult::getAst); + assertThat(exception).hasMessageThat().contains("expression node limit (2) exceeded"); + assertThat(exception.getErrors()).hasSize(1); + } + + @Test + public void parse_macroExpansionNodeLimitExceeded_throws() { + CelParser parser = + CelParserImpl.newBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(5).build()) + .build(); + CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)"); + + CelValidationException exception = + assertThrows(CelValidationException.class, parseResult::getAst); + assertThat(exception).hasMessageThat().contains("expression node limit (5) exceeded"); + assertThat( + exception.getErrors().stream() + .anyMatch( + issue -> + issue + .getMessage() + .contains("could not expand macro: expression node limit exceeded"))) + .isTrue(); + } + + @Test + public void parse_macroExpansionNodeLimitNotExceeded_success() throws CelValidationException { + CelParser parser = + CelParserImpl.newBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(100).build()) + .build(); + CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)"); + assertThat(parseResult.hasError()).isFalse(); + assertThat(parseResult.getAst()).isNotNull(); + } + @Test @TestParameters("{expression: 'A.map(a?b, c)'}") @TestParameters("{expression: 'A.all(a?b, c)'}") From 2a08d7861ec1da1b4a3e3adc8114c56463f50b8e Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Tue, 28 Jul 2026 15:56:36 -0700 Subject: [PATCH 147/204] Fix floating point comparisons involving infinity/NaN for cross-type numeric comparisons PiperOrigin-RevId: 955519324 --- .../dev/cel/verifier/axioms/AxiomHelpers.java | 48 ++++++++++++++++++ .../dev/cel/verifier/axioms/GreaterAxiom.java | 28 ++++++----- .../verifier/axioms/GreaterEqualsAxiom.java | 28 ++++++----- .../dev/cel/verifier/axioms/LessAxiom.java | 20 +++++--- .../cel/verifier/axioms/LessEqualsAxiom.java | 20 +++++--- .../cel/verifier/CelVerifierZ3ImplTest.java | 50 +++++++++++++++++++ 6 files changed, 154 insertions(+), 40 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/AxiomHelpers.java b/verifier/src/main/java/dev/cel/verifier/axioms/AxiomHelpers.java index a249b4fe9..c7b101e21 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/AxiomHelpers.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/AxiomHelpers.java @@ -16,7 +16,9 @@ import com.microsoft.z3.BoolExpr; import com.microsoft.z3.Context; +import com.microsoft.z3.FPExpr; import com.microsoft.z3.IntExpr; +import com.microsoft.z3.RealExpr; /** Helper methods for Z3 axioms operations. */ final class AxiomHelpers { @@ -47,5 +49,51 @@ static IntExpr mkTruncatedMod(Context ctx, IntExpr a, IntExpr b) { return (IntExpr) ctx.mkSub(a, ctx.mkMul(mkTruncatedDiv(ctx, a, b), b)); } + /** + * Safe comparison between a Z3 Real (from int/uint) and a Z3 FloatingPoint (double) for {@code + * <}. + */ + static BoolExpr mkRealLtFp(Context ctx, RealExpr real, FPExpr fp) { + return mkSafeFpComparison(ctx, fp, isPosInf(ctx, fp), ctx.mkLt(real, ctx.mkFPToReal(fp))); + } + + /** + * Safe comparison between a Z3 FloatingPoint (double) and a Z3 Real (from int/uint) for {@code + * <}. + */ + static BoolExpr mkFpLtReal(Context ctx, FPExpr fp, RealExpr real) { + return mkSafeFpComparison(ctx, fp, isNegInf(ctx, fp), ctx.mkLt(ctx.mkFPToReal(fp), real)); + } + + /** + * Safe comparison between a Z3 Real (from int/uint) and a Z3 FloatingPoint (double) for {@code + * <=}. + */ + static BoolExpr mkRealLeFp(Context ctx, RealExpr real, FPExpr fp) { + return mkSafeFpComparison(ctx, fp, isPosInf(ctx, fp), ctx.mkLe(real, ctx.mkFPToReal(fp))); + } + + /** + * Safe comparison between a Z3 FloatingPoint (double) and a Z3 Real (from int/uint) for {@code + * <=}. + */ + static BoolExpr mkFpLeReal(Context ctx, FPExpr fp, RealExpr real) { + return mkSafeFpComparison(ctx, fp, isNegInf(ctx, fp), ctx.mkLe(ctx.mkFPToReal(fp), real)); + } + + private static BoolExpr isPosInf(Context ctx, FPExpr fp) { + return ctx.mkAnd(ctx.mkFPIsInfinite(fp), ctx.mkFPIsPositive(fp)); + } + + private static BoolExpr isNegInf(Context ctx, FPExpr fp) { + return ctx.mkAnd(ctx.mkFPIsInfinite(fp), ctx.mkFPIsNegative(fp)); + } + + private static BoolExpr mkSafeFpComparison( + Context ctx, FPExpr fp, BoolExpr infCondition, BoolExpr finiteComparison) { + BoolExpr isFinite = ctx.mkNot(ctx.mkOr(ctx.mkFPIsNaN(fp), ctx.mkFPIsInfinite(fp))); + return ctx.mkOr(infCondition, ctx.mkAnd(isFinite, finiteComparison)); + } + private AxiomHelpers() {} } diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/GreaterAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/GreaterAxiom.java index 2dce5c8e6..527ef70ad 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/GreaterAxiom.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/GreaterAxiom.java @@ -88,33 +88,37 @@ final class GreaterAxiom { (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkGt( - ctx.mkInt2Real(typeSystem.getInt(lhs)), - ctx.mkFPToReal((FPExpr) typeSystem.getDouble(rhs)))))) + AxiomHelpers.mkFpLtReal( + ctx, + (FPExpr) typeSystem.getDouble(rhs), + ctx.mkInt2Real(typeSystem.getInt(lhs)))))) .addBinaryOverloadTranslator( Comparison.GREATER_UINT64_DOUBLE.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkGt( - ctx.mkInt2Real(typeSystem.getUint(lhs)), - ctx.mkFPToReal((FPExpr) typeSystem.getDouble(rhs)))))) + AxiomHelpers.mkFpLtReal( + ctx, + (FPExpr) typeSystem.getDouble(rhs), + ctx.mkInt2Real(typeSystem.getUint(lhs)))))) .addBinaryOverloadTranslator( Comparison.GREATER_DOUBLE_INT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkGt( - ctx.mkFPToReal((FPExpr) typeSystem.getDouble(lhs)), - ctx.mkInt2Real(typeSystem.getInt(rhs)))))) + AxiomHelpers.mkRealLtFp( + ctx, + ctx.mkInt2Real(typeSystem.getInt(rhs)), + (FPExpr) typeSystem.getDouble(lhs))))) .addBinaryOverloadTranslator( Comparison.GREATER_DOUBLE_UINT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkGt( - ctx.mkFPToReal((FPExpr) typeSystem.getDouble(lhs)), - ctx.mkInt2Real(typeSystem.getUint(rhs)))))) + AxiomHelpers.mkRealLtFp( + ctx, + ctx.mkInt2Real(typeSystem.getUint(rhs)), + (FPExpr) typeSystem.getDouble(lhs))))) .addBinaryOverloadTranslator( Comparison.GREATER_INT64_UINT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/GreaterEqualsAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/GreaterEqualsAxiom.java index b3c401aa7..ec3ffa69a 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/GreaterEqualsAxiom.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/GreaterEqualsAxiom.java @@ -88,33 +88,37 @@ final class GreaterEqualsAxiom { (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkGe( - ctx.mkInt2Real(typeSystem.getInt(lhs)), - ctx.mkFPToReal((FPExpr) typeSystem.getDouble(rhs)))))) + AxiomHelpers.mkFpLeReal( + ctx, + (FPExpr) typeSystem.getDouble(rhs), + ctx.mkInt2Real(typeSystem.getInt(lhs)))))) .addBinaryOverloadTranslator( Comparison.GREATER_EQUALS_UINT64_DOUBLE.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkGe( - ctx.mkInt2Real(typeSystem.getUint(lhs)), - ctx.mkFPToReal((FPExpr) typeSystem.getDouble(rhs)))))) + AxiomHelpers.mkFpLeReal( + ctx, + (FPExpr) typeSystem.getDouble(rhs), + ctx.mkInt2Real(typeSystem.getUint(lhs)))))) .addBinaryOverloadTranslator( Comparison.GREATER_EQUALS_DOUBLE_INT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkGe( - ctx.mkFPToReal((FPExpr) typeSystem.getDouble(lhs)), - ctx.mkInt2Real(typeSystem.getInt(rhs)))))) + AxiomHelpers.mkRealLeFp( + ctx, + ctx.mkInt2Real(typeSystem.getInt(rhs)), + (FPExpr) typeSystem.getDouble(lhs))))) .addBinaryOverloadTranslator( Comparison.GREATER_EQUALS_DOUBLE_UINT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkGe( - ctx.mkFPToReal((FPExpr) typeSystem.getDouble(lhs)), - ctx.mkInt2Real(typeSystem.getUint(rhs)))))) + AxiomHelpers.mkRealLeFp( + ctx, + ctx.mkInt2Real(typeSystem.getUint(rhs)), + (FPExpr) typeSystem.getDouble(lhs))))) .addBinaryOverloadTranslator( Comparison.GREATER_EQUALS_INT64_UINT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/LessAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/LessAxiom.java index 9c34709d0..429aee284 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/LessAxiom.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/LessAxiom.java @@ -88,32 +88,36 @@ final class LessAxiom { (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkLt( + AxiomHelpers.mkRealLtFp( + ctx, ctx.mkInt2Real(typeSystem.getInt(lhs)), - ctx.mkFPToReal((FPExpr) typeSystem.getDouble(rhs)))))) + (FPExpr) typeSystem.getDouble(rhs))))) .addBinaryOverloadTranslator( Comparison.LESS_UINT64_DOUBLE.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkLt( + AxiomHelpers.mkRealLtFp( + ctx, ctx.mkInt2Real(typeSystem.getUint(lhs)), - ctx.mkFPToReal((FPExpr) typeSystem.getDouble(rhs)))))) + (FPExpr) typeSystem.getDouble(rhs))))) .addBinaryOverloadTranslator( Comparison.LESS_DOUBLE_INT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkLt( - ctx.mkFPToReal((FPExpr) typeSystem.getDouble(lhs)), + AxiomHelpers.mkFpLtReal( + ctx, + (FPExpr) typeSystem.getDouble(lhs), ctx.mkInt2Real(typeSystem.getInt(rhs)))))) .addBinaryOverloadTranslator( Comparison.LESS_DOUBLE_UINT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkLt( - ctx.mkFPToReal((FPExpr) typeSystem.getDouble(lhs)), + AxiomHelpers.mkFpLtReal( + ctx, + (FPExpr) typeSystem.getDouble(lhs), ctx.mkInt2Real(typeSystem.getUint(rhs)))))) .addBinaryOverloadTranslator( Comparison.LESS_INT64_UINT64.celOverloadDecl(), diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/LessEqualsAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/LessEqualsAxiom.java index 750961515..5099850dd 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/LessEqualsAxiom.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/LessEqualsAxiom.java @@ -88,32 +88,36 @@ final class LessEqualsAxiom { (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkLe( + AxiomHelpers.mkRealLeFp( + ctx, ctx.mkInt2Real(typeSystem.getInt(lhs)), - ctx.mkFPToReal((FPExpr) typeSystem.getDouble(rhs)))))) + (FPExpr) typeSystem.getDouble(rhs))))) .addBinaryOverloadTranslator( Comparison.LESS_EQUALS_UINT64_DOUBLE.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkLe( + AxiomHelpers.mkRealLeFp( + ctx, ctx.mkInt2Real(typeSystem.getUint(lhs)), - ctx.mkFPToReal((FPExpr) typeSystem.getDouble(rhs)))))) + (FPExpr) typeSystem.getDouble(rhs))))) .addBinaryOverloadTranslator( Comparison.LESS_EQUALS_DOUBLE_INT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkLe( - ctx.mkFPToReal((FPExpr) typeSystem.getDouble(lhs)), + AxiomHelpers.mkFpLeReal( + ctx, + (FPExpr) typeSystem.getDouble(lhs), ctx.mkInt2Real(typeSystem.getInt(rhs)))))) .addBinaryOverloadTranslator( Comparison.LESS_EQUALS_DOUBLE_UINT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkLe( - ctx.mkFPToReal((FPExpr) typeSystem.getDouble(lhs)), + AxiomHelpers.mkFpLeReal( + ctx, + (FPExpr) typeSystem.getDouble(lhs), ctx.mkInt2Real(typeSystem.getUint(rhs)))))) .addBinaryOverloadTranslator( Comparison.LESS_EQUALS_INT64_UINT64.celOverloadDecl(), diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index 2b41408d7..59a5e8438 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -498,6 +498,34 @@ private enum IsAlwaysTrueTestCase { IEEE_754_NEG_ZERO_IN_LIST("-0.0 in [0.0]"), IEEE_754_POS_ZERO_IN_LIST("0.0 in [-0.0]"), IEEE_754_NAN_IN_LIST_FALSE("!((0.0/0.0) in [1.0, (0.0/0.0)])"), + CROSS_TYPE_NUMERIC_LESS_NAN_INT_FALSE("!(x < (0.0 / 0.0))"), + CROSS_TYPE_NUMERIC_LESS_INT_NAN_FALSE("!((0.0 / 0.0) < x)"), + CROSS_TYPE_NUMERIC_LESS_EQUALS_NAN_INT_FALSE("!(x <= (0.0 / 0.0))"), + CROSS_TYPE_NUMERIC_GREATER_NAN_INT_FALSE("!(x > (0.0 / 0.0))"), + CROSS_TYPE_NUMERIC_GREATER_EQUALS_NAN_INT_FALSE("!(x >= (0.0 / 0.0))"), + CROSS_TYPE_NUMERIC_LESS_NAN_UINT_FALSE("!(u < (0.0 / 0.0))"), + CROSS_TYPE_NUMERIC_LESS_UINT_NAN_FALSE("!((0.0 / 0.0) < u)"), + CROSS_TYPE_NUMERIC_LESS_EQUALS_NAN_UINT_FALSE("!(u <= (0.0 / 0.0))"), + CROSS_TYPE_NUMERIC_GREATER_NAN_UINT_FALSE("!(u > (0.0 / 0.0))"), + CROSS_TYPE_NUMERIC_GREATER_EQUALS_NAN_UINT_FALSE("!(u >= (0.0 / 0.0))"), + CROSS_TYPE_NUMERIC_LESS_POS_INF_INT("x < (1.0 / 0.0)"), + CROSS_TYPE_NUMERIC_LESS_EQUALS_POS_INF_INT("x <= (1.0 / 0.0)"), + CROSS_TYPE_NUMERIC_GREATER_POS_INF_INT_FALSE("!(x > (1.0 / 0.0))"), + CROSS_TYPE_NUMERIC_GREATER_EQUALS_POS_INF_INT_FALSE("!(x >= (1.0 / 0.0))"), + CROSS_TYPE_NUMERIC_GREATER_POS_INF_DOUBLE_INT("(1.0 / 0.0) > x"), + CROSS_TYPE_NUMERIC_GREATER_EQUALS_POS_INF_DOUBLE_INT("(1.0 / 0.0) >= x"), + CROSS_TYPE_NUMERIC_LESS_POS_INF_DOUBLE_INT_FALSE("!((1.0 / 0.0) < x)"), + CROSS_TYPE_NUMERIC_LESS_EQUALS_POS_INF_DOUBLE_INT_FALSE("!((1.0 / 0.0) <= x)"), + CROSS_TYPE_NUMERIC_GREATER_NEG_INF_INT("x > (-1.0 / 0.0)"), + CROSS_TYPE_NUMERIC_GREATER_EQUALS_NEG_INF_INT("x >= (-1.0 / 0.0)"), + CROSS_TYPE_NUMERIC_LESS_NEG_INF_INT_FALSE("!(x < (-1.0 / 0.0))"), + CROSS_TYPE_NUMERIC_LESS_EQUALS_NEG_INF_INT_FALSE("!(x <= (-1.0 / 0.0))"), + CROSS_TYPE_NUMERIC_LESS_NEG_INF_DOUBLE_INT("(-1.0 / 0.0) < x"), + CROSS_TYPE_NUMERIC_LESS_EQUALS_NEG_INF_DOUBLE_INT("(-1.0 / 0.0) <= x"), + CROSS_TYPE_NUMERIC_LESS_POS_INF_UINT("u < (1.0 / 0.0)"), + CROSS_TYPE_NUMERIC_LESS_EQUALS_POS_INF_UINT("u <= (1.0 / 0.0)"), + CROSS_TYPE_NUMERIC_GREATER_NEG_INF_UINT("u > (-1.0 / 0.0)"), + CROSS_TYPE_NUMERIC_GREATER_EQUALS_NEG_INF_UINT("u >= (-1.0 / 0.0)"), STRING_IN_LIST("'b' in ['a', 'b', 'c']"), INT_IN_MAP("1 in {1: 2}"), MAP_MISSING_KEY("!(3 in {1: 'a', 2: 'b'})"), @@ -1426,6 +1454,14 @@ private enum EquivalenceTestCase { CROSS_TYPE_NUMERIC_EQUALITY_INT_DOUBLE("request == 1.0", "request == 1"), CROSS_TYPE_NUMERIC_EQUALITY_UINT_DOUBLE("request == 1u", "request == 1.0"), CROSS_TYPE_NUMERIC_EQUALITY_INT_UINT("request == 1", "request == 1u"), + CROSS_TYPE_NUMERIC_NAN_LESS_INT("x < (0.0 / 0.0)", "false"), + CROSS_TYPE_NUMERIC_NAN_LESS_UINT("u < (0.0 / 0.0)", "false"), + CROSS_TYPE_NUMERIC_NAN_GREATER_INT("x > (0.0 / 0.0)", "false"), + CROSS_TYPE_NUMERIC_NAN_GREATER_UINT("u > (0.0 / 0.0)", "false"), + CROSS_TYPE_NUMERIC_POS_INF_GREATER_INT("(1.0 / 0.0) > x", "true"), + CROSS_TYPE_NUMERIC_POS_INF_LESS_INT("x < (1.0 / 0.0)", "true"), + CROSS_TYPE_NUMERIC_NEG_INF_GREATER_INT("x > (-1.0 / 0.0)", "true"), + CROSS_TYPE_NUMERIC_NEG_INF_LESS_INT("(-1.0 / 0.0) < x", "true"), STATIC_DOUBLE_EQUALITY("d + 1.0 == d + 1.0", "d == d"), MAP_FIELD_SELECT("string_int_map.my_field > 0", "string_int_map['my_field'] > 0"), HETEROGENEOUS_LIST_SIZES_SAFE_FALSE("!([1, 2] == [1, 2, 3])", "true"), @@ -2715,4 +2751,18 @@ public void verifyImplication_loopExceedsLimit_returnsTruncatedInconclusive() th assertThat(result.message()) .contains("implication holds within the current loop unroll limit"); } + + @Test + public void verifyImplication_symbolicNan_crossNumericComparisonReturnsFalse() throws Exception { + // Assumption: d is NaN (d != d) + CelAbstractSyntaxTree assumeAst = CEL.compile("d != d").getAst(); + // Assertion: x < d is false when d is NaN + CelAbstractSyntaxTree assertAst = CEL.compile("!(x < d)").getAst(); + + CelVerifier verifier = CelVerifierFactory.newVerifier().build(); + CelVerificationResult result = + ((CelVerifierZ3Impl) verifier) + .verifyImplication(assumeAst, assertAst, ImmutableMap.of(), "Implication"); + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } } From 8bfc4c7547bbf8aa86f6e86862d32fd6a8e0fcb5 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Tue, 28 Jul 2026 23:13:00 -0700 Subject: [PATCH 148/204] Fix conformance issues around type conversion overflows and duration subtractions PiperOrigin-RevId: 955679575 --- .../main/java/dev/cel/common/CelOptions.java | 12 +++++++++ .../dev/cel/common/internal/ProtoAdapter.java | 26 +++++++++---------- .../cel/common/internal/ProtoTimeUtils.java | 11 ++++++++ .../test/java/dev/cel/conformance/BUILD.bazel | 17 ++---------- .../java/dev/cel/runtime/RuntimeHelpers.java | 2 +- .../dev/cel/runtime/standard/IntFunction.java | 2 +- .../runtime/standard/SubtractOperator.java | 24 +++++++++++++++-- 7 files changed, 61 insertions(+), 33 deletions(-) diff --git a/common/src/main/java/dev/cel/common/CelOptions.java b/common/src/main/java/dev/cel/common/CelOptions.java index 3525e45d7..417d4dc9d 100644 --- a/common/src/main/java/dev/cel/common/CelOptions.java +++ b/common/src/main/java/dev/cel/common/CelOptions.java @@ -120,6 +120,8 @@ public enum ProtoUnsetFieldOptions { public abstract boolean enableComprehension(); + public abstract boolean enableTimestampOverflowCheck(); + public abstract int maxRegexProgramSize(); public abstract Builder toBuilder(); @@ -166,6 +168,7 @@ public static Builder newBuilder() { .unwrapWellKnownTypesOnFunctionDispatch(true) .fromProtoUnsetFieldOption(ProtoUnsetFieldOptions.BIND_DEFAULT) .enableComprehension(true) + .enableTimestampOverflowCheck(true) .maxRegexProgramSize(-1); } @@ -529,6 +532,15 @@ public abstract static class Builder { */ public abstract Builder enableJsonFieldNames(boolean value); + /** + * Enable or disable validating that duration values resulting from timestamp arithmetic do not + * overflow 64-bit nanoseconds. Defaults to enabled. + * + *

Disabling this option is an out-of-conformance behavior that suppresses nanosecond + * overflow validation when subtracting timestamps. + */ + public abstract Builder enableTimestampOverflowCheck(boolean value); + public abstract CelOptions build(); } } diff --git a/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java b/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java index 7e3910433..b1b56afe1 100644 --- a/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java +++ b/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java @@ -325,13 +325,6 @@ private BidiConverter fieldToValueConverter(FieldDescriptor fieldDescriptor) { value -> BidiConverter.IDENTITY.backwardConverter().convert(maybeUnwrap(value))); case FLOAT: return unwrapAndConvert(DOUBLE_CONVERTER); - case DOUBLE: - case SFIXED64: - case SINT64: - case INT64: - return BidiConverter.of( - BidiConverter.IDENTITY.forwardConverter(), - value -> BidiConverter.IDENTITY.backwardConverter().convert(maybeUnwrap(value))); case BYTES: if (celOptions.evaluateCanonicalTypesToNativeValues()) { return BidiConverter.of( @@ -342,10 +335,11 @@ private BidiConverter fieldToValueConverter(FieldDescriptor fieldDescriptor) { return BidiConverter.of( BidiConverter.IDENTITY.forwardConverter(), value -> BidiConverter.IDENTITY.backwardConverter().convert(maybeUnwrap(value))); + case DOUBLE: + case SFIXED64: + case SINT64: + case INT64: case STRING: - return BidiConverter.of( - BidiConverter.IDENTITY.forwardConverter(), - value -> BidiConverter.IDENTITY.backwardConverter().convert(maybeUnwrap(value))); case BOOL: return BidiConverter.of( BidiConverter.IDENTITY.forwardConverter(), @@ -353,10 +347,14 @@ private BidiConverter fieldToValueConverter(FieldDescriptor fieldDescriptor) { case ENUM: return BidiConverter.of( value -> (long) ((EnumValueDescriptor) value).getNumber(), - number -> - fieldDescriptor - .getEnumType() - .findValueByNumberCreatingIfUnknown(number.intValue())); + number -> { + if (number > Integer.MAX_VALUE || number < Integer.MIN_VALUE) { + throw new IllegalArgumentException("Enum value out of int32 range: " + number); + } + return fieldDescriptor + .getEnumType() + .findValueByNumberCreatingIfUnknown(number.intValue()); + }); case MESSAGE: return BidiConverter.of( this::adaptProtoToValue, diff --git a/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java b/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java index 36671842d..a6e98c571 100644 --- a/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java +++ b/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java @@ -402,10 +402,21 @@ public static Timestamp subtract(Timestamp ts, Duration dur) { /** Calculate the difference between two timestamps. */ public static Duration between(Timestamp from, Timestamp to) { + return between(from, to, /* validateOverflow= */ true); + } + + /** Calculate the difference between two timestamps. */ + public static Duration between(Timestamp from, Timestamp to, boolean validateOverflow) { Instant javaFrom = ProtoTimeUtils.toJavaInstant(checkValid(from)); Instant javaTo = ProtoTimeUtils.toJavaInstant(checkValid(to)); java.time.Duration between = java.time.Duration.between(javaFrom, javaTo); + if (validateOverflow) { + // Call toNanos() to validate 64-bit nanosecond overflow (throws ArithmeticException). + // Suppress unused variable warning as the duration object itself is returned. + @SuppressWarnings("unused") + long unused = between.toNanos(); + } return ProtoTimeUtils.toProtoDuration(between); } diff --git a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel index e9ed58642..e6f67fe66 100644 --- a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -104,16 +104,12 @@ _ALL_TESTS = [ ] _TESTS_TO_SKIP_LEGACY = [ + # Broken test cases which should be supported. # TODO: Support setting / getting enum values out of the defined enum value range. "enums/legacy_proto2/select_big,select_neg", "enums/legacy_proto2/assign_standalone_int_big,assign_standalone_int_neg", - # TODO: Generate errors on enum value assignment overflows for proto3. - "enums/legacy_proto3/assign_standalone_int_too_big,assign_standalone_int_too_neg", - # TODO: Ensure overflow occurs on conversions of double values which might not work properly on all platforms. - "conversions/int/double_int_min_range", - # TODO: Duration and timestamp operations should error on overflow. - "timestamps/timestamp_range/sub_time_duration_over,sub_time_duration_under", + # TODO: Ensure adding negative duration values is appropriately supported. "timestamps/timestamp_arithmetic/add_time_to_duration_nanos_negative", @@ -154,15 +150,6 @@ _TESTS_TO_SKIP_PLANNER = [ # TODO: Check behavior for go/cpp "basic/functions/unbound_is_runtime_error", - # TODO: Ensure overflow occurs on conversions of double values which might not work properly on all platforms. - "conversions/int/double_int_min_range", - "enums/legacy_proto3/assign_standalone_int_too_big", - "enums/legacy_proto3/assign_standalone_int_too_neg", - - # TODO: Duration and timestamp operations should error on overflow. - "timestamps/timestamp_range/sub_time_duration_over", - "timestamps/timestamp_range/sub_time_duration_under", - # Skip until fixed. "parse/receiver_function_names", diff --git a/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java b/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java index 0ee7824b7..fb512541f 100644 --- a/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java @@ -391,7 +391,7 @@ public static Optional doubleToUnsignedChecked(double v) { public static Optional doubleToLongChecked(double v) { // getExponent of NaN or Infinite values will return a Double.MAX_EXPONENT + 1 (or 128) int exp = Math.getExponent(v); - if (exp >= 63 && v != Math.scalb(-1.0, 63)) { + if (exp >= 63) { return Optional.empty(); } return Optional.of((long) v); diff --git a/runtime/src/main/java/dev/cel/runtime/standard/IntFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/IntFunction.java index 63959af87..77924d873 100644 --- a/runtime/src/main/java/dev/cel/runtime/standard/IntFunction.java +++ b/runtime/src/main/java/dev/cel/runtime/standard/IntFunction.java @@ -82,7 +82,7 @@ public enum IntOverload implements CelStandardOverload { return RuntimeHelpers.doubleToLongChecked(arg) .orElseThrow( () -> - new CelNumericOverflowException("double is out of range for int")); + new CelNumericOverflowException("double is out of range for int")); } return arg.longValue(); })), diff --git a/runtime/src/main/java/dev/cel/runtime/standard/SubtractOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/SubtractOperator.java index 784c46825..822c3066a 100644 --- a/runtime/src/main/java/dev/cel/runtime/standard/SubtractOperator.java +++ b/runtime/src/main/java/dev/cel/runtime/standard/SubtractOperator.java @@ -68,13 +68,33 @@ public enum SubtractOverload implements CelStandardOverload { "subtract_timestamp_timestamp", Instant.class, Instant.class, - (Instant i1, Instant i2) -> java.time.Duration.between(i2, i1)); + (Instant i1, Instant i2) -> { + java.time.Duration between = java.time.Duration.between(i2, i1); + if (celOptions.enableTimestampOverflowCheck()) { + try { + // Call toNanos() to validate 64-bit nanosecond overflow (throws + // ArithmeticException). + @SuppressWarnings("unused") + long unused = between.toNanos(); + } catch (ArithmeticException e) { + throw new CelNumericOverflowException(e); + } + } + return between; + }); } else { return CelFunctionBinding.from( "subtract_timestamp_timestamp", Timestamp.class, Timestamp.class, - (Timestamp t1, Timestamp t2) -> ProtoTimeUtils.between(t2, t1)); + (Timestamp t1, Timestamp t2) -> { + try { + return ProtoTimeUtils.between( + t2, t1, celOptions.enableTimestampOverflowCheck()); + } catch (ArithmeticException e) { + throw new CelNumericOverflowException(e); + } + }); } }), SUBTRACT_TIMESTAMP_DURATION( From 01ac8a546acc115198239cd5e5589a49cabdc3bc Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Wed, 29 Jul 2026 10:12:36 -0700 Subject: [PATCH 149/204] Fix conformance test case around receiver function names containing reserved keywords for parsed-only case PiperOrigin-RevId: 955950135 --- .../test/java/dev/cel/conformance/BUILD.bazel | 6 +-- .../cel/runtime/planner/ProgramPlanner.java | 38 +++++++++++++------ 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel index e6f67fe66..c5364b146 100644 --- a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -147,12 +147,10 @@ _TESTS_TO_SKIP_PLANNER = [ "string_ext/format", "string_ext/format_errors", - # TODO: Check behavior for go/cpp + # TODO: This is actually a user experience degradation. + # Not worth fixing until we see a concrete need. "basic/functions/unbound_is_runtime_error", - # Skip until fixed. - "parse/receiver_function_names", - # Type inference edgecases around null(able) assignability. # These type check, but resolve to a different type. # list(int), want list(wrapper(int)) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index 6bb3d1e22..77f605efc 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -14,6 +14,8 @@ package dev.cel.runtime.planner; +import static com.google.common.base.Preconditions.checkNotNull; + import com.google.auto.value.AutoValue; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; @@ -293,9 +295,13 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { } if (resolvedOverload == null) { - if (!lateBoundFunctionNames.contains(functionName)) { + boolean isLateBound = lateBoundFunctionNames.contains(functionName); + // For type-checked ASTs, functions that are not explicitly registered as late-bound + // must be resolved at plan time. + // For parsed-only ASTs or late-bound functions, defer overload resolution to runtime. + if (ctx.isChecked() && !isLateBound) { CelReference reference = ctx.referenceMap().get(expr.id()); - if (reference != null) { + if (reference != null && !reference.overloadIds().isEmpty()) { throw new CelOverloadNotFoundException(functionName, reference.overloadIds()); } else { throw new CelOverloadNotFoundException(functionName); @@ -303,7 +309,10 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { } ImmutableList overloadIds = ImmutableList.of(); - if (resolvedFunction.overloadId().isPresent()) { + CelReference reference = ctx.referenceMap().get(expr.id()); + if (reference != null && !reference.overloadIds().isEmpty()) { + overloadIds = reference.overloadIds(); + } else if (resolvedFunction.overloadId().isPresent()) { overloadIds = ImmutableList.of(resolvedFunction.overloadId().get()); } @@ -628,16 +637,23 @@ private static Builder newBuilder() { } static final class PlannerContext { - private final ImmutableMap referenceMap; - private final ImmutableMap typeMap; + private final CelAbstractSyntaxTree ast; private final HashMap localVars = new HashMap<>(); + CelAbstractSyntaxTree ast() { + return ast; + } + ImmutableMap referenceMap() { - return referenceMap; + return ast.getReferenceMap(); } ImmutableMap typeMap() { - return typeMap; + return ast.getTypeMap(); + } + + boolean isChecked() { + return ast.isChecked(); } private void pushLocalVars(String... names) { @@ -670,14 +686,12 @@ private boolean isLocalVar(String name) { return localVars.containsKey(name); } - private PlannerContext( - ImmutableMap referenceMap, ImmutableMap typeMap) { - this.referenceMap = referenceMap; - this.typeMap = typeMap; + private PlannerContext(CelAbstractSyntaxTree ast) { + this.ast = checkNotNull(ast); } static PlannerContext create(CelAbstractSyntaxTree ast) { - return new PlannerContext(ast.getReferenceMap(), ast.getTypeMap()); + return new PlannerContext(ast); } } From 310d96f5d57ed67676e6d36a6b8b63f360618b0b Mon Sep 17 00:00:00 2001 From: Jonathan Tatum Date: Wed, 29 Jul 2026 12:12:02 -0700 Subject: [PATCH 150/204] Avoid copying complex target in optMap/optFlatMap cross ref: https://github.com/cel-expr/cel-go/pull/1387 PiperOrigin-RevId: 956019109 --- .../cel/extensions/CelOptionalLibrary.java | 75 +++++++++++++++++-- .../test/java/dev/cel/extensions/BUILD.bazel | 1 + .../extensions/CelOptionalLibraryTest.java | 63 ++++++++++++++++ 3 files changed, 131 insertions(+), 8 deletions(-) diff --git a/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java b/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java index 8b67d5c79..85ee9c756 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java +++ b/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java @@ -297,6 +297,7 @@ static CelExtensionLibrary library() { public static final CelOptionalLibrary INSTANCE = CelOptionalLibrary.library().latest(); private static final String UNUSED_ITER_VAR = "#unused"; + private static final String OPTIONAL_MAP_VAR = "@target"; private final int version; private final ImmutableSet functions; @@ -524,21 +525,51 @@ private static Optional expandOptMap( CelExpr mapExpr = checkNotNull(arguments.get(1)); String varName = varIdent.ident().name(); - return Optional.of( + if (target.exprKind().getKind() == CelExpr.ExprKind.Kind.IDENT) { + return Optional.of( + exprFactory.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + exprFactory.newReceiverCall(HAS_VALUE.getFunction(), target), + exprFactory.newGlobalCall( + OPTIONAL_OF.getFunction(), + exprFactory.fold( + UNUSED_ITER_VAR, + exprFactory.newList(), + varName, + exprFactory.newReceiverCall(VALUE.getFunction(), exprFactory.copy(target)), + exprFactory.newBoolLiteral(true), + exprFactory.newIdentifier(varName), + mapExpr)), + exprFactory.newGlobalCall(OPTIONAL_NONE.getFunction()))); + } + + CelExpr localVar = exprFactory.newIdentifier(OPTIONAL_MAP_VAR); + CelExpr localVarCopy = exprFactory.copy(localVar); + CelExpr conditionalExpr = exprFactory.newGlobalCall( Operator.CONDITIONAL.getFunction(), - exprFactory.newReceiverCall(HAS_VALUE.getFunction(), target), + exprFactory.newReceiverCall(HAS_VALUE.getFunction(), localVar), exprFactory.newGlobalCall( OPTIONAL_OF.getFunction(), exprFactory.fold( UNUSED_ITER_VAR, exprFactory.newList(), varName, - exprFactory.newReceiverCall(VALUE.getFunction(), exprFactory.copy(target)), + exprFactory.newReceiverCall(VALUE.getFunction(), localVarCopy), exprFactory.newBoolLiteral(true), exprFactory.newIdentifier(varName), mapExpr)), - exprFactory.newGlobalCall(OPTIONAL_NONE.getFunction()))); + exprFactory.newGlobalCall(OPTIONAL_NONE.getFunction())); + + return Optional.of( + exprFactory.fold( + UNUSED_ITER_VAR, + exprFactory.newList(), + OPTIONAL_MAP_VAR, + target, + exprFactory.newBoolLiteral(false), + exprFactory.newIdentifier(OPTIONAL_MAP_VAR), + conditionalExpr)); } private static Optional expandOptFlatMap( @@ -558,19 +589,47 @@ private static Optional expandOptFlatMap( CelExpr mapExpr = checkNotNull(arguments.get(1)); String varName = varIdent.ident().name(); - return Optional.of( + if (target.exprKind().getKind() == CelExpr.ExprKind.Kind.IDENT) { + return Optional.of( + exprFactory.newGlobalCall( + Operator.CONDITIONAL.getFunction(), + exprFactory.newReceiverCall(HAS_VALUE.getFunction(), target), + exprFactory.fold( + UNUSED_ITER_VAR, + exprFactory.newList(), + varName, + exprFactory.newReceiverCall(VALUE.getFunction(), exprFactory.copy(target)), + exprFactory.newBoolLiteral(true), + exprFactory.newIdentifier(varName), + mapExpr), + exprFactory.newGlobalCall(OPTIONAL_NONE.getFunction()))); + } + + CelExpr localVar = exprFactory.newIdentifier(OPTIONAL_MAP_VAR); + CelExpr localVarCopy = exprFactory.copy(localVar); + CelExpr conditionalExpr = exprFactory.newGlobalCall( Operator.CONDITIONAL.getFunction(), - exprFactory.newReceiverCall(HAS_VALUE.getFunction(), target), + exprFactory.newReceiverCall(HAS_VALUE.getFunction(), localVar), exprFactory.fold( UNUSED_ITER_VAR, exprFactory.newList(), varName, - exprFactory.newReceiverCall(VALUE.getFunction(), exprFactory.copy(target)), + exprFactory.newReceiverCall(VALUE.getFunction(), localVarCopy), exprFactory.newBoolLiteral(true), exprFactory.newIdentifier(varName), mapExpr), - exprFactory.newGlobalCall(OPTIONAL_NONE.getFunction()))); + exprFactory.newGlobalCall(OPTIONAL_NONE.getFunction())); + + return Optional.of( + exprFactory.fold( + UNUSED_ITER_VAR, + exprFactory.newList(), + OPTIONAL_MAP_VAR, + target, + exprFactory.newBoolLiteral(false), + exprFactory.newIdentifier(OPTIONAL_MAP_VAR), + conditionalExpr)); } private static Object indexOptionalMap( diff --git a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel index 9fda186cf..920ba537b 100644 --- a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel @@ -17,6 +17,7 @@ java_library( "//common:compiler_common", "//common:container", "//common:options", + "//common/ast", "//common/exceptions:attribute_not_found", "//common/exceptions:divide_by_zero", "//common/exceptions:index_out_of_bounds", diff --git a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java index 2ba12910f..fab444750 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java @@ -34,6 +34,7 @@ import dev.cel.common.CelOverloadDecl; import dev.cel.common.CelValidationException; import dev.cel.common.CelVarDecl; +import dev.cel.common.ast.CelExpr; import dev.cel.common.types.CelType; import dev.cel.common.types.ListType; import dev.cel.common.types.MapType; @@ -1571,6 +1572,68 @@ public void optionalFlatMapMacro_receiverHasValue_returnsOptionalValue() throws assertThat(result).hasValue(43L); } + @Test + public void optionalMapMacro_simpleTarget_notWrappedInComprehension() throws Exception { + Cel cel = + newCelBuilder() + .addVar("x", OptionalType.create(SimpleType.INT)) + .setResultType(OptionalType.create(SimpleType.INT)) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "x.optMap(y, y + 1)"); + + assertThat(ast.getExpr().exprKind().getKind()).isEqualTo(CelExpr.ExprKind.Kind.CALL); + } + + @Test + public void optionalMapMacro_complexTarget_astWrappedInComprehension() throws Exception { + Cel cel = + newCelBuilder() + .setResultType(OptionalType.create(SimpleType.INT)) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "msg.?single_int32.optMap(y, y + 1)"); + + assertThat(ast.getExpr().exprKind().getKind()).isEqualTo(CelExpr.ExprKind.Kind.COMPREHENSION); + assertThat(ast.getExpr().comprehension().accuVar()).isEqualTo("@target"); + + Optional result = + (Optional) + cel.createProgram(ast) + .eval(ImmutableMap.of("msg", TestAllTypes.newBuilder().setSingleInt32(42).build())); + assertThat(result).hasValue(43L); + } + + @Test + public void optionalFlatMapMacro_simpleTarget_notWrappedInComprehension() throws Exception { + Cel cel = + newCelBuilder() + .addVar("x", OptionalType.create(SimpleType.INT)) + .setResultType(OptionalType.create(SimpleType.INT)) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "x.optFlatMap(y, optional.of(y + 1))"); + + assertThat(ast.getExpr().exprKind().getKind()).isEqualTo(CelExpr.ExprKind.Kind.CALL); + } + + @Test + public void optionalFlatMapMacro_complexTarget_astWrappedInComprehension() throws Exception { + Cel cel = + newCelBuilder() + .setResultType(OptionalType.create(SimpleType.INT)) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .build(); + CelAbstractSyntaxTree ast = compile(cel, "msg.?single_int32.optFlatMap(y, optional.of(y + 1))"); + + assertThat(ast.getExpr().exprKind().getKind()).isEqualTo(CelExpr.ExprKind.Kind.COMPREHENSION); + assertThat(ast.getExpr().comprehension().accuVar()).isEqualTo("@target"); + + Optional result = + (Optional) + cel.createProgram(ast) + .eval(ImmutableMap.of("msg", TestAllTypes.newBuilder().setSingleInt32(42).build())); + assertThat(result).hasValue(43L); + } + @Test public void optionalFlatMapMacro_withOptionalOfNonZeroValue_optionalEmptyWhenValueIsZero() throws Exception { From e947ca594afee51a980d80440048e94ac59330da Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Thu, 30 Jul 2026 16:46:30 -0700 Subject: [PATCH 151/204] Fix ConstantFoldingOptimizer to not treat true && dyn_x as a tautology true && bool_x continues to fold to bool_x PiperOrigin-RevId: 956806977 --- .../optimizers/ConstantFoldingOptimizer.java | 164 ++++++++++++++---- .../ConstantFoldingOptimizerTest.java | 47 +++-- 2 files changed, 158 insertions(+), 53 deletions(-) diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java index 0fcbb497c..b69f5ec52 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java @@ -21,6 +21,7 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CanIgnoreReturnValue; import dev.cel.bundle.Cel; @@ -62,9 +63,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import org.jspecify.annotations.Nullable; @@ -73,6 +77,19 @@ * calls and select statements with their evaluated result. */ public final class ConstantFoldingOptimizer implements CelAstOptimizer { + private static final ImmutableSet BOOLEAN_RETURN_OPERATORS = + ImmutableSet.of( + Operator.LOGICAL_AND.getFunction(), + Operator.LOGICAL_OR.getFunction(), + Operator.LOGICAL_NOT.getFunction(), + Operator.EQUALS.getFunction(), + Operator.NOT_EQUALS.getFunction(), + Operator.LESS.getFunction(), + Operator.LESS_EQUALS.getFunction(), + Operator.GREATER.getFunction(), + Operator.GREATER_EQUALS.getFunction(), + Operator.IN.getFunction()); + private static final ConstantFoldingOptimizer INSTANCE = new ConstantFoldingOptimizer(ConstantFoldingOptions.newBuilder().build()); @@ -115,6 +132,25 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) Cel optimizerEnv = builder.setResultType(SimpleType.DYN).build(); CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + + // HACK: The AstMutator strips type metadata during intermediate folds due to ID renumbering. + // We pre-compute identifier types from the unmutated AST to safely evaluate boolean conditions + // later. + // TODO: Improve AstMutator to retain type metadata when possible. + Map mutableIdentTypes = new HashMap<>(); + Iterator identNodes = + CelNavigableMutableAst.fromAst(mutableAst) + .getRoot() + .allNodes() + .filter(node -> node.getKind().equals(Kind.IDENT)) + .iterator(); + while (identNodes.hasNext()) { + CelNavigableMutableExpr node = identNodes.next(); + Optional type = mutableAst.getType(node.id()); + type.ifPresent(celType -> mutableIdentTypes.put(node.expr().ident().name(), celType)); + } + ImmutableMap identTypes = ImmutableMap.copyOf(mutableIdentTypes); + int iterCount = 0; boolean continueFolding = true; while (continueFolding) { @@ -123,7 +159,6 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) } iterCount++; continueFolding = false; - ImmutableList foldableExprs = CelNavigableMutableAst.fromAst(mutableAst) .getRoot() @@ -135,7 +170,7 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) Optional mutatedResult; // Attempt to prune if it is a non-strict call - mutatedResult = maybePruneBranches(mutableAst, foldableExpr.expr()); + mutatedResult = maybePruneBranches(mutableAst, identTypes, foldableExpr.expr()); if (!mutatedResult.isPresent()) { // Evaluate the call then fold try { @@ -210,6 +245,9 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) { if (functionName.equals(Operator.EQUALS.getFunction()) || functionName.equals(Operator.NOT_EQUALS.getFunction())) { + if (hasComprehensionVar(navigableExpr)) { + return false; + } if (mutableCall.args().stream() .anyMatch(node -> isExprConstantOfKind(node, CelConstant.Kind.BOOLEAN_VALUE)) || mutableCall.args().stream() @@ -219,7 +257,7 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) { } if (functionName.equals(Operator.IN.getFunction())) { - return canFoldInOperator(navigableExpr); + return !hasComprehensionVar(navigableExpr); } // Default case: all call arguments must be constants. If the argument is a container (ex: @@ -248,32 +286,31 @@ private static boolean isCallTimestampOrDuration(CelMutableCall call) { || call.function().equals(DURATION.functionName()); } - private static boolean canFoldInOperator(CelNavigableMutableExpr navigableExpr) { - ImmutableList allIdents = - navigableExpr - .allNodes() - .filter(node -> node.getKind().equals(Kind.IDENT)) - .collect(toImmutableList()); - for (CelNavigableMutableExpr identNode : allIdents) { - CelNavigableMutableExpr parent = identNode.parent().orElse(null); - while (parent != null) { - if (parent.getKind().equals(Kind.COMPREHENSION)) { - String identName = identNode.expr().ident().name(); - CelMutableComprehension parentComprehension = parent.expr().comprehension(); - if (parentComprehension.accuVar().equals(identName) - || parentComprehension.iterVar().equals(identName) - || parentComprehension.iterVar2().equals(identName)) { - // Prevent folding a subexpression if it contains a variable declared by a - // comprehension. The subexpression cannot be compiled without the full context of the - // surrounding comprehension. - return false; - } - } - parent = parent.parent().orElse(null); - } - } - - return true; + private static boolean hasComprehensionVar(CelNavigableMutableExpr expr) { + return expr.allNodes() + .filter(node -> node.getKind().equals(Kind.IDENT)) + .anyMatch( + identNode -> { + String identName = identNode.expr().ident().name(); + CelNavigableMutableExpr curr = identNode; + Optional maybeParent = curr.parent(); + while (maybeParent.isPresent()) { + CelNavigableMutableExpr parent = maybeParent.get(); + if (parent.getKind().equals(Kind.COMPREHENSION)) { + CelMutableComprehension compre = parent.expr().comprehension(); + if ((compre.accuVar().equals(identName) + || compre.iterVar().equals(identName) + || compre.iterVar2().equals(identName)) + && curr.id() != compre.iterRange().id() + && curr.id() != compre.accuInit().id()) { + return true; + } + } + curr = parent; + maybeParent = parent.parent(); + } + return false; + }); } private static boolean areChildrenArgConstant(CelNavigableMutableExpr expr) { @@ -311,6 +348,9 @@ private Optional maybeFold( CelMutableAst mutableAst, CelNavigableMutableExpr node) throws CelOptimizationException, CelEvaluationException { + if (!node.getKind().equals(Kind.COMPREHENSION) && hasComprehensionVar(node)) { + return Optional.empty(); + } Object result; try { result = evaluateExpr(cel, node); @@ -465,7 +505,7 @@ private static boolean isCallToFunction(CelMutableExpr expr, String functionName /** Inspects the non-strict calls to determine whether a branch can be removed. */ private Optional maybePruneBranches( - CelMutableAst mutableAst, CelMutableExpr expr) { + CelMutableAst mutableAst, Map identTypes, CelMutableExpr expr) { if (!expr.getKind().equals(Kind.CALL)) { return Optional.empty(); } @@ -474,7 +514,7 @@ private Optional maybePruneBranches( String function = call.function(); if (function.equals(Operator.LOGICAL_AND.getFunction()) || function.equals(Operator.LOGICAL_OR.getFunction())) { - return maybeShortCircuitCall(mutableAst, expr); + return maybeShortCircuitCall(mutableAst, identTypes, expr); } else if (function.equals(Operator.CONDITIONAL.getFunction())) { CelMutableExpr cond = call.args().get(0); CelMutableExpr truthy = call.args().get(1); @@ -518,8 +558,8 @@ private Optional maybePruneBranches( || function.equals(Operator.NOT_EQUALS.getFunction())) { CelMutableExpr lhs = call.args().get(0); CelMutableExpr rhs = call.args().get(1); - boolean lhsIsBoolean = isExprConstantOfKind(lhs, CelConstant.Kind.BOOLEAN_VALUE); - boolean rhsIsBoolean = isExprConstantOfKind(rhs, CelConstant.Kind.BOOLEAN_VALUE); + boolean lhsIsBooleanConstant = isExprConstantOfKind(lhs, CelConstant.Kind.BOOLEAN_VALUE); + boolean rhsIsBooleanConstant = isExprConstantOfKind(rhs, CelConstant.Kind.BOOLEAN_VALUE); boolean invertCondition = function.equals(Operator.NOT_EQUALS.getFunction()); Optional replacementExpr = Optional.empty(); @@ -527,7 +567,9 @@ private Optional maybePruneBranches( // If both args are const, don't prune any branches and let maybeFold method evaluate this // subExpr return Optional.empty(); - } else if (lhsIsBoolean) { + } else if (lhsIsBooleanConstant + && (!constantFoldingOptions.enableSafeLogicalOptimization() + || evaluatesToBoolean(mutableAst, identTypes, rhs))) { boolean cond = invertCondition != lhs.constant().booleanValue(); replacementExpr = Optional.of( @@ -535,7 +577,9 @@ private Optional maybePruneBranches( ? rhs : CelMutableExpr.ofCall( CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), rhs))); - } else if (rhsIsBoolean) { + } else if (rhsIsBooleanConstant + && (!constantFoldingOptions.enableSafeLogicalOptimization() + || evaluatesToBoolean(mutableAst, identTypes, lhs))) { boolean cond = invertCondition != rhs.constant().booleanValue(); replacementExpr = Optional.of( @@ -552,7 +596,7 @@ private Optional maybePruneBranches( } private Optional maybeShortCircuitCall( - CelMutableAst mutableAst, CelMutableExpr expr) { + CelMutableAst mutableAst, Map identTypes, CelMutableExpr expr) { CelMutableCall call = expr.call(); boolean shortCircuit = false; boolean skip = true; @@ -583,7 +627,12 @@ private Optional maybeShortCircuitCall( return Optional.of(astMutator.replaceSubtree(mutableAst, shortCircuitTarget, expr.id())); } if (newArgs.size() == 1) { - return Optional.of(astMutator.replaceSubtree(mutableAst, newArgs.get(0), expr.id())); + CelMutableExpr remainingArg = newArgs.get(0); + if (!constantFoldingOptions.enableSafeLogicalOptimization() + || evaluatesToBoolean(mutableAst, identTypes, remainingArg)) { + return Optional.of(astMutator.replaceSubtree(mutableAst, remainingArg, expr.id())); + } + return Optional.empty(); } // TODO: Support folding variadic AND/ORs. @@ -591,6 +640,26 @@ private Optional maybeShortCircuitCall( "Folding variadic logical operator is not supported yet."); } + private boolean evaluatesToBoolean( + CelMutableAst mutableAst, Map identTypes, CelMutableExpr expr) { + if (isExprConstantOfKind(expr, CelConstant.Kind.BOOLEAN_VALUE)) { + return true; + } + // The AST's type map relies on the type-checker having explicitly populated the type for a + // given node. However, during the optimization pipeline, mutated intermediate nodes might + // temporarily lack type metadata. Standard CEL operators like &&, ||, and == inherently + // always return a boolean, so checking the function name provides a reliable fallback when + // the type map is incomplete. + if (expr.getKind().equals(Kind.CALL) + && BOOLEAN_RETURN_OPERATORS.contains(expr.call().function())) { + return true; + } + if (expr.getKind().equals(Kind.IDENT)) { + return Objects.equals(identTypes.get(expr.ident().name()), SimpleType.BOOL); + } + return mutableAst.getType(expr.id()).map(SimpleType.BOOL::equals).orElse(false); + } + private boolean isFoldedAggregateLiteral(CelMutableExpr expr) { if (expr.getKind().equals(Kind.CONSTANT)) { return true; @@ -811,6 +880,13 @@ public abstract static class ConstantFoldingOptions { public abstract ImmutableSet foldableFunctions(); + /** + * Returns true if safe logical optimization is enabled. When enabled, logical (&&, ||) and + * equality (==, !=) expression optimizations strictly verify that pruned sub-expressions + * evaluate to booleans. + */ + public abstract boolean enableSafeLogicalOptimization(); + /** Builder for configuring the {@link ConstantFoldingOptions}. */ @AutoValue.Builder public abstract static class Builder { @@ -823,6 +899,17 @@ public abstract static class Builder { */ public abstract Builder maxIterationLimit(int value); + /** + * Enables or disables safe logical optimization. When enabled (default: {@code true}), + * constant folding on logical (&&, ||) and equality (==, !=) operators strictly checks + * whether sub-expressions evaluate to booleans before pruning them. Disabling this flag + * restores legacy aggressive folding behavior. + * + *

Note: Disabling this flag should only be done temporarily for migration purposes, with + * the goal of eventually enabling it for safety. + */ + public abstract Builder enableSafeLogicalOptimization(boolean value); + /** * Adds a collection of custom functions that will be a candidate for constant folding. By * default, standard functions are foldable. @@ -850,7 +937,8 @@ public Builder addFoldableFunctions(String... functions) { /** Returns a new options builder with recommended defaults pre-configured. */ public static Builder newBuilder() { return new AutoValue_ConstantFoldingOptimizer_ConstantFoldingOptions.Builder() - .maxIterationLimit(400); + .maxIterationLimit(400) + .enableSafeLogicalOptimization(true); } ConstantFoldingOptions() {} diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java index 3cb388408..3b503bc28 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java @@ -80,6 +80,7 @@ private static Cel setupEnv(CelBuilder celBuilder) { return celBuilder .addVar("x", SimpleType.DYN) .addVar("y", SimpleType.DYN) + .addVar("bool_var", SimpleType.BOOL) .addVar("list_var", ListType.create(SimpleType.STRING)) .addVar("map_var", MapType.create(SimpleType.STRING, SimpleType.STRING)) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) @@ -127,17 +128,16 @@ private static Cel setupEnv(CelBuilder celBuilder) { @TestParameters("{source: 'false || false', expected: 'false'}") @TestParameters("{source: 'true && false || true', expected: 'true'}") @TestParameters("{source: 'false && true || false', expected: 'false'}") - @TestParameters("{source: 'true && x', expected: 'x'}") - @TestParameters("{source: 'x && true', expected: 'x'}") + @TestParameters("{source: 'true && bool_var', expected: 'bool_var'}") + @TestParameters("{source: 'bool_var && false', expected: 'false'}") + @TestParameters("{source: 'bool_var && true', expected: 'bool_var'}") + @TestParameters("{source: 'false || [1 + 2, x][0]', expected: 'false || [3, x][0]'}") @TestParameters("{source: 'false && x', expected: 'false'}") @TestParameters("{source: 'x && false', expected: 'false'}") @TestParameters("{source: 'true || x', expected: 'true'}") @TestParameters("{source: 'x || true', expected: 'true'}") - @TestParameters("{source: 'false || x', expected: 'x'}") - @TestParameters("{source: 'x || false', expected: 'x'}") - @TestParameters("{source: 'true && x && true && x', expected: 'x && x'}") - @TestParameters("{source: 'false || x || false || x', expected: 'x || x'}") - @TestParameters("{source: 'false || x || false || y', expected: 'x || y'}") + @TestParameters("{source: 'false || bool_var', expected: 'bool_var'}") + @TestParameters("{source: 'bool_var || false', expected: 'bool_var'}") @TestParameters("{source: 'true ? x + 1 : x + 2', expected: 'x + 1'}") @TestParameters("{source: 'false ? x + 1 : x + 2', expected: 'x + 2'}") @TestParameters( @@ -230,10 +230,10 @@ private static Cel setupEnv(CelBuilder celBuilder) { @TestParameters("{source: 'sets.contains([1], [1])', expected: 'true'}") @TestParameters( "{source: 'cel.bind(r0, [1, 2, 3], cel.bind(r1, 1 in r0, r1))', expected: 'true'}") - @TestParameters("{source: 'x == true', expected: 'x'}") - @TestParameters("{source: 'true == x', expected: 'x'}") - @TestParameters("{source: 'x == false', expected: '!x'}") - @TestParameters("{source: 'false == x', expected: '!x'}") + @TestParameters("{source: 'bool_var == true', expected: 'bool_var'}") + @TestParameters("{source: 'true == bool_var', expected: 'bool_var'}") + @TestParameters("{source: 'bool_var == false', expected: '!bool_var'}") + @TestParameters("{source: 'false == bool_var', expected: '!bool_var'}") @TestParameters("{source: 'true == false', expected: 'false'}") @TestParameters("{source: 'true == true', expected: 'true'}") @TestParameters("{source: 'false == true', expected: 'false'}") @@ -257,10 +257,10 @@ private static Cel setupEnv(CelBuilder celBuilder) { @TestParameters("{source: 'false == false', expected: 'true'}") @TestParameters("{source: '10 == 42', expected: 'false'}") @TestParameters("{source: '42 == 42', expected: 'true'}") - @TestParameters("{source: 'x != true', expected: '!x'}") - @TestParameters("{source: 'true != x', expected: '!x'}") - @TestParameters("{source: 'x != false', expected: 'x'}") - @TestParameters("{source: 'false != x', expected: 'x'}") + @TestParameters("{source: 'bool_var != true', expected: '!bool_var'}") + @TestParameters("{source: 'true != bool_var', expected: '!bool_var'}") + @TestParameters("{source: 'bool_var != false', expected: 'bool_var'}") + @TestParameters("{source: 'false != bool_var', expected: 'bool_var'}") @TestParameters("{source: 'true != false', expected: 'true'}") @TestParameters("{source: 'true != true', expected: 'false'}") @TestParameters("{source: 'false != true', expected: 'true'}") @@ -395,6 +395,7 @@ public void constantFold_protoMessageLiteral_success(String source, String expec @TestParameters( "{source: 'cel.bind(myMap, {\"foo\": \"bar\"}, myMap[?\"foo\"].optMap(x, x + \"baz\"))', " + "expected: 'optional.of(\"barbaz\")'}") + @TestParameters("{source: '(1 + 2 + 3 == x) && (x in [1, 2, x])', expected: '6 == x'}") public void constantFold_macros_macroCallMetadataPopulated(String source, String expected) throws Exception { Cel cel = @@ -498,6 +499,22 @@ public void constantFold_macros_withoutMacroCallMetadata(String source) throws E @TestParameters("{source: '[true].exists(x, x == get_true())'}") @TestParameters("{source: 'get_list([1, 2]).map(x, x * 2)'}") @TestParameters("{source: '[(x - 1 > 3) ? (x - 1) : 5].exists(x, x - 1 > 3)'}") + @TestParameters("{source: 'true && x'}") + @TestParameters("{source: 'x && true'}") + @TestParameters("{source: 'false || x'}") + @TestParameters("{source: 'x || false'}") + @TestParameters("{source: 'true && x && true && x'}") + @TestParameters("{source: 'false || x || false || x'}") + @TestParameters("{source: 'false || x || false || y'}") + @TestParameters("{source: 'x == true'}") + @TestParameters("{source: 'true == x'}") + @TestParameters("{source: 'x == false'}") + @TestParameters("{source: 'false == x'}") + @TestParameters("{source: 'x != true'}") + @TestParameters("{source: 'true != x'}") + @TestParameters("{source: 'x != false'}") + @TestParameters("{source: 'false != x'}") + @TestParameters("{source: '[x].exists(item, item == true)'}") public void constantFold_noOp(String source) throws Exception { CelAbstractSyntaxTree ast = cel.compile(source).getAst(); From 8d150b2e9b6aa7af3b5f586059379bc60d4525d5 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Fri, 31 Jul 2026 14:01:18 -0700 Subject: [PATCH 152/204] Add aggregate semantics to Policy Compiler Aggregate walks through all matching rules (including nested ones) and appends them into a list: ```yaml rule: aggregate: - condition: "true" emit: "'FOO'" - condition: "true" emit: "'BAR'" # Output: ['FOO', 'BAR'] ``` Few noteworthy design decisions below. All examples assume all conditions matched: 1. For usability reasons, subrules under an aggregate ancestor will always have their **lists flattened**: ```YAML name: aggregate_flat_flattening_example rule: aggregate: - rule: match: - condition: "resource.is_admin == true" output: "['GDPR_STANDARD', 'EU_B2C_NOTICE']" - condition: "true" emit: "'FALLBACK'" # Output: ['GDPR_STANDARD', 'EU_B2C_NOTICE', 'FALLBACK'] ``` 2. Base case of an aggregate rule is an empty list. Nested conditional rules within an aggregate rule which outputs `optional.none()` are pruned (except in cases where policy output explicitly emits an `optional.none()`): ```YAML name: optional_pruning_example rule: aggregate: - rule: match: - condition: "1 == 2" output: "'EU_NOTICE'" - condition: "true" emit: "'ALWAYS'" # Output: ['ALWAYS'] ``` ```YAML name: explicit_optional_none_example rule: aggregate: - rule: match: - condition: "resource.is_b2c == true" output: "optional.none()" # Explicitly authored by user - condition: "true" emit: "optional.of('ALWAYS')" # Output: [optional.none(), optional.of('ALWAYS')] ``` Note: nesting `aggregate` clauses is currently not allowed, and will result in a compilation error. PiperOrigin-RevId: 957321565 --- .../java/dev/cel/optimizer/AstMutator.java | 5 +- .../src/main/java/dev/cel/policy/BUILD.bazel | 4 + .../java/dev/cel/policy/CelCompiledRule.java | 15 +- .../main/java/dev/cel/policy/CelPolicy.java | 13 +- .../dev/cel/policy/CelPolicyCompilerImpl.java | 41 +++- .../dev/cel/policy/CelPolicyYamlParser.java | 50 ++++- .../java/dev/cel/policy/RuleComposer.java | 175 +++++++++++++----- .../cel/policy/CelPolicyCompilerImplTest.java | 173 ++++++++++++++++- .../cel/policy/CelPolicyYamlParserTest.java | 30 +++ .../aggregate_errors/expected_errors.baseline | 6 + .../expected_errors.baseline | 6 + .../expected_errors.baseline | 3 + .../expected_errors.baseline | 5 +- .../unreachable/expected_errors.baseline | 3 + 14 files changed, 466 insertions(+), 63 deletions(-) create mode 100644 testing/src/test/resources/policy/aggregate_errors/expected_errors.baseline create mode 100644 testing/src/test/resources/policy/aggregate_list_errors/expected_errors.baseline create mode 100644 testing/src/test/resources/policy/aggregate_nested_mixed_semantics/expected_errors.baseline diff --git a/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java b/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java index 59f842e29..0f428f75a 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java +++ b/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java @@ -664,8 +664,8 @@ private CelMutableSource mangleIdentsInMacroSource( return newSource; } - private static CelMutableSource combine( - CelMutableSource celSource1, CelMutableSource celSource2) { + /** Combines two {@link CelMutableSource} instances into a single new instance. */ + public static CelMutableSource combine(CelMutableSource celSource1, CelMutableSource celSource2) { return CelMutableSource.newInstance() .setDescription( Strings.isNullOrEmpty(celSource1.getDescription()) @@ -677,6 +677,7 @@ private static CelMutableSource combine( .addAllMacroCalls(celSource2.getMacroCalls()); } + /** * Stabilizes the incoming AST by ensuring that all of expr IDs are consistently renumbered * (monotonically increased) from the starting seed ID. If the AST contains any macro calls, its diff --git a/policy/src/main/java/dev/cel/policy/BUILD.bazel b/policy/src/main/java/dev/cel/policy/BUILD.bazel index e0d6af461..79c7a1f05 100644 --- a/policy/src/main/java/dev/cel/policy/BUILD.bazel +++ b/policy/src/main/java/dev/cel/policy/BUILD.bazel @@ -179,6 +179,7 @@ java_library( name = "compiled_rule", srcs = ["CelCompiledRule.java"], deps = [ + ":policy", "//:auto_value", "//bundle:cel", "//common:cel_ast", @@ -246,6 +247,7 @@ java_library( srcs = ["RuleComposer.java"], deps = [ ":compiled_rule", + ":policy", "//bundle:cel", "//common:cel_ast", "//common:compiler_common", @@ -256,11 +258,13 @@ java_library( "//common/ast:mutable_expr", "//common/formats:value_string", "//common/navigation:mutable_navigation", + "//common/types", "//common/types:cel_types", "//common/types:type_providers", "//extensions:optional_library", "//optimizer:ast_optimizer", "//optimizer:mutable_ast", "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", ], ) diff --git a/policy/src/main/java/dev/cel/policy/CelCompiledRule.java b/policy/src/main/java/dev/cel/policy/CelCompiledRule.java index af40bd74f..ccacfd628 100644 --- a/policy/src/main/java/dev/cel/policy/CelCompiledRule.java +++ b/policy/src/main/java/dev/cel/policy/CelCompiledRule.java @@ -23,6 +23,7 @@ import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; import dev.cel.common.formats.ValueString; +import dev.cel.policy.CelPolicy.EvaluationSemantic; import java.util.Optional; /** @@ -43,11 +44,20 @@ public abstract class CelCompiledRule { public abstract Cel cel(); + public abstract EvaluationSemantic semantic(); + /** * HasOptionalOutput returns whether the rule returns a concrete or optional value. The rule may * return an optional value if all match expressions under the rule are conditional. */ public boolean hasOptionalOutput() { + // AGGREGATE rules always return a concrete list (falling back to an empty list rather than + // optional.none()), meaning they are never optional structurally. This also prevents dead + // code evasion inside parent FIRST_MATCH rules. + if (semantic() == EvaluationSemantic.AGGREGATE) { + return false; + } + boolean isOptionalOutput = false; for (CelCompiledMatch match : matches()) { if (match.result().kind().equals(CelCompiledMatch.Result.Kind.RULE) @@ -157,7 +167,8 @@ static CelCompiledRule create( Optional ruleId, ImmutableList variables, ImmutableList matches, - Cel cel) { - return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel); + Cel cel, + CelPolicy.EvaluationSemantic semantic) { + return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel, semantic); } } diff --git a/policy/src/main/java/dev/cel/policy/CelPolicy.java b/policy/src/main/java/dev/cel/policy/CelPolicy.java index 6756481df..2df6f4e5f 100644 --- a/policy/src/main/java/dev/cel/policy/CelPolicy.java +++ b/policy/src/main/java/dev/cel/policy/CelPolicy.java @@ -40,6 +40,12 @@ @AutoValue public abstract class CelPolicy { + /** Evaluation semantic for a rule. */ + public enum EvaluationSemantic { + FIRST_MATCH, + AGGREGATE + } + public abstract ValueString name(); public abstract Optional description(); @@ -176,12 +182,15 @@ public abstract static class Rule { public abstract ImmutableSet matches(); + public abstract EvaluationSemantic semantic(); + /** Builder for {@link Rule}. */ public static Builder newBuilder(long id) { return new AutoValue_CelPolicy_Rule.Builder() .setId(id) .setVariables(ImmutableSet.of()) - .setMatches(ImmutableSet.of()); + .setMatches(ImmutableSet.of()) + .setSemantic(EvaluationSemantic.FIRST_MATCH); } /** Creates a new builder to construct a {@link Rule} instance. */ @@ -228,6 +237,8 @@ public Builder addMatches(Iterable matches) { abstract Rule.Builder setMatches(ImmutableSet matches); + public abstract Rule.Builder setSemantic(EvaluationSemantic semantic); + public abstract Rule build(); } } diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java index 37a79f98a..d57ad3260 100644 --- a/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java +++ b/policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java @@ -44,6 +44,7 @@ import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result; import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result.Kind; import dev.cel.policy.CelCompiledRule.CelCompiledVariable; +import dev.cel.policy.CelPolicy.EvaluationSemantic; import dev.cel.policy.CelPolicy.Import; import dev.cel.policy.CelPolicy.Match; import dev.cel.policy.CelPolicy.Variable; @@ -91,7 +92,8 @@ public CelCompiledRule compileRule(CelPolicy policy) throws CelPolicyValidationE extendedCel = extendedCel.toCelBuilder().setContainer(containerBuilder.build()).build(); } - CelCompiledRule compiledRule = compileRuleImpl(policy.rule(), extendedCel, compilerContext); + CelCompiledRule compiledRule = + compileRuleImpl(policy.rule(), extendedCel, compilerContext, false); if (compilerContext.hasError()) { throw new CelPolicyValidationException(compilerContext.getIssueString()); } @@ -172,7 +174,14 @@ private void assertAstDepthIsSafe(CelAbstractSyntaxTree ast, Cel cel) } private CelCompiledRule compileRuleImpl( - CelPolicy.Rule rule, Cel ruleCel, CompilerContext compilerContext) { + CelPolicy.Rule rule, + Cel ruleCel, + CompilerContext compilerContext, + boolean hasAggregateAncestor) { + if (hasAggregateAncestor && rule.semantic().equals(EvaluationSemantic.AGGREGATE)) { + compilerContext.addIssue( + rule.id(), CelIssue.formatError(1, 0, "nested aggregate rules are not allowed")); + } // A local CEL environment used to compile a single rule. This temporary environment // is used to declare policy variables iteratively in a given policy, ensuring proper scoping // across a single / nested rule. @@ -227,8 +236,11 @@ private CelCompiledRule compileRuleImpl( matchResult = Result.ofOutput(output.id(), outputAst); break; case RULE: + boolean nextHasAggregateAncestor = + hasAggregateAncestor || rule.semantic().equals(EvaluationSemantic.AGGREGATE); CelCompiledRule nestedRule = - compileRuleImpl(match.result().rule(), localCel, compilerContext); + compileRuleImpl( + match.result().rule(), localCel, compilerContext, nextHasAggregateAncestor); matchResult = Result.ofRule(nestedRule); break; default: @@ -240,7 +252,12 @@ private CelCompiledRule compileRuleImpl( CelCompiledRule compiledRule = CelCompiledRule.create( - rule.id(), rule.ruleId(), variableBuilder.build(), matchBuilder.build(), ruleCel); + rule.id(), + rule.ruleId(), + variableBuilder.build(), + matchBuilder.build(), + ruleCel, + rule.semantic()); // Validate that all branches in the policy are reachable checkUnreachableCode(compiledRule, compilerContext); @@ -255,6 +272,12 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext CelCompiledMatch compiledMatch = compiledMatches.get(i); boolean isTriviallyTrue = compiledMatch.isConditionTriviallyTrue(); + // Flag literally false conditions as dead code regardless of semantic + if (isConditionLiterallyFalse(compiledMatch.condition())) { + compilerContext.addIssue( + compiledMatch.sourceId(), CelIssue.formatError(1, 0, "Condition is always false")); + } + // If the match is a single output or a nested rule that always returns a value, it is // exhaustive. If the condition is trivially true, then all subsequent branches are // unreachable. @@ -263,7 +286,9 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext && (compiledMatch.result().kind().equals(Kind.OUTPUT) || !compiledMatch.result().rule().hasOptionalOutput()); - if (isExhaustive && i != matchCount - 1) { + if (compiledRule.semantic() == EvaluationSemantic.FIRST_MATCH + && isExhaustive + && i != matchCount - 1) { if (compiledMatch.result().kind().equals(Kind.OUTPUT)) { compilerContext.addIssue( compiledMatch.sourceId(), @@ -277,6 +302,12 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext } } + private static boolean isConditionLiterallyFalse(CelAbstractSyntaxTree condition) { + CelExpr celExpr = condition.getExpr(); + return celExpr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE) + && !celExpr.constant().booleanValue(); + } + private static CelAbstractSyntaxTree newErrorAst() { return CelAbstractSyntaxTree.newParsedAst( CelExpr.ofConstant(0, CelConstant.ofValue("*error*")), CelSource.newBuilder().build()); diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java b/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java index 408c86247..09702e77c 100644 --- a/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java +++ b/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java @@ -28,6 +28,7 @@ import dev.cel.common.formats.YamlHelper.YamlNodeType; import dev.cel.common.formats.YamlParserContextImpl; import dev.cel.common.internal.CelCodePointArray; +import dev.cel.policy.CelPolicy.EvaluationSemantic; import dev.cel.policy.CelPolicy.Import; import dev.cel.policy.CelPolicy.Invariant; import dev.cel.policy.CelPolicy.Match; @@ -271,6 +272,8 @@ public CelPolicy.Rule parseRule( return ruleBuilder.build(); } + boolean hasMatch = false; + boolean hasAggregate = false; for (NodeTuple nodeTuple : ((MappingNode) node).getValue()) { Node key = nodeTuple.getKeyNode(); long tagId = ctx.collectMetadata(key); @@ -290,8 +293,24 @@ public CelPolicy.Rule parseRule( ruleBuilder.addVariables(parseVariables(ctx, policyBuilder, value)); break; case "match": - ruleBuilder.addMatches(parseMatches(ctx, policyBuilder, value)); + if (hasAggregate) { + ctx.reportError(tagId, "Only one of 'match' or 'aggregate' may be set in a rule"); + } + hasMatch = true; + ruleBuilder + .addMatches(parseMatches(ctx, policyBuilder, value, false)) + .setSemantic(EvaluationSemantic.FIRST_MATCH); + break; + case "aggregate": + if (hasMatch) { + ctx.reportError(tagId, "Only one of 'match' or 'aggregate' may be set in a rule"); + } + hasAggregate = true; + ruleBuilder + .addMatches(parseMatches(ctx, policyBuilder, value, true)) + .setSemantic(EvaluationSemantic.AGGREGATE); break; + default: tagVisitor.visitRuleTag(ctx, tagId, fieldName, value, policyBuilder, ruleBuilder); break; @@ -301,7 +320,10 @@ public CelPolicy.Rule parseRule( } private ImmutableSet parseMatches( - PolicyParserContext ctx, CelPolicy.Builder policyBuilder, Node node) { + PolicyParserContext ctx, + CelPolicy.Builder policyBuilder, + Node node, + boolean isAggregate) { long valueId = ctx.collectMetadata(node); ImmutableSet.Builder matchesBuilder = ImmutableSet.builder(); if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) { @@ -310,7 +332,7 @@ private ImmutableSet parseMatches( SequenceNode matchListNode = (SequenceNode) node; for (Node elementNode : matchListNode.getValue()) { - matchesBuilder.add(parseMatch(ctx, policyBuilder, elementNode)); + matchesBuilder.add(parseMatchInternal(ctx, policyBuilder, elementNode, isAggregate)); } return matchesBuilder.build(); @@ -319,6 +341,14 @@ private ImmutableSet parseMatches( @Override public CelPolicy.Match parseMatch( PolicyParserContext ctx, CelPolicy.Builder policyBuilder, Node node) { + return parseMatchInternal(ctx, policyBuilder, node, false); + } + + private CelPolicy.Match parseMatchInternal( + PolicyParserContext ctx, + CelPolicy.Builder policyBuilder, + Node node, + boolean isAggregate) { long nodeId = ctx.collectMetadata(node); if (!assertYamlType(ctx, nodeId, node, YamlNodeType.MAP)) { return ERROR_MATCH; @@ -339,6 +369,20 @@ public CelPolicy.Match parseMatch( matchBuilder.setCondition(ctx.newSourceString(value)); break; case "output": + if (isAggregate) { + ctx.reportError(tagId, "Rule aggregate requires 'emit' tag instead of 'output'"); + } + matchBuilder + .result() + .filter(result -> result.kind().equals(Match.Result.Kind.RULE)) + .ifPresent( + result -> ctx.reportError(tagId, "Only the rule or the output may be set")); + matchBuilder.setResult(Match.Result.ofOutput(ctx.newSourceString(value))); + break; + case "emit": + if (!isAggregate) { + ctx.reportError(tagId, "Rule match requires 'output' tag instead of 'emit'"); + } matchBuilder .result() .filter(result -> result.kind().equals(Match.Result.Kind.RULE)) diff --git a/policy/src/main/java/dev/cel/policy/RuleComposer.java b/policy/src/main/java/dev/cel/policy/RuleComposer.java index 73d31a4ee..bf667cb93 100644 --- a/policy/src/main/java/dev/cel/policy/RuleComposer.java +++ b/policy/src/main/java/dev/cel/policy/RuleComposer.java @@ -30,11 +30,14 @@ import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr.ExprKind.Kind; import dev.cel.common.ast.CelMutableExpr; +import dev.cel.common.ast.CelMutableExpr.CelMutableList; import dev.cel.common.formats.ValueString; import dev.cel.common.navigation.CelNavigableMutableAst; import dev.cel.common.navigation.CelNavigableMutableExpr; import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypes; +import dev.cel.common.types.ListType; +import dev.cel.common.types.OptionalType; import dev.cel.extensions.CelOptionalLibrary.Function; import dev.cel.optimizer.AstMutator; import dev.cel.optimizer.CelAstOptimizer; @@ -45,6 +48,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; /** Package-private class for composing various rules into a single expression using optimizer. */ final class RuleComposer implements CelAstOptimizer { @@ -54,11 +58,11 @@ final class RuleComposer implements CelAstOptimizer { @Override public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) { - Step result = optimizeRule(cel, compiledRule); + Step result = optimizeRule(cel, compiledRule, /* asList= */ false); return OptimizationResult.create(result.expr.toParsedAst()); } - private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) { + private Step optimizeRule(Cel cel, CelCompiledRule compiledRule, boolean asList) { cel = cel.toCelBuilder() .addVarDeclarations( @@ -67,15 +71,10 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) { .collect(toImmutableList())) .build(); - Step output = null; - // If the rule has an optional output, the last result in the ternary should return - // `optional.none`. This output is implicit and created here to reflect the desired - // last possible output of this type of rule. - if (compiledRule.hasOptionalOutput()) { - output = - Step.newUnconditionalOptionalStep( - newTrueLiteral(), astMutator.newGlobalCall(Function.OPTIONAL_NONE.getFunction())); - } + boolean isAggregate = compiledRule.semantic() == CelPolicy.EvaluationSemantic.AGGREGATE; + boolean returnList = isAggregate || asList; + + Step output = createBaseStep(returnList, compiledRule.hasOptionalOutput()); long lastOutputId = 0; // The expected output type of the rule, used to verify that all branches agree on the type. @@ -85,59 +84,55 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) { boolean isTriviallyTrue = match.isConditionTriviallyTrue(); CelMutableAst condAst = CelMutableAst.fromCelAst(conditionAst); - long currentSourceId = lastOutputId; + Step currentStep; + long currentSourceId; + String validationMessage; switch (match.result().kind()) { case OUTPUT: + OutputValue matchOutput = match.result().output(); // If the match has an output, then it is considered a non-optional output since // it is explicitly stated. If the rule itself is optional, then the base case value // of output being optional.none() will convert the non-optional value to an optional // one. - OutputValue matchOutput = match.result().output(); - Step step = + CelMutableAst matchOutputAst = CelMutableAst.fromCelAst(matchOutput.ast()); + currentStep = Step.newNonOptionalStep( - !isTriviallyTrue, condAst, CelMutableAst.fromCelAst(matchOutput.ast())); - currentSourceId = matchOutput.sourceId(); + !isTriviallyTrue, condAst, returnList ? newList(matchOutputAst) : matchOutputAst); - output = combine(astMutator, step, output); - - String outputFailureMessage = - String.format( - "incompatible output types: block has output type %s, but previous outputs have" - + " type %s", - lastOutputType == null ? "" : CelTypes.format(lastOutputType), - CelTypes.format(matchOutput.ast().getResultType())); - lastOutputType = - assertComposedAstIsValid( - cel, output.expr, outputFailureMessage, currentSourceId, lastOutputId) - .getResultType(); + currentSourceId = matchOutput.sourceId(); + validationMessage = + incompatibleOutputTypesMessage( + lastOutputType, + matchOutput.ast().getResultType(), + returnList, + compiledRule.hasOptionalOutput()); break; case RULE: + CelCompiledRule matchNestedRule = match.result().rule(); // If the match has a nested rule, then compute the rule and whether it has // an optional return value. - CelCompiledRule matchNestedRule = match.result().rule(); - Step nestedRule = optimizeRule(cel, matchNestedRule); - Step ruleStep = - new Step( - matchNestedRule.hasOptionalOutput(), !isTriviallyTrue, condAst, nestedRule.expr); + Step nestedRule = optimizeRule(cel, matchNestedRule, returnList); + currentStep = new Step(nestedRule.isOptional, !isTriviallyTrue, condAst, nestedRule.expr); currentSourceId = getFirstOutputSourceId(matchNestedRule); - - output = combine(astMutator, ruleStep, output); - - lastOutputType = - assertComposedAstIsValid( - cel, - output.expr, - String.format( - "failed composing the subrule '%s' due to incompatible output types.", - matchNestedRule.ruleId().map(ValueString::value).orElse("")), - currentSourceId, - lastOutputId) - .getResultType(); + validationMessage = + String.format( + "failed composing the subrule '%s' due to incompatible output types.", + matchNestedRule.ruleId().map(ValueString::value).orElse("")); break; + default: + throw new IllegalStateException("Unknown match kind"); } + output = + isAggregate + ? combineAggregate(astMutator, currentStep, output) + : combine(astMutator, currentStep, output); + lastOutputType = + assertComposedAstIsValid( + cel, output.expr, validationMessage, currentSourceId, lastOutputId) + .getResultType(); lastOutputId = currentSourceId; } @@ -151,6 +146,24 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) { : Step.newUnconditionalNonOptionalStep(newTrueLiteral(), resultExpr); } + private @Nullable Step createBaseStep(boolean returnList, boolean hasOptionalOutput) { + if (returnList) { + // If the rule is evaluated as a list (AGGREGATE), the base case is an empty list. + return Step.newUnconditionalNonOptionalStep(newTrueLiteral(), newList()); + } + + if (hasOptionalOutput) { + // If the rule has an optional output, the last result in the ternary should return + // `optional.none`. This output is implicit and created here to reflect the desired + // last possible output of this type of rule. + return Step.newUnconditionalOptionalStep( + newTrueLiteral(), astMutator.newGlobalCall(Function.OPTIONAL_NONE.getFunction())); + } + + // Exhaustive non-optional rules start with no base case. + return null; + } + static RuleComposer newInstance( CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) { return new RuleComposer(compiledRule, variablePrefix, iterationLimit); @@ -250,6 +263,76 @@ private Step combineWhenCurrentIsNonOptional( } } + private Step combineAggregate(AstMutator astMutator, Step currentStep, Step accumulatedStep) { + CelMutableAst trueCondition = newTrueLiteral(); + // We assume currentStep.expr evaluates to a list due to contextual list generation. + CelMutableAst currentListPart = currentStep.expr; + // Stitch: currentStep.cond ? currentListPart : [] + // If the condition is false, we contribute an empty list to the accumulation, + // effectively dropping the result of this branch if it didn't match. + CelMutableAst conditionalListPart; + if (currentStep.isConditional) { + conditionalListPart = + astMutator.newGlobalCall( + Operator.CONDITIONAL.getFunction(), currentStep.cond, currentListPart, newList()); + + } else { + conditionalListPart = currentListPart; + } + + CelMutableAst concatenated = + astMutator.newGlobalCall( + Operator.ADD.getFunction(), conditionalListPart, accumulatedStep.expr); + + return Step.newUnconditionalNonOptionalStep(trueCondition, concatenated); + } + + /** + * Strips the structural type wrapper injected by the RuleComposer (e.g., optionals for + * FIRST_MATCH, lists for AGGREGATE) so that type mismatch errors display the raw underlying types + * authored by the user. + */ + private static @Nullable CelType unwrapComposerWrapper( + @Nullable CelType type, boolean returnList, boolean hasOptionalOutput) { + if (type == null) { + return null; + } + + if (returnList && type instanceof ListType) { + return ((ListType) type).elemType(); + } + + if (!returnList && hasOptionalOutput && type instanceof OptionalType) { + return type.parameters().get(0); + } + + return type; + } + + private static String incompatibleOutputTypesMessage( + @Nullable CelType lastOutputType, + CelType matchOutputType, + boolean returnList, + boolean hasOptionalOutput) { + CelType unwrappedLastOutputType = + unwrapComposerWrapper(lastOutputType, returnList, hasOptionalOutput); + return String.format( + "incompatible output types: block has output type %s, but previous outputs have" + + " type %s", + unwrappedLastOutputType == null ? "unknown type" : CelTypes.format(unwrappedLastOutputType), + CelTypes.format(matchOutputType)); + } + + private static CelMutableAst newList(CelMutableAst... elements) { + List exprs = new ArrayList<>(); + CelMutableSource combinedSource = CelMutableSource.newInstance(); + for (CelMutableAst element : elements) { + exprs.add(element.expr()); + combinedSource = AstMutator.combine(combinedSource, element.source()); + } + return CelMutableAst.of(CelMutableExpr.ofList(CelMutableList.create(exprs)), combinedSource); + } + private static boolean isOptionalNone(CelMutableAst ast) { CelMutableExpr expr = ast.expr(); return expr.getKind().equals(Kind.CALL) diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java index 73950069f..2e3274912 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java @@ -34,6 +34,8 @@ import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelOptions; import dev.cel.common.formats.ValueString; +import dev.cel.common.types.ListType; +import dev.cel.common.types.MapType; import dev.cel.common.types.OptionalType; import dev.cel.common.types.SimpleType; import dev.cel.expr.conformance.proto3.TestAllTypes; @@ -137,6 +139,168 @@ public void compileYamlPolicy_nestedRuleOptionalFallbackDivergence() throws Exce assertThat(ast.getResultType()).isEqualTo(OptionalType.create(SimpleType.BOOL)); } + @Test + public void evalYamlPolicy_aggregate() throws Exception { + String policySource = + "name: \"aggregate_policy\"\n" + + "rule:\n" + + " aggregate:\n" + + " - condition: 'true'\n" + + " emit: '\"PII\"'\n" + + " - condition: 'true'\n" + + " emit: '\"CONFIDENTIAL\"'\n"; + Cel cel = newCel(); + CelPolicy policy = POLICY_PARSER.parse(policySource); + + CelAbstractSyntaxTree ast = + CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy); + + Object evalResult = cel.createProgram(ast).eval(); + assertThat(evalResult).isEqualTo(ImmutableList.of("PII", "CONFIDENTIAL")); + } + + @Test + public void evaluateYamlPolicy_aggregate_cseApplied() throws Exception { + String policySource = + "name: \"cse_policy\"\n" + + "rule:\n" + + " aggregate:\n" + + " - condition: \"size(resource.payload) > 5\"\n" + + " emit: '\"CSE1\"'\n" + + " - condition: \"size(resource.payload) > 5\"\n" + + " emit: '\"CSE2\"'\n" + + " - condition: 'true'\n" + + " emit: '\"ALWAYS\"'\n"; + Cel cel = + newCel() + .toCelBuilder() + .addVar("resource", MapType.create(SimpleType.STRING, ListType.create(SimpleType.INT))) + .build(); + CelPolicy policy = POLICY_PARSER.parse(policySource); + + CelAbstractSyntaxTree ast = + CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy); + + String unparsed = CelUnparserFactory.newUnparser().unparse(ast); + assertThat(unparsed) + .isEqualTo( + "cel.@block(" + + "[size(resource.payload) > 5], " + + "(@index0 ? [\"CSE1\"] : []) " + + "+ ((@index0 ? [\"CSE2\"] : []) + [\"ALWAYS\"]))"); + + // Evaluate under true condition (size of payload is 6 > 5) + ImmutableMap inputTrue = + ImmutableMap.of("resource", ImmutableMap.of("payload", ImmutableList.of(1, 2, 3, 4, 5, 6))); + Object evalResultTrue = cel.createProgram(ast).eval(inputTrue); + assertThat(evalResultTrue).isEqualTo(ImmutableList.of("CSE1", "CSE2", "ALWAYS")); + // Evaluate under false condition (size of payload is 3 <= 5) + ImmutableMap inputFalse = + ImmutableMap.of("resource", ImmutableMap.of("payload", ImmutableList.of(1, 2, 3))); + Object evalResultFalse = cel.createProgram(ast).eval(inputFalse); + assertThat(evalResultFalse).isEqualTo(ImmutableList.of("ALWAYS")); + } + + @Test + public void compileYamlPolicy_aggregate_macrosPreserved() throws Exception { + String policySource = + "name: aggregate_macros_preserved\n" + + "rule:\n" + + " aggregate:\n" + + " - condition: \"cond\"\n" + + " rule:\n" + + " match:\n" + + " - condition: \"true\"\n" + + " output: \"payload.filter(x, x > 10).exists(y, y % 2 == 0)\"\n" + + " - condition: \"true\"\n" + + " emit: \"payload.all(x, x > 0)\"\n"; + Cel cel = + newCel() + .toCelBuilder() + .addVar("cond", SimpleType.BOOL) + .addVar("payload", ListType.create(SimpleType.INT)) + .build(); + + CelPolicy policy = POLICY_PARSER.parse(policySource); + + CelAbstractSyntaxTree ast = + CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy); + + String unparsed = CelUnparserFactory.newUnparser().unparse(ast); + assertThat(unparsed) + .isEqualTo( + "(cond ? [payload.filter(x, x > 10, x).exists(y, y % 2 == 0)] : []) " + + "+ ([payload.all(x, x > 0)] + [])"); + } + + @Test + public void compileYamlPolicy_nestedAggregate_throws() throws Exception { + String policySource = + "name: nested_aggregate\n" + + "rule:\n" + + " aggregate:\n" + + " - condition: 'true'\n" + + " rule:\n" + + " aggregate:\n" + + " - condition: 'true'\n" + + " emit: \"'foo'\"\n"; + CelPolicy policy = POLICY_PARSER.parse(policySource); + + CelPolicyValidationException e = + assertThrows( + CelPolicyValidationException.class, + () -> CelPolicyCompilerFactory.newPolicyCompiler(newCel()).build().compile(policy)); + + assertThat(e) + .hasMessageThat() + .contains("ERROR: :6:9: nested aggregate rules are not allowed"); + } + + @Test + public void compileYamlPolicy_nestedAggregate_withInterveningMatch_throws() throws Exception { + String policySource = + "name: nested_aggregate_with_match\n" + + "rule:\n" + + " aggregate:\n" + + " - condition: 'true'\n" + + " rule:\n" + + " match:\n" + + " - condition: 'true'\n" + + " rule:\n" + + " aggregate:\n" + + " - condition: 'true'\n" + + " emit: \"'foo'\"\n"; + CelPolicy policy = POLICY_PARSER.parse(policySource); + + CelPolicyValidationException e = + assertThrows( + CelPolicyValidationException.class, + () -> CelPolicyCompilerFactory.newPolicyCompiler(newCel()).build().compile(policy)); + + assertThat(e) + .hasMessageThat() + .contains("ERROR: :9:15: nested aggregate rules are not allowed"); + } + + @Test + public void compileYamlPolicy_aggregateUnderMatch_success() throws Exception { + String policySource = + "name: aggregate_under_match\n" + + "rule:\n" + + " match:\n" + + " - condition: 'true'\n" + + " rule:\n" + + " aggregate:\n" + + " - condition: 'true'\n" + + " emit: \"'foo'\"\n"; + CelPolicy policy = POLICY_PARSER.parse(policySource); + + CelAbstractSyntaxTree ast = + CelPolicyCompilerFactory.newPolicyCompiler(newCel()).build().compile(policy); + + assertThat(ast).isNotNull(); + } + @Test public void compileYamlPolicy_containsCompilationError_throws( @TestParameter TestErrorYamlPolicy testCase) throws Exception { @@ -398,7 +562,8 @@ public void compose_ruleWithNoOutputs_throws() throws Exception { Optional.of(ValueString.of(2L, "empty_rule")), ImmutableList.of(), ImmutableList.of(), - cel); + cel, + CelPolicy.EvaluationSemantic.FIRST_MATCH); RuleComposer composer = RuleComposer.newInstance(emptyRule, "variables.", 1000); CelAbstractSyntaxTree ast = cel.compile("true").getAst(); @@ -547,8 +712,12 @@ private enum TestErrorYamlPolicy { DUPLICATE_VARIABLE("duplicate_variable"), IMPORT("import"), INCOMPATIBLE_OUTPUTS("incompatible_outputs"), - SYNTAX("syntax"), UNDECLARED_REFERENCE("undeclared_reference"); + // TODO: Re-enable once cel-policy OSS dependency is updated with aggregate + // testdata. + // AGGREGATE_ERRORS("aggregate_errors"), + // AGGREGATE_LIST_ERRORS("aggregate_list_errors"), + // AGGREGATE_NESTED_MIXED_SEMANTICS("aggregate_nested_mixed_semantics"); private final String name; private final String policyFilePath; diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java index aaa30518a..dfb483981 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java @@ -328,6 +328,36 @@ private enum PolicyParseErrorTestCase { + " [tag:yaml.org,2002:str !txt]\n" + " | illegal: yaml-type\n" + " | ..^"), + BOTH_MATCH_AND_AGGREGATE_SET( + "name: test\n" + + "rule:\n" + + " match:\n" + + " - output: 'true'\n" + + " aggregate:\n" + + " - emit: 'true'\n", + "ERROR: :5:3: Only one of 'match' or 'aggregate' may be set in a rule\n" + + " | aggregate:\n" + + " | ..^"), + BOTH_AGGREGATE_AND_MATCH_SET( + "name: test\n" + + "rule:\n" + + " aggregate:\n" + + " - emit: 'true'\n" + + " match:\n" + + " - output: 'true'\n", + "ERROR: :5:3: Only one of 'match' or 'aggregate' may be set in a rule\n" + + " | match:\n" + + " | ..^"), + AGGREGATE_RULE_USES_OUTPUT( + "name: test\n" + "rule:\n" + " aggregate:\n" + " - output: 'true'\n", + "ERROR: :4:7: Rule aggregate requires 'emit' tag instead of 'output'\n" + + " | - output: 'true'\n" + + " | ......^"), + MATCH_RULE_USES_EMIT( + "name: test\n" + "rule:\n" + " match:\n" + " - emit: 'true'\n", + "ERROR: :4:7: Rule match requires 'output' tag instead of 'emit'\n" + + " | - emit: 'true'\n" + + " | ......^"), ILLEGAL_YAML_TYPE_ON_RULE_VALUE( "rule: illegal", "ERROR: :1:7: Got yaml node type tag:yaml.org,2002:str, wanted type(s)" diff --git a/testing/src/test/resources/policy/aggregate_errors/expected_errors.baseline b/testing/src/test/resources/policy/aggregate_errors/expected_errors.baseline new file mode 100644 index 000000000..538728881 --- /dev/null +++ b/testing/src/test/resources/policy/aggregate_errors/expected_errors.baseline @@ -0,0 +1,6 @@ +ERROR: aggregate_errors/policy.yaml:22:22: incompatible output types: block has output type int, but previous outputs have type optional_type(string) + | output: "optional.of('USER_PII')" + | .....................^ +ERROR: aggregate_errors/policy.yaml:24:22: incompatible output types: block has output type int, but previous outputs have type optional_type(string) + | output: "403" + | .....................^ \ No newline at end of file diff --git a/testing/src/test/resources/policy/aggregate_list_errors/expected_errors.baseline b/testing/src/test/resources/policy/aggregate_list_errors/expected_errors.baseline new file mode 100644 index 000000000..69afbc7e7 --- /dev/null +++ b/testing/src/test/resources/policy/aggregate_list_errors/expected_errors.baseline @@ -0,0 +1,6 @@ +ERROR: aggregate_list_errors/policy.yaml:22:22: incompatible output types: block has output type int, but previous outputs have type list(string) + | output: "['tag1', 'tag2']" + | .....................^ +ERROR: aggregate_list_errors/policy.yaml:24:22: incompatible output types: block has output type int, but previous outputs have type list(string) + | output: "403" + | .....................^ \ No newline at end of file diff --git a/testing/src/test/resources/policy/aggregate_nested_mixed_semantics/expected_errors.baseline b/testing/src/test/resources/policy/aggregate_nested_mixed_semantics/expected_errors.baseline new file mode 100644 index 000000000..4fe16c4ff --- /dev/null +++ b/testing/src/test/resources/policy/aggregate_nested_mixed_semantics/expected_errors.baseline @@ -0,0 +1,3 @@ +ERROR: aggregate_nested_mixed_semantics/policy.yaml:23:15: nested aggregate rules are not allowed + | aggregate: + | ..............^ diff --git a/testing/src/test/resources/policy/incompatible_outputs/expected_errors.baseline b/testing/src/test/resources/policy/incompatible_outputs/expected_errors.baseline index be370847f..7510dc02d 100644 --- a/testing/src/test/resources/policy/incompatible_outputs/expected_errors.baseline +++ b/testing/src/test/resources/policy/incompatible_outputs/expected_errors.baseline @@ -1,6 +1,7 @@ -ERROR: incompatible_outputs/policy.yaml:19:16: incompatible output types: block has output type optional_type(string), but previous outputs have type bool +ERROR: incompatible_outputs/policy.yaml:19:16: incompatible output types: block has output type string, but previous outputs have type bool | output: "true" | ...............^ -ERROR: incompatible_outputs/policy.yaml:21:16: incompatible output types: block has output type optional_type(string), but previous outputs have type bool +ERROR: incompatible_outputs/policy.yaml:21:16: incompatible output types: block has output type string, but previous outputs have type bool | output: "'false'" | ...............^ + diff --git a/testing/src/test/resources/policy/unreachable/expected_errors.baseline b/testing/src/test/resources/policy/unreachable/expected_errors.baseline index 768f0eeb1..875e00392 100644 --- a/testing/src/test/resources/policy/unreachable/expected_errors.baseline +++ b/testing/src/test/resources/policy/unreachable/expected_errors.baseline @@ -1,3 +1,6 @@ +ERROR: unreachable/policy.yaml:38:9: Condition is always false + | - condition: "false" + | ........^ ERROR: unreachable/policy.yaml:36:9: Match creates unreachable outputs | - output: | | ........^ From eb6fc204941f68b0564608bb2cd985588dd43522 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Fri, 31 Jul 2026 17:47:36 -0700 Subject: [PATCH 153/204] Introduce timestamp/duration as first class types in verifier. Add arithmetic axioms for the two types along with type conversions PiperOrigin-RevId: 957414761 --- .../cel/common/internal/ProtoTimeUtils.java | 13 +- verifier/README.md | 2 +- .../main/java/dev/cel/verifier/BUILD.bazel | 1 + .../cel/verifier/CelAstToZ3Translator.java | 17 +++ .../CelZ3CounterexampleGenerator.java | 4 + .../cel/verifier/CelZ3OperatorTranslator.java | 33 ++--- .../dev/cel/verifier/CelZ3TypeSystem.java | 105 +++++++++++++-- .../dev/cel/verifier/axioms/AddAxiom.java | 57 ++++++-- .../verifier/axioms/CelZ3FunctionAxiom.java | 3 +- .../dev/cel/verifier/axioms/GreaterAxiom.java | 16 +-- .../verifier/axioms/GreaterEqualsAxiom.java | 16 +-- .../dev/cel/verifier/axioms/LessAxiom.java | 16 +-- .../cel/verifier/axioms/LessEqualsAxiom.java | 16 +-- .../cel/verifier/axioms/SubtractAxiom.java | 57 ++++++-- .../dev/cel/verifier/axioms/TypeAxiom.java | 2 + .../verifier/axioms/TypeConversionAxioms.java | 50 +++++-- .../cel/verifier/CelVerifierZ3ImplTest.java | 124 ++++++++++++++++-- 17 files changed, 397 insertions(+), 135 deletions(-) diff --git a/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java b/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java index a6e98c571..124f9dbe1 100644 --- a/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java +++ b/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java @@ -18,7 +18,6 @@ import static com.google.common.math.LongMath.checkedMultiply; import static com.google.common.math.LongMath.checkedSubtract; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.protobuf.Duration; @@ -50,15 +49,11 @@ public final class ProtoTimeUtils { // Timestamp for "0001-01-01T00:00:00Z" - @VisibleForTesting - static final long TIMESTAMP_SECONDS_MIN = -62135596800L; + public static final long TIMESTAMP_SECONDS_MIN = -62135596800L; // Timestamp for "9999-12-31T23:59:59Z" - @VisibleForTesting - static final long TIMESTAMP_SECONDS_MAX = 253402300799L; - @VisibleForTesting - static final long DURATION_SECONDS_MIN = -315576000000L; - @VisibleForTesting - static final long DURATION_SECONDS_MAX = 315576000000L; + public static final long TIMESTAMP_SECONDS_MAX = 253402300799L; + public static final long DURATION_SECONDS_MIN = -315576000000L; + public static final long DURATION_SECONDS_MAX = 315576000000L; private static final int MILLIS_PER_SECOND = 1000; diff --git a/verifier/README.md b/verifier/README.md index db797a283..f286a4d6f 100644 --- a/verifier/README.md +++ b/verifier/README.md @@ -400,7 +400,7 @@ public class InvariantsExample { ### Timeouts -SMT solving is NP-complete and can theoretically stop responding or take an +SMT solving is NP-hard and can theoretically stop responding or take an exponential amount of time for complex formulas. The verifier uses a default timeout of 10 seconds. It is recommended to configure this to a reasonable duration for your specific use case using diff --git a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel index b9ac88687..4ca9794cc 100644 --- a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel +++ b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel @@ -98,6 +98,7 @@ java_library( tags = [ ], deps = [ + "//common/internal:proto_time_utils", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:tools_aqua_z3_turnkey", diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java index 8c0239efd..159a66a77 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java @@ -533,6 +533,12 @@ private Expr getDefaultValueForType(CelType type) { if (type.equals(SimpleType.UINT)) { return typeSystem.mkUint(0); } + if (type.equals(SimpleType.TIMESTAMP)) { + return typeSystem.wrapTimestamp(ctx.mkInt(0)); + } + if (type.equals(SimpleType.DURATION)) { + return typeSystem.wrapDuration(ctx.mkInt(0)); + } if (type instanceof ListType) { if (emptyListCache == null) { emptyListCache = typeSystem.mkListRefConst(EMPTY_LIST_PREFIX); @@ -1269,6 +1275,17 @@ private BoolExpr createTypeConstraintForType(Expr val, CelType type) { if (type.equals(SimpleType.BYTES)) { return (BoolExpr) ctx.mkApp(typeSystem.bytesCons().getTesterDecl(), val); } + if (type.equals(SimpleType.TIMESTAMP)) { + IntExpr seconds = typeSystem.getTimestamp(val); + return ctx.mkAnd( + typeSystem.isTimestamp(val), ctx.mkNot(typeSystem.checkTimestampOverflow(seconds))); + } + if (type.equals(SimpleType.DURATION)) { + IntExpr seconds = typeSystem.getDuration(val); + return ctx.mkAnd( + typeSystem.isDuration(val), ctx.mkNot(typeSystem.checkDurationOverflow(seconds))); + } + if (type instanceof ListType) { // Lists are explicitly bounded (sequence theory). We're safe in using for-all quantifiers // here. diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java index f7d47635f..2355d36bf 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java @@ -82,6 +82,10 @@ private static String formatExpr( // Handle CelType constructors wrapper unwrapping if (decl.equals(typeSystem.intCons().ConstructorDecl())) { return formatExpr(ctx, typeSystem, model, expr.getArgs()[0]); + } else if (decl.equals(typeSystem.timestampCons().ConstructorDecl())) { + return "timestamp(" + formatExpr(ctx, typeSystem, model, expr.getArgs()[0]) + ")"; + } else if (decl.equals(typeSystem.durationCons().ConstructorDecl())) { + return "duration(" + formatExpr(ctx, typeSystem, model, expr.getArgs()[0]) + ")"; } else if (decl.equals(typeSystem.uintCons().ConstructorDecl())) { return formatExpr(ctx, typeSystem, model, expr.getArgs()[0]) + "u"; } else if (decl.equals(typeSystem.boolCons().ConstructorDecl())) { diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java index effa91c2b..59795c6c5 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java @@ -148,11 +148,11 @@ private BoolExpr mkTypeGuard(Expr arg, CelType expectedType) { // These match everything structurally type-wise, although we might refine this later. return ctx.mkTrue(); case INT: + return typeSystem.isInt(arg); case TIMESTAMP: + return typeSystem.isTimestamp(arg); case DURATION: - // Safe to map int, timestamp, and duration to IntSort because CEL's static checker prevents - // invalid cross-type usage and their operator axioms translate to identical Z3 ASTs. - return typeSystem.isInt(arg); + return typeSystem.isDuration(arg); case UINT: return typeSystem.isUint(arg); case DOUBLE: @@ -386,7 +386,7 @@ private BoolExpr getNumericEqualityWithConstant( ? ctx.mkEq(typeSystem.getUint(symVal), ctx.mkInt(uintVal)) : ctx.mkFalse(); } else if (symType.kind() == CelKind.DOUBLE) { - return ctx.mkFPEq((FPExpr) typeSystem.getDouble(symVal), typeSystem.mkFpDouble(doubleVal)); + return ctx.mkFPEq(typeSystem.getDouble(symVal), typeSystem.mkFpDouble(doubleVal)); } } @@ -396,8 +396,7 @@ private BoolExpr getNumericEqualityWithConstant( (uintVal != null) ? ctx.mkEq(typeSystem.getUint(symVal), ctx.mkInt(uintVal)) : ctx.mkFalse(); - BoolExpr doubleEq = - ctx.mkFPEq((FPExpr) typeSystem.getDouble(symVal), typeSystem.mkFpDouble(doubleVal)); + BoolExpr doubleEq = ctx.mkFPEq(typeSystem.getDouble(symVal), typeSystem.mkFpDouble(doubleVal)); return (BoolExpr) CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) @@ -435,18 +434,17 @@ private BoolExpr getStaticallyKnownNumericEquality( case UINT: return ctx.mkEq(typeSystem.getUint(z3Expr0), typeSystem.getUint(z3Expr1)); case DOUBLE: - return ctx.mkFPEq( - (FPExpr) typeSystem.getDouble(z3Expr0), (FPExpr) typeSystem.getDouble(z3Expr1)); + return ctx.mkFPEq(typeSystem.getDouble(z3Expr0), typeSystem.getDouble(z3Expr1)); default: return ctx.mkFalse(); } } private BoolExpr mkIsFiniteDouble(Expr z3Expr) { - Expr fpVal = typeSystem.getDouble(z3Expr); + FPExpr fpVal = typeSystem.getDouble(z3Expr); return ctx.mkAnd( typeSystem.isDouble(z3Expr), - ctx.mkNot(ctx.mkOr(ctx.mkFPIsNaN((FPExpr) fpVal), ctx.mkFPIsInfinite((FPExpr) fpVal)))); + ctx.mkNot(ctx.mkOr(ctx.mkFPIsNaN(fpVal), ctx.mkFPIsInfinite(fpVal)))); } private BoolExpr getDynamicNumericEquality(Expr z3Expr0, Expr z3Expr1) { @@ -475,25 +473,28 @@ private BoolExpr getDynamicNumericEquality(Expr z3Expr0, Expr z3Expr1) { BoolExpr isIntOrUintAndDouble = ctx.mkAnd(isIntOrUint0, typeSystem.isDouble(z3Expr1)); BoolExpr isDoubleAndIntOrUint = ctx.mkAnd(typeSystem.isDouble(z3Expr0), isIntOrUint1); - Expr fpVal1 = typeSystem.getDouble(z3Expr1); + FPExpr fpVal1 = typeSystem.getDouble(z3Expr1); + ArithExpr realVal0 = ctx.mkInt2Real(val0); BoolExpr intDoubleEq = ctx.mkAnd( mkIsFiniteDouble(z3Expr1), - ctx.mkEq(ctx.mkInt2Real(val0), ctx.mkFPToReal((FPExpr) fpVal1))); + ctx.mkLe(realVal0, ctx.mkFPToReal(fpVal1)), + ctx.mkLe(ctx.mkFPToReal(fpVal1), realVal0)); - Expr fpVal0 = typeSystem.getDouble(z3Expr0); + FPExpr fpVal0 = typeSystem.getDouble(z3Expr0); + ArithExpr realVal1 = ctx.mkInt2Real(val1); BoolExpr doubleIntEq = ctx.mkAnd( mkIsFiniteDouble(z3Expr0), - ctx.mkEq(ctx.mkFPToReal((FPExpr) fpVal0), ctx.mkInt2Real(val1))); + ctx.mkLe(realVal1, ctx.mkFPToReal(fpVal0)), + ctx.mkLe(ctx.mkFPToReal(fpVal0), realVal1)); return (BoolExpr) CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) .addCase(bothIntOrUint, ctx.mkEq(val0, val1)) .addCase( bothDouble, - ctx.mkFPEq( - (FPExpr) typeSystem.getDouble(z3Expr0), (FPExpr) typeSystem.getDouble(z3Expr1))) + ctx.mkFPEq(typeSystem.getDouble(z3Expr0), typeSystem.getDouble(z3Expr1))) .addCase(isIntOrUintAndDouble, intDoubleEq) .addCase(isDoubleAndIntOrUint, doubleIntEq) .build(ctx.mkFalse()); diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java index af1a4688b..5c0b87fbe 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java @@ -31,6 +31,7 @@ import com.microsoft.z3.SeqExpr; import com.microsoft.z3.SeqSort; import com.microsoft.z3.Sort; +import dev.cel.common.internal.ProtoTimeUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -82,6 +83,14 @@ public final class CelZ3TypeSystem { private static final String IS_BYTES = "isBytes"; private static final String GET_BYTES = "getBytes"; + private static final String CONS_TIMESTAMP = "Timestamp"; + private static final String IS_TIMESTAMP = "isTimestamp"; + private static final String GET_TIMESTAMP = "getTimestamp"; + + private static final String CONS_DURATION = "Duration"; + private static final String IS_DURATION = "isDuration"; + private static final String GET_DURATION = "getDuration"; + private static final String CONS_ERROR = "CelError"; private static final String IS_ERROR = "isError"; @@ -170,6 +179,8 @@ public int hashCode() { private final Constructor doubleCons; private final Constructor stringCons; private final Constructor bytesCons; + private final Constructor timestampCons; + private final Constructor durationCons; private final Constructor errorCons; private final Constructor unknownCons; private final Constructor nullCons; @@ -233,30 +244,38 @@ public Sort listRefSort() { return listRefSort; } - Constructor boolCons() { + public Constructor boolCons() { return boolCons; } - Constructor intCons() { + public Constructor intCons() { return intCons; } - Constructor uintCons() { + public Constructor uintCons() { return uintCons; } - Constructor doubleCons() { + public Constructor doubleCons() { return doubleCons; } - Constructor stringCons() { + public Constructor stringCons() { return stringCons; } - Constructor bytesCons() { + public Constructor bytesCons() { return bytesCons; } + public Constructor timestampCons() { + return timestampCons; + } + + public Constructor durationCons() { + return durationCons; + } + Constructor optionalCons() { return optionalCons; } @@ -296,6 +315,16 @@ public Expr wrapBytes(Expr expr) { return ctx.mkApp(bytesCons.ConstructorDecl(), expr); } + /** Wraps a Z3 integer expression into a timestamp CelValue. */ + public Expr wrapTimestamp(IntExpr expr) { + return ctx.mkApp(timestampCons.ConstructorDecl(), expr); + } + + /** Wraps a Z3 integer expression into a duration CelValue. */ + public Expr wrapDuration(IntExpr expr) { + return ctx.mkApp(durationCons.ConstructorDecl(), expr); + } + /** Creates a CelValue containing an integer. */ public Expr mkInt(long val) { return ctx.mkApp(intCons.ConstructorDecl(), ctx.mkInt(val)); @@ -326,8 +355,8 @@ public BoolExpr isDouble(Expr val) { } /** Extracts the double reference from a double CelValue. */ - public Expr getDouble(Expr val) { - return ctx.mkApp(doubleCons.getAccessorDecls()[0], val); + public FPExpr getDouble(Expr val) { + return (FPExpr) ctx.mkApp(doubleCons.getAccessorDecls()[0], val); } /** @@ -372,7 +401,7 @@ public BoolExpr getStructuralEquality(Expr arg0, Expr arg1) { // Doubles must be compared using native floating-point equality to follow IEEE-754. // Z3's structural mkEq evaluates NaN == NaN as true and 0.0 == -0.0 as false. BoolExpr isDoubleEq = ctx.mkAnd(isDouble(arg0), isDouble(arg1)); - BoolExpr doubleEq = ctx.mkFPEq((FPExpr) getDouble(arg0), (FPExpr) getDouble(arg1)); + BoolExpr doubleEq = ctx.mkFPEq(getDouble(arg0), getDouble(arg1)); // For primitives, generic equality matches the direct Z3 datatype wrapper. BoolExpr genericEq = ctx.mkEq(arg0, arg1); @@ -409,10 +438,14 @@ public Expr mkNull() { return ctx.mkConst(nullCons.ConstructorDecl()); } - Constructor errorCons() { + public Constructor errorCons() { return errorCons; } + public Constructor nullCons() { + return nullCons; + } + /** Creates a CelValue representing an unknown value. */ public Expr mkUnknown() { return mkUnknown(ctx.mkConst(GENERIC_UNKNOWN_ID, unknownIdSort)); @@ -498,7 +531,7 @@ public Expr withRuntimeError( return ctx.mkITE(condition, mkError(), result); } - Constructor unknownCons() { + public Constructor unknownCons() { return unknownCons; } @@ -582,6 +615,26 @@ public IntExpr getUint(Expr val) { return (IntExpr) ctx.mkApp(uintCons.getAccessorDecls()[0], val); } + /** Checks if the given CelValue is a timestamp. */ + public BoolExpr isTimestamp(Expr val) { + return (BoolExpr) ctx.mkApp(timestampCons.getTesterDecl(), val); + } + + /** Extracts the integer expression from a timestamp CelValue. */ + public IntExpr getTimestamp(Expr val) { + return (IntExpr) ctx.mkApp(timestampCons.getAccessorDecls()[0], val); + } + + /** Checks if the given CelValue is a duration. */ + public BoolExpr isDuration(Expr val) { + return (BoolExpr) ctx.mkApp(durationCons.getTesterDecl(), val); + } + + /** Extracts the integer expression from a duration CelValue. */ + public IntExpr getDuration(Expr val) { + return (IntExpr) ctx.mkApp(durationCons.getAccessorDecls()[0], val); + } + /** Checks if the given CelValue is a string. */ public BoolExpr isString(Expr val) { return (BoolExpr) ctx.mkApp(stringCons.getTesterDecl(), val); @@ -719,6 +772,20 @@ public BoolExpr checkIntOverflow(ArithExpr result) { return ctx.mkOr(ctx.mkGt(result, ctx.mkInt(MAX_INT64)), ctx.mkLt(result, ctx.mkInt(MIN_INT64))); } + /** Checks if the given arithmetic expression overflows CEL Timestamp bounds. */ + public BoolExpr checkTimestampOverflow(ArithExpr result) { + return ctx.mkOr( + ctx.mkGt(result, ctx.mkInt(ProtoTimeUtils.TIMESTAMP_SECONDS_MAX)), + ctx.mkLt(result, ctx.mkInt(ProtoTimeUtils.TIMESTAMP_SECONDS_MIN))); + } + + /** Checks if the given arithmetic expression overflows CEL Duration bounds. */ + public BoolExpr checkDurationOverflow(ArithExpr result) { + return ctx.mkOr( + ctx.mkGt(result, ctx.mkInt(ProtoTimeUtils.DURATION_SECONDS_MAX)), + ctx.mkLt(result, ctx.mkInt(ProtoTimeUtils.DURATION_SECONDS_MIN))); + } + /** Checks if the given arithmetic expression overflows a 64-bit unsigned integer. */ public BoolExpr checkUintOverflow(ArithExpr result) { return ctx.mkOr(ctx.mkGt(result, ctx.mkInt(MAX_UINT64)), ctx.mkLt(result, ctx.mkInt(0))); @@ -890,6 +957,20 @@ public static BoolExpr mkNotFlattened(Context ctx, BoolExpr arg) { this.bytesCons = ctx.mkConstructor( CONS_BYTES, IS_BYTES, new String[] {GET_BYTES}, new Sort[] {ctx.getStringSort()}, null); + this.timestampCons = + ctx.mkConstructor( + CONS_TIMESTAMP, + IS_TIMESTAMP, + new String[] {GET_TIMESTAMP}, + new Sort[] {ctx.getIntSort()}, + null); + this.durationCons = + ctx.mkConstructor( + CONS_DURATION, + IS_DURATION, + new String[] {GET_DURATION}, + new Sort[] {ctx.getIntSort()}, + null); this.errorCons = ctx.mkConstructor(CONS_ERROR, IS_ERROR, null, null, null); this.unknownIdSort = ctx.mkUninterpretedSort("UnknownId"); @@ -936,6 +1017,8 @@ public static BoolExpr mkNotFlattened(Context ctx, BoolExpr arg) { this.doubleCons, this.stringCons, this.bytesCons, + this.timestampCons, + this.durationCons, this.errorCons, this.unknownCons, this.optionalCons, diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/AddAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/AddAxiom.java index 072f75088..bff6b1edb 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/AddAxiom.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/AddAxiom.java @@ -14,30 +14,67 @@ package dev.cel.verifier.axioms; +import com.microsoft.z3.ArithExpr; import com.microsoft.z3.BoolExpr; import com.microsoft.z3.Expr; -import com.microsoft.z3.FPExpr; import com.microsoft.z3.FuncDecl; import com.microsoft.z3.IntExpr; import com.microsoft.z3.SeqExpr; import com.microsoft.z3.Sort; import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import dev.cel.verifier.CelZ3TypeSystem; import java.util.Optional; +import java.util.function.BiFunction; /** Axiomatization for CEL's addition operator (+). */ final class AddAxiom { + @SuppressWarnings("Immutable") // Actually immutable -- BiFunction just isn't annotated as such. + private static CelZ3FunctionAxiom.BinaryTranslator createAddTranslator( + BiFunction, IntExpr> getLeft, + BiFunction, IntExpr> getRight, + BiFunction> wrapResult, + BiFunction, BoolExpr> overflowChecker) { + return (ctx, ts, sink, l, r) -> { + IntExpr a1 = getLeft.apply(ts, l); + IntExpr a2 = getRight.apply(ts, r); + ArithExpr addition = ctx.mkAdd(a1, a2); + Expr result = wrapResult.apply(ts, (IntExpr) addition); + BoolExpr overflow = overflowChecker.apply(ts, addition); + return Optional.of(ts.withRuntimeError(result, overflow)); + }; + } + static final CelZ3FunctionAxiom INSTANCE = CelZ3FunctionAxiom.newBuilder(StandardFunction.ADD.functionDecl()) .addBinaryOverloadTranslator( StandardFunction.Overload.Arithmetic.ADD_INT64.celOverloadDecl(), - (ctx, ts, sink, l, r) -> { - IntExpr a1 = ts.getInt(l); - IntExpr a2 = ts.getInt(r); - Expr result = ts.wrapInt((IntExpr) ctx.mkAdd(a1, a2)); - BoolExpr overflow = ts.checkIntOverflow(ctx.mkAdd(a1, a2)); - return Optional.of(ts.withRuntimeError(result, overflow)); - }) + createAddTranslator( + CelZ3TypeSystem::getInt, + CelZ3TypeSystem::getInt, + CelZ3TypeSystem::wrapInt, + CelZ3TypeSystem::checkIntOverflow)) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.ADD_TIMESTAMP_DURATION.celOverloadDecl(), + createAddTranslator( + CelZ3TypeSystem::getTimestamp, + CelZ3TypeSystem::getDuration, + CelZ3TypeSystem::wrapTimestamp, + CelZ3TypeSystem::checkTimestampOverflow)) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.ADD_DURATION_TIMESTAMP.celOverloadDecl(), + createAddTranslator( + CelZ3TypeSystem::getDuration, + CelZ3TypeSystem::getTimestamp, + CelZ3TypeSystem::wrapTimestamp, + CelZ3TypeSystem::checkTimestampOverflow)) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.ADD_DURATION_DURATION.celOverloadDecl(), + createAddTranslator( + CelZ3TypeSystem::getDuration, + CelZ3TypeSystem::getDuration, + CelZ3TypeSystem::wrapDuration, + CelZ3TypeSystem::checkDurationOverflow)) .addBinaryOverloadTranslator( StandardFunction.Overload.Arithmetic.ADD_UINT64.celOverloadDecl(), (ctx, ts, sink, l, r) -> { @@ -53,9 +90,7 @@ final class AddAxiom { Optional.of( ts.wrapDouble( ctx.mkFPAdd( - ctx.mkFPRoundNearestTiesToEven(), - (FPExpr) ts.getDouble(l), - (FPExpr) ts.getDouble(r))))) + ctx.mkFPRoundNearestTiesToEven(), ts.getDouble(l), ts.getDouble(r))))) .addBinaryOverloadTranslator( StandardFunction.Overload.Arithmetic.ADD_STRING.celOverloadDecl(), (ctx, ts, sink, l, r) -> diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/CelZ3FunctionAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/CelZ3FunctionAxiom.java index 2d1129618..06d797d4d 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/CelZ3FunctionAxiom.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/CelZ3FunctionAxiom.java @@ -110,8 +110,7 @@ public Builder addUnaryOverloadTranslator( Expr val = res.get(); BoolExpr approx = argApproximations.get(0); if (isApproximated) { - BoolExpr isErrorOrUnknown = ctx.mkOr(ts.isError(val), ts.isUnknown(val)); - approx = (BoolExpr) ctx.mkITE(isErrorOrUnknown, approx, ctx.mkTrue()); + approx = (BoolExpr) ctx.mkITE(ts.isUnknown(val), approx, ctx.mkTrue()); } return Optional.of(CelZ3OverloadResult.create(val, approx)); }; diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/GreaterAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/GreaterAxiom.java index 527ef70ad..292b86135 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/GreaterAxiom.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/GreaterAxiom.java @@ -32,33 +32,25 @@ final class GreaterAxiom { (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkGt( - (ArithExpr) typeSystem.getInt(lhs), - (ArithExpr) typeSystem.getInt(rhs))))) + ctx.mkGt(typeSystem.getInt(lhs), typeSystem.getInt(rhs))))) .addBinaryOverloadTranslator( Comparison.GREATER_TIMESTAMP.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkGt( - (ArithExpr) typeSystem.getInt(lhs), - (ArithExpr) typeSystem.getInt(rhs))))) + ctx.mkGt(typeSystem.getTimestamp(lhs), typeSystem.getTimestamp(rhs))))) .addBinaryOverloadTranslator( Comparison.GREATER_DURATION.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkGt( - (ArithExpr) typeSystem.getInt(lhs), - (ArithExpr) typeSystem.getInt(rhs))))) + ctx.mkGt(typeSystem.getDuration(lhs), typeSystem.getDuration(rhs))))) .addBinaryOverloadTranslator( Comparison.GREATER_UINT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkGt( - (ArithExpr) typeSystem.getUint(lhs), - (ArithExpr) typeSystem.getUint(rhs))))) + ctx.mkGt(typeSystem.getUint(lhs), typeSystem.getUint(rhs))))) .addBinaryOverloadTranslator( Comparison.GREATER_DOUBLE.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/GreaterEqualsAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/GreaterEqualsAxiom.java index ec3ffa69a..4be0c23e2 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/GreaterEqualsAxiom.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/GreaterEqualsAxiom.java @@ -32,33 +32,25 @@ final class GreaterEqualsAxiom { (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkGe( - (ArithExpr) typeSystem.getInt(lhs), - (ArithExpr) typeSystem.getInt(rhs))))) + ctx.mkGe(typeSystem.getInt(lhs), typeSystem.getInt(rhs))))) .addBinaryOverloadTranslator( Comparison.GREATER_EQUALS_TIMESTAMP.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkGe( - (ArithExpr) typeSystem.getInt(lhs), - (ArithExpr) typeSystem.getInt(rhs))))) + ctx.mkGe(typeSystem.getTimestamp(lhs), typeSystem.getTimestamp(rhs))))) .addBinaryOverloadTranslator( Comparison.GREATER_EQUALS_DURATION.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkGe( - (ArithExpr) typeSystem.getInt(lhs), - (ArithExpr) typeSystem.getInt(rhs))))) + ctx.mkGe(typeSystem.getDuration(lhs), typeSystem.getDuration(rhs))))) .addBinaryOverloadTranslator( Comparison.GREATER_EQUALS_UINT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkGe( - (ArithExpr) typeSystem.getUint(lhs), - (ArithExpr) typeSystem.getUint(rhs))))) + ctx.mkGe(typeSystem.getUint(lhs), typeSystem.getUint(rhs))))) .addBinaryOverloadTranslator( Comparison.GREATER_EQUALS_DOUBLE.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/LessAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/LessAxiom.java index 429aee284..e09484f28 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/LessAxiom.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/LessAxiom.java @@ -32,33 +32,25 @@ final class LessAxiom { (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkLt( - (ArithExpr) typeSystem.getInt(lhs), - (ArithExpr) typeSystem.getInt(rhs))))) + ctx.mkLt(typeSystem.getInt(lhs), typeSystem.getInt(rhs))))) .addBinaryOverloadTranslator( Comparison.LESS_TIMESTAMP.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkLt( - (ArithExpr) typeSystem.getInt(lhs), - (ArithExpr) typeSystem.getInt(rhs))))) + ctx.mkLt(typeSystem.getTimestamp(lhs), typeSystem.getTimestamp(rhs))))) .addBinaryOverloadTranslator( Comparison.LESS_DURATION.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkLt( - (ArithExpr) typeSystem.getInt(lhs), - (ArithExpr) typeSystem.getInt(rhs))))) + ctx.mkLt(typeSystem.getDuration(lhs), typeSystem.getDuration(rhs))))) .addBinaryOverloadTranslator( Comparison.LESS_UINT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkLt( - (ArithExpr) typeSystem.getUint(lhs), - (ArithExpr) typeSystem.getUint(rhs))))) + ctx.mkLt(typeSystem.getUint(lhs), typeSystem.getUint(rhs))))) .addBinaryOverloadTranslator( Comparison.LESS_DOUBLE.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/LessEqualsAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/LessEqualsAxiom.java index 5099850dd..e27b47631 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/LessEqualsAxiom.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/LessEqualsAxiom.java @@ -32,33 +32,25 @@ final class LessEqualsAxiom { (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkLe( - (ArithExpr) typeSystem.getInt(lhs), - (ArithExpr) typeSystem.getInt(rhs))))) + ctx.mkLe(typeSystem.getInt(lhs), typeSystem.getInt(rhs))))) .addBinaryOverloadTranslator( Comparison.LESS_EQUALS_TIMESTAMP.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkLe( - (ArithExpr) typeSystem.getInt(lhs), - (ArithExpr) typeSystem.getInt(rhs))))) + ctx.mkLe(typeSystem.getTimestamp(lhs), typeSystem.getTimestamp(rhs))))) .addBinaryOverloadTranslator( Comparison.LESS_EQUALS_DURATION.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkLe( - (ArithExpr) typeSystem.getInt(lhs), - (ArithExpr) typeSystem.getInt(rhs))))) + ctx.mkLe(typeSystem.getDuration(lhs), typeSystem.getDuration(rhs))))) .addBinaryOverloadTranslator( Comparison.LESS_EQUALS_UINT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkLe( - (ArithExpr) typeSystem.getUint(lhs), - (ArithExpr) typeSystem.getUint(rhs))))) + ctx.mkLe(typeSystem.getUint(lhs), typeSystem.getUint(rhs))))) .addBinaryOverloadTranslator( Comparison.LESS_EQUALS_DOUBLE.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/SubtractAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/SubtractAxiom.java index bd0ecf882..3bedacf8c 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/SubtractAxiom.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/SubtractAxiom.java @@ -14,27 +14,64 @@ package dev.cel.verifier.axioms; +import com.microsoft.z3.ArithExpr; import com.microsoft.z3.BoolExpr; import com.microsoft.z3.Expr; -import com.microsoft.z3.FPExpr; import com.microsoft.z3.IntExpr; import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import dev.cel.verifier.CelZ3TypeSystem; import java.util.Optional; +import java.util.function.BiFunction; /** Axiomatization for CEL's subtraction operator (-). */ final class SubtractAxiom { + @SuppressWarnings("Immutable") // Actually immutable -- BiFunction just isn't annotated as such. + private static CelZ3FunctionAxiom.BinaryTranslator createSubtractTranslator( + BiFunction, IntExpr> getLeft, + BiFunction, IntExpr> getRight, + BiFunction> wrapResult, + BiFunction, BoolExpr> overflowChecker) { + return (ctx, ts, sink, l, r) -> { + IntExpr a1 = getLeft.apply(ts, l); + IntExpr a2 = getRight.apply(ts, r); + ArithExpr subtraction = ctx.mkSub(a1, a2); + Expr result = wrapResult.apply(ts, (IntExpr) subtraction); + BoolExpr overflow = overflowChecker.apply(ts, subtraction); + return Optional.of(ts.withRuntimeError(result, overflow)); + }; + } + static final CelZ3FunctionAxiom INSTANCE = CelZ3FunctionAxiom.newBuilder(StandardFunction.SUBTRACT.functionDecl()) .addBinaryOverloadTranslator( StandardFunction.Overload.Arithmetic.SUBTRACT_INT64.celOverloadDecl(), - (ctx, ts, sink, l, r) -> { - IntExpr a1 = ts.getInt(l); - IntExpr a2 = ts.getInt(r); - Expr result = ts.wrapInt((IntExpr) ctx.mkSub(a1, a2)); - BoolExpr overflow = ts.checkIntOverflow(ctx.mkSub(a1, a2)); - return Optional.of(ts.withRuntimeError(result, overflow)); - }) + createSubtractTranslator( + CelZ3TypeSystem::getInt, + CelZ3TypeSystem::getInt, + CelZ3TypeSystem::wrapInt, + CelZ3TypeSystem::checkIntOverflow)) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.SUBTRACT_TIMESTAMP_TIMESTAMP.celOverloadDecl(), + createSubtractTranslator( + CelZ3TypeSystem::getTimestamp, + CelZ3TypeSystem::getTimestamp, + CelZ3TypeSystem::wrapDuration, + CelZ3TypeSystem::checkDurationOverflow)) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.SUBTRACT_TIMESTAMP_DURATION.celOverloadDecl(), + createSubtractTranslator( + CelZ3TypeSystem::getTimestamp, + CelZ3TypeSystem::getDuration, + CelZ3TypeSystem::wrapTimestamp, + CelZ3TypeSystem::checkTimestampOverflow)) + .addBinaryOverloadTranslator( + StandardFunction.Overload.Arithmetic.SUBTRACT_DURATION_DURATION.celOverloadDecl(), + createSubtractTranslator( + CelZ3TypeSystem::getDuration, + CelZ3TypeSystem::getDuration, + CelZ3TypeSystem::wrapDuration, + CelZ3TypeSystem::checkDurationOverflow)) .addBinaryOverloadTranslator( StandardFunction.Overload.Arithmetic.SUBTRACT_UINT64.celOverloadDecl(), (ctx, ts, sink, l, r) -> { @@ -50,9 +87,7 @@ final class SubtractAxiom { Optional.of( ts.wrapDouble( ctx.mkFPSub( - ctx.mkFPRoundNearestTiesToEven(), - (FPExpr) ts.getDouble(l), - (FPExpr) ts.getDouble(r))))) + ctx.mkFPRoundNearestTiesToEven(), ts.getDouble(l), ts.getDouble(r))))) .build(); private SubtractAxiom() {} diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/TypeAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/TypeAxiom.java index fc4757686..9c49ef958 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/TypeAxiom.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/TypeAxiom.java @@ -61,6 +61,8 @@ private static Expr getTypeExpression(Context ctx, CelZ3TypeSystem typeSystem typeSystem.wrapString(typeSystem.getMsgTypeName(typeSystem.getMessageRef(val)))) .addCase(typeSystem.isOptional(val), typeSystem.mkString(OptionalType.NAME)) .addCase(typeSystem.isNull(val), typeSystem.mkString(SimpleType.NULL_TYPE.name())) + .addCase(typeSystem.isTimestamp(val), typeSystem.mkString(SimpleType.TIMESTAMP.name())) + .addCase(typeSystem.isDuration(val), typeSystem.mkString(SimpleType.DURATION.name())) .addCase(typeSystem.isMap(val), typeSystem.mkString(TYPE_NAME_MAP)) .addCase(typeSystem.isList(val), typeSystem.mkString(TYPE_NAME_LIST)) .addCase(typeSystem.isBytes(val), typeSystem.mkString(SimpleType.BYTES.name())) diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java b/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java index 94bdf0651..fabdd3d9b 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java @@ -19,7 +19,6 @@ import com.google.common.collect.ImmutableList; import com.microsoft.z3.BoolExpr; import com.microsoft.z3.Expr; -import com.microsoft.z3.FPExpr; import com.microsoft.z3.FuncDecl; import com.microsoft.z3.IntExpr; import com.microsoft.z3.Sort; @@ -53,8 +52,8 @@ final class TypeConversionAxioms { true) .addUnaryOverloadTranslator( Conversions.TIMESTAMP_TO_INT64.celOverloadDecl(), - createUninterpretedConversion(Conversions.TIMESTAMP_TO_INT64), - true) + (ctx, typeSystem, sink, arg) -> + Optional.of(typeSystem.wrapInt(typeSystem.getTimestamp(arg)))) .build(); private static final CelZ3FunctionAxiom UINT_AXIOM = @@ -173,8 +172,12 @@ final class TypeConversionAxioms { true) .addUnaryOverloadTranslator( Conversions.INT64_TO_TIMESTAMP.celOverloadDecl(), - createUninterpretedConversion(Conversions.INT64_TO_TIMESTAMP), - true) + (ctx, typeSystem, sink, arg) -> { + IntExpr intVal = typeSystem.getInt(arg); + BoolExpr overflow = typeSystem.checkTimestampOverflow(intVal); + return Optional.of( + typeSystem.withRuntimeError(typeSystem.wrapTimestamp(intVal), overflow)); + }) .build(); private static final CelZ3FunctionAxiom BOOL_AXIOM = @@ -212,18 +215,38 @@ private static CelZ3FunctionAxiom.UnaryTranslator createUninterpretedConversion( switch (conversion.celOverloadDecl().resultType().kind()) { case INT: + BoolExpr intValid = + ctx.mkAnd( + typeSystem.isInt(res), + ctx.mkNot(typeSystem.checkIntOverflow(typeSystem.getInt(res)))); + sink.accept(intValid); + break; case TIMESTAMP: + BoolExpr timestampValid = + ctx.mkAnd( + typeSystem.isTimestamp(res), + ctx.mkNot(typeSystem.checkTimestampOverflow(typeSystem.getTimestamp(res)))); + sink.accept(timestampValid); + break; case DURATION: - sink.accept(typeSystem.isInt(res)); - sink.accept(ctx.mkNot(typeSystem.checkIntOverflow(typeSystem.getInt(res)))); + BoolExpr durationValid = + ctx.mkAnd( + typeSystem.isDuration(res), + ctx.mkNot(typeSystem.checkDurationOverflow(typeSystem.getDuration(res)))); + sink.accept(durationValid); break; case UINT: - sink.accept(typeSystem.isUint(res)); - sink.accept(ctx.mkNot(typeSystem.checkUintOverflow(typeSystem.getUint(res)))); + BoolExpr uintValid = + ctx.mkAnd( + typeSystem.isUint(res), + ctx.mkNot(typeSystem.checkUintOverflow(typeSystem.getUint(res)))); + sink.accept(uintValid); break; case DOUBLE: - sink.accept(typeSystem.isDouble(res)); - sink.accept(ctx.mkNot(ctx.mkFPIsNaN((FPExpr) typeSystem.getDouble(res)))); + BoolExpr doubleValid = + ctx.mkAnd( + typeSystem.isDouble(res), ctx.mkNot(ctx.mkFPIsNaN(typeSystem.getDouble(res)))); + sink.accept(doubleValid); break; case STRING: sink.accept(typeSystem.isString(res)); @@ -235,8 +258,11 @@ private static CelZ3FunctionAxiom.UnaryTranslator createUninterpretedConversion( sink.accept(typeSystem.isBool(res)); break; default: - break; + throw new IllegalArgumentException( + "Unsupported uninterpreted conversion result type: " + + conversion.celOverloadDecl().resultType()); } + return Optional.of(res); }; } diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index 59a5e8438..8759422c3 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -94,6 +94,8 @@ public final class CelVerifierZ3ImplTest { .addVar("role", SimpleType.STRING) .addVar("country", SimpleType.STRING) .addVar("port", SimpleType.INT) + .addVar("dur", SimpleType.DURATION) + .addVar("ts", SimpleType.TIMESTAMP) .addVar("request", SimpleType.DYN) .addVar("unknown_var", SimpleType.DYN) .addVar("int_list", ListType.create(SimpleType.INT)) @@ -288,7 +290,9 @@ private enum IsUnsatisfiableTestCase { TYPE_CONVERSION_UNSATISFIABLE_DOUBLE_TO_STRING("type(string(1.5)) == int"), EMPTY_MAP_SIZE_NOT_ZERO("size({}) != 0"), TIMESTAMP_INEQUALITY_CONTRADICTION( - "timestamp('2023-01-01T00:00:00Z') != timestamp('2023-01-01T00:00:00Z')"); + "timestamp('2023-01-01T00:00:00Z') != timestamp('2023-01-01T00:00:00Z')"), + TYPE_TIMESTAMP_NOT_INT("type(timestamp('1970-01-01T00:00:00Z')) == int"), + DYN_INT_NOT_DURATION("dyn(1) == dyn(duration('1s'))"); final String expr; @@ -321,6 +325,22 @@ public void isSatisfiable_timeout_throwsException() throws Exception { private enum IsAlwaysTrueTestCase { LOGICAL_OR_CONSTANTS("true || false"), + DYNAMIC_EQUALITY_TIMESTAMP_INT_COLLISION( + "type(dyn_var) == int && dyn_var == 0 ? dyn_var != timestamp('1970-01-01T00:00:00Z') :" + + " true"), + TIMESTAMP_STRING_CONVERSION_VALID( + "timestamp('2023-01-01T00:00:00Z') == timestamp('2023-01-01T00:00:00Z')"), + DURATION_STRING_CONVERSION_VALID("duration('100s') == duration('100s')"), + TIMESTAMP_BOUNDS_VALID("timestamp('2023-01-01T00:00:00Z') <= timestamp(253402300799)"), + TIMESTAMP_GREATER_EQUALS("timestamp(200) >= timestamp(100)"), + DURATION_GREATER_EQUALS( + "(timestamp(200) - timestamp(100)) >= (timestamp(150) - timestamp(100))"), + DURATION_GREATER("(timestamp(200) - timestamp(100)) > (timestamp(150) - timestamp(100))"), + TIMESTAMP_LESS("timestamp(100) < timestamp(200)"), + DURATION_LESS("(timestamp(150) - timestamp(100)) < (timestamp(200) - timestamp(100))"), + TIMESTAMP_VARIABLE_TYPE("type(ts) == type(timestamp(0))"), + DURATION_VARIABLE_TYPE("type(dur) == type(timestamp(1) - timestamp(0))"), + TIMESTAMP_VARIABLE_BOUNDS("ts >= timestamp(-62135596800) && ts <= timestamp(253402300799)"), CYCLIC_MACRO_SHADOWING_SAFETY("[1].all(x, [x].all(x, x == 1))"), TAUTOLOGY("x > 5 || x <= 5"), LIST_VARIABLE_CONSTRAINED("1 in int_list || !(1 in int_list)"), @@ -954,7 +974,12 @@ private enum UnconditionalErrorTestCase { COLLECTION_ERROR("{'a': 1 / 0} == {'a': 1 / 0}"), LIST_ERROR("[1 / 0] == [1 / 0]"), STRICT_LITERAL_ERROR("{'a': 1/0}.exists(k, k == 'a') == {'a': 1/0}.exists(k, k == 'a')"), - STRICT_LITERAL_ERROR_KEY("{1/0: 'a'}.exists(k, k == 1) == {1/0: 'a'}.exists(k, k == 1)"); + STRICT_LITERAL_ERROR_KEY("{1/0: 'a'}.exists(k, k == 1) == {1/0: 'a'}.exists(k, k == 1)"), + // TODO: Handle nanos as well (proto) + TIMESTAMP_INT_CONVERSION_OUT_OF_BOUNDS( + "timestamp(999999999999999) == timestamp(999999999999999)"), + TIMESTAMP_INT_CONVERSION_UNDERFLOW( + "timestamp(-999999999999999) == timestamp(-999999999999999)"); final String expr; @@ -1075,6 +1100,58 @@ public void verifyEquivalence_infinityConstants_notEquivalent() throws Exception private enum IsAlwaysTrueViolationTestCase { NOT_ALWAYS_TRUE( "x > 5", "Condition is not always true\\.", "Counterexample input:", "x = -?\\d+"), + UNINTERPRETED_CONVERSION_CAN_ERROR_INT( + "int(request) == int(request)", + "Condition is not always true\\.", + "Counterexample input:", + "request = .*"), + UNINTERPRETED_CONVERSION_CAN_ERROR_UINT( + "uint(request) == uint(request)", + "Condition is not always true\\.", + "Counterexample input:", + "request = .*"), + UNINTERPRETED_CONVERSION_CAN_ERROR_DOUBLE( + "double(request) == double(request)", + "Condition is not always true\\.", + "Counterexample input:", + "request = .*"), + UNINTERPRETED_CONVERSION_CAN_ERROR_TIMESTAMP( + "timestamp(request) == timestamp(request)", + "Condition is not always true\\.", + "Counterexample input:", + "request = .*"), + UNINTERPRETED_CONVERSION_CAN_ERROR_BOOL( + "bool(request) == bool(request)", + "Condition is not always true\\.", + "Counterexample input:", + "request = .*"), + UNINTERPRETED_CONVERSION_CAN_ERROR_STRING( + "string(request) == string(request)", + "Condition is not always true\\.", + "Counterexample input:", + "request = .*"), + UNINTERPRETED_CONVERSION_CAN_ERROR_BYTES( + "bytes(request) == bytes(request)", + "Condition is not always true\\.", + "Counterexample input:", + "request = .*"), + UNINTERPRETED_CONVERSION_CAN_ERROR_DURATION( + "duration(request) == duration(request)", + "Condition is not always true\\.", + "Counterexample input:", + "request = .*"), + DURATION_VARIABLE_COUNTEREXAMPLE( + "dur != dur", + "Condition is not always true\\.", + "Counterexample input:", + "dur = duration\\(-?\\d+\\)"), + TIMESTAMP_VARIABLE_COUNTEREXAMPLE( + "ts != ts", + "Condition is not always true\\.", + "Counterexample input:", + "ts = timestamp\\(-?\\d+\\)"), + UNINTERPRETED_CONVERSION_NULL_FAILS_WITH_ERRORS( + "string(null) == string(null)", "Condition is not always true\\."), LAW_OF_EXCLUDED_MIDDLE_FAILS_WITH_ERRORS( "(1 / 0 == 5) || !(1 / 0 == 5)", "Condition is not always true\\."), INTEGER_OVERFLOW_FAILS_WITH_ERRORS( @@ -1087,6 +1164,12 @@ private enum IsAlwaysTrueViolationTestCase { "Condition is not always true\\.", "Counterexample input:", "u = \\d+u?"), + CROSS_TYPE_DYNAMIC_EQUALITY_NOT_ALWAYS_UNEQUAL_INT_DOUBLE( + "!(request == unknown_var && type(request) == int && type(unknown_var) == double)", + "Condition is not always true\\.", + "Counterexample input:", + "unknown_var = -?\\d+\\.\\d+", + "request = -?\\d+"), NEGATE_MIN_INT_FAILS_WITH_ERRORS( "-(-x) == x", "Condition is not always true\\.", "Counterexample input:", "x = -?\\d+"), HETEROGENEOUS_ARITHMETIC_FAILS("dyn(1) + 1u == 2u", "Condition is not always true\\."), @@ -1102,12 +1185,6 @@ private enum IsAlwaysTrueViolationTestCase { "Counterexample input:", "x = -?\\d+", "u = \\d+u?"), - CROSS_TYPE_DYNAMIC_EQUALITY_NOT_ALWAYS_UNEQUAL_INT_DOUBLE( - "!(request == unknown_var && type(request) == int && type(unknown_var) == double)", - "Condition is not always true\\.", - "Counterexample input:", - "unknown_var = -?\\d+\\.\\d+", - "request = -?\\d+"), OPTIONAL_DYN_VAR_HAS_VALUE_NOT_IMPLIES_INT( "opt_dyn_var.hasValue() ? type(opt_dyn_var.value()) == int : true", "Condition is not always true\\.", @@ -1117,18 +1194,18 @@ private enum IsAlwaysTrueViolationTestCase { "[?dyn_var] == [?dyn_var] ? true : true", "Condition is not always true\\.", "Counterexample input:", - "dyn_var = b\"![01]!\""), + "dyn_var = .*"), OPTIONAL_MAP_ENTRY_DYN_VAR_TYPE_MISMATCH( "{?1: dyn_var} == {?1: dyn_var} ? true : true", "Condition is not always true\\.", "Counterexample input:", - "dyn_var = b\"![01]!\""), + "dyn_var = .*"), OPTIONAL_STRUCT_ENTRY_DYN_VAR_TYPE_MISMATCH( "cel.expr.conformance.proto3.TestAllTypes{?single_int32: dyn_var} ==" + " cel.expr.conformance.proto3.TestAllTypes{?single_int32: dyn_var} ? true : true", "Condition is not always true\\.", "Counterexample input:", - "dyn_var = b\"(!0!|i)\""), + "dyn_var = .*"), OPTIONAL_NONE_COUNTEREXAMPLE( "opt_dyn_var.hasValue()", "Condition is not always true\\.", @@ -1235,7 +1312,7 @@ private enum IsAlwaysTrueViolationTestCase { "dyn_var == 1.0", "Condition is not always true\\.", "Counterexample input:", - "dyn_var = b\"![01]!\""), + "dyn_var = .*"), DYNAMIC_NOT_TYPE_MISMATCH( "!dyn_var", "Condition is not always true\\.", @@ -1245,7 +1322,7 @@ private enum IsAlwaysTrueViolationTestCase { "dyn_var ? true : false", "Condition is not always true\\.", "Counterexample input:", - "dyn_var = b\"![01]!\""), + "dyn_var = .*"), DYNAMIC_NOT_TYPE_MISMATCH_SURVIVOR( "type(dyn_var) == int ? (!dyn_var == !dyn_var) : true", "Condition is not always true\\.", @@ -1317,6 +1394,7 @@ public void isAlwaysTrue_violation_returnsFalse( } private enum IsInconclusiveTestCase { + TIMESTAMP_ADD_DURATION_OVERFLOW("timestamp(253402300799) + duration('100s') > timestamp(0)"), UNINTERPRETED_FUNCTION("request.matches('^[a-z]+$')"), INT_STRING_UNINTERPRETED("int('123') == 123"), LIST_WITH_APPROXIMATE_ELEMENT("[request.matches('a')]"), @@ -1398,7 +1476,18 @@ private enum EquivalenceInconclusiveTestCase { "size(int_list) == 6 ? int_list.map(x, 2.0) : [1.0]"), TRUNCATION_DIVERGENCE_DIFFERENT_BYTES( "size(int_list) == 6 ? int_list.map(x, b'a') : [b'a']", - "size(int_list) == 6 ? int_list.map(x, b'b') : [b'a']"); + "size(int_list) == 6 ? int_list.map(x, b'b') : [b'a']"), + TIMESTAMP_MATH_ADD_DUR_TS("timestamp(100) + duration('100s')", "timestamp(200)"), + TIMESTAMP_MATH_ADD_TS_DUR("duration('100s') + timestamp(100)", "timestamp(200)"), + TIMESTAMP_MATH_SUBTRACT_DUR("timestamp(900000) - duration('100s')", "timestamp(899900)"), + DURATION_MATH_ADD_DUR_DUR("duration('100s') + duration('200s')", "duration('300s')"), + DURATION_MATH_SUBTRACT_DUR_DUR("duration('300s') - duration('100s')", "duration('200s')"), + DURATION_MATH_ASSOCIATIVITY( + "(duration('10s') + duration('20s')) + duration('30s')", + "duration('10s') + (duration('20s') + duration('30s'))"), + TIMESTAMP_DURATION_MATH_ASSOCIATIVITY( + "(timestamp(10) + duration('20s')) + duration('30s')", + "timestamp(10) + (duration('20s') + duration('30s'))"); final String exprA; final String exprB; @@ -1421,6 +1510,7 @@ public void verifyEquivalence_inconclusive( } private enum EquivalenceTestCase { + STRUCT_UNSET_TIMESTAMP_DEFAULT("TestAllTypes{}.single_timestamp", "timestamp(0)"), TRUNCATION_STRICT_PROPAGATION_EQUIVALENT( "size(int_list) == 6 ? size(int_list.filter(x, x > 2)) : 0", "size(int_list) == 6 ? size(int_list.filter(y, y > 2)) : 0"), @@ -1438,6 +1528,12 @@ private enum EquivalenceTestCase { MACRO_EXISTS_ONE_EQUIVALENT( "[1, 2, 3].exists_one(x, x == 2)", "(1 == 2 ? 1 : 0) + (2 == 2 ? 1 : 0) + (3 == 2 ? 1 : 0) == 1"), + TIMESTAMP_MATH_SUBTRACT_TS( + "timestamp(900000) - timestamp(100)", "timestamp(899900) - timestamp(0)"), + TIMESTAMP_MATH_COMMUTATIVITY( + "duration('10s') + timestamp(50)", "timestamp(50) + duration('10s')"), + DURATION_MATH_COMMUTATIVITY( + "duration('10s') + duration('20s')", "duration('20s') + duration('10s')"), MACRO_MAP_EQUIVALENT("{1: true, 2: true, 3: true}.all(k, k > 0)", "1 > 0 && 2 > 0 && 3 > 0"), MACRO_BIND_EQUIVALENT("cel.bind(x, 10, x > 0)", "10 > 0"), NESTED_MACRO( From 1dccf2f5c63c36054faa09cfefc2a5446d93ca7c Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Fri, 31 Jul 2026 17:58:15 -0700 Subject: [PATCH 154/204] Add type constraints to uninterpreted conversions PiperOrigin-RevId: 957417703 --- .../verifier/axioms/TypeConversionAxioms.java | 60 +++++++++---------- .../cel/verifier/CelVerifierZ3ImplTest.java | 53 ++++++++-------- 2 files changed, 53 insertions(+), 60 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java b/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java index fabdd3d9b..feccfd04e 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java @@ -45,11 +45,11 @@ final class TypeConversionAxioms { .addUnaryOverloadTranslator( Conversions.DOUBLE_TO_INT64.celOverloadDecl(), createUninterpretedConversion(Conversions.DOUBLE_TO_INT64), - true) + /* isApproximated= */ true) .addUnaryOverloadTranslator( Conversions.STRING_TO_INT64.celOverloadDecl(), createUninterpretedConversion(Conversions.STRING_TO_INT64), - true) + /* isApproximated= */ true) .addUnaryOverloadTranslator( Conversions.TIMESTAMP_TO_INT64.celOverloadDecl(), (ctx, typeSystem, sink, arg) -> @@ -72,11 +72,11 @@ final class TypeConversionAxioms { .addUnaryOverloadTranslator( Conversions.DOUBLE_TO_UINT64.celOverloadDecl(), createUninterpretedConversion(Conversions.DOUBLE_TO_UINT64), - true) + /* isApproximated= */ true) .addUnaryOverloadTranslator( Conversions.STRING_TO_UINT64.celOverloadDecl(), createUninterpretedConversion(Conversions.STRING_TO_UINT64), - true) + /* isApproximated= */ true) .build(); private static final CelZ3FunctionAxiom DOUBLE_AXIOM = @@ -87,14 +87,15 @@ final class TypeConversionAxioms { .addUnaryOverloadTranslator( Conversions.INT64_TO_DOUBLE.celOverloadDecl(), createUninterpretedConversion(Conversions.INT64_TO_DOUBLE), - true) + /* isApproximated= */ true) .addUnaryOverloadTranslator( Conversions.UINT64_TO_DOUBLE.celOverloadDecl(), createUninterpretedConversion(Conversions.UINT64_TO_DOUBLE), - true) + /* isApproximated= */ true) .addUnaryOverloadTranslator( Conversions.STRING_TO_DOUBLE.celOverloadDecl(), - createUninterpretedConversion(Conversions.STRING_TO_DOUBLE)) + createUninterpretedConversion(Conversions.STRING_TO_DOUBLE), + /* isApproximated= */ true) .build(); private static final CelZ3FunctionAxiom STRING_AXIOM = @@ -105,31 +106,31 @@ final class TypeConversionAxioms { .addUnaryOverloadTranslator( Conversions.INT64_TO_STRING.celOverloadDecl(), createUninterpretedConversion(Conversions.INT64_TO_STRING), - true) + /* isApproximated= */ true) .addUnaryOverloadTranslator( Conversions.UINT64_TO_STRING.celOverloadDecl(), createUninterpretedConversion(Conversions.UINT64_TO_STRING), - true) + /* isApproximated= */ true) .addUnaryOverloadTranslator( Conversions.DOUBLE_TO_STRING.celOverloadDecl(), createUninterpretedConversion(Conversions.DOUBLE_TO_STRING), - true) + /* isApproximated= */ true) .addUnaryOverloadTranslator( Conversions.BOOL_TO_STRING.celOverloadDecl(), createUninterpretedConversion(Conversions.BOOL_TO_STRING), - true) + /* isApproximated= */ true) .addUnaryOverloadTranslator( Conversions.BYTES_TO_STRING.celOverloadDecl(), createUninterpretedConversion(Conversions.BYTES_TO_STRING), - true) + /* isApproximated= */ true) .addUnaryOverloadTranslator( Conversions.TIMESTAMP_TO_STRING.celOverloadDecl(), createUninterpretedConversion(Conversions.TIMESTAMP_TO_STRING), - true) + /* isApproximated= */ true) .addUnaryOverloadTranslator( Conversions.DURATION_TO_STRING.celOverloadDecl(), createUninterpretedConversion(Conversions.DURATION_TO_STRING), - true) + /* isApproximated= */ true) .build(); private static final CelZ3FunctionAxiom BYTES_AXIOM = @@ -140,7 +141,7 @@ final class TypeConversionAxioms { .addUnaryOverloadTranslator( Conversions.STRING_TO_BYTES.celOverloadDecl(), createUninterpretedConversion(Conversions.STRING_TO_BYTES), - true) + /* isApproximated= */ true) .build(); private static final CelZ3FunctionAxiom DYN_AXIOM = @@ -158,7 +159,7 @@ final class TypeConversionAxioms { .addUnaryOverloadTranslator( Conversions.STRING_TO_DURATION.celOverloadDecl(), createUninterpretedConversion(Conversions.STRING_TO_DURATION), - true) + /* isApproximated= */ true) .build(); private static final CelZ3FunctionAxiom TIMESTAMP_AXIOM = @@ -169,7 +170,7 @@ final class TypeConversionAxioms { .addUnaryOverloadTranslator( Conversions.STRING_TO_TIMESTAMP.celOverloadDecl(), createUninterpretedConversion(Conversions.STRING_TO_TIMESTAMP), - true) + /* isApproximated= */ true) .addUnaryOverloadTranslator( Conversions.INT64_TO_TIMESTAMP.celOverloadDecl(), (ctx, typeSystem, sink, arg) -> { @@ -188,7 +189,7 @@ final class TypeConversionAxioms { .addUnaryOverloadTranslator( Conversions.STRING_TO_BOOL.celOverloadDecl(), createUninterpretedConversion(Conversions.STRING_TO_BOOL), - true) + /* isApproximated= */ true) .build(); static final ImmutableList ALL_AXIOMS = @@ -213,49 +214,45 @@ private static CelZ3FunctionAxiom.UnaryTranslator createUninterpretedConversion( typeSystem.celValueSort()); Expr res = ctx.mkApp(funcDecl, arg); + BoolExpr isValid; switch (conversion.celOverloadDecl().resultType().kind()) { case INT: - BoolExpr intValid = + isValid = ctx.mkAnd( typeSystem.isInt(res), ctx.mkNot(typeSystem.checkIntOverflow(typeSystem.getInt(res)))); - sink.accept(intValid); break; case TIMESTAMP: - BoolExpr timestampValid = + isValid = ctx.mkAnd( typeSystem.isTimestamp(res), ctx.mkNot(typeSystem.checkTimestampOverflow(typeSystem.getTimestamp(res)))); - sink.accept(timestampValid); break; case DURATION: - BoolExpr durationValid = + isValid = ctx.mkAnd( typeSystem.isDuration(res), ctx.mkNot(typeSystem.checkDurationOverflow(typeSystem.getDuration(res)))); - sink.accept(durationValid); break; case UINT: - BoolExpr uintValid = + isValid = ctx.mkAnd( typeSystem.isUint(res), ctx.mkNot(typeSystem.checkUintOverflow(typeSystem.getUint(res)))); - sink.accept(uintValid); break; case DOUBLE: - BoolExpr doubleValid = + isValid = ctx.mkAnd( typeSystem.isDouble(res), ctx.mkNot(ctx.mkFPIsNaN(typeSystem.getDouble(res)))); - sink.accept(doubleValid); break; case STRING: - sink.accept(typeSystem.isString(res)); + isValid = typeSystem.isString(res); break; case BYTES: - sink.accept(typeSystem.isBytes(res)); + isValid = typeSystem.isBytes(res); break; case BOOL: - sink.accept(typeSystem.isBool(res)); + isValid = typeSystem.isBool(res); break; default: throw new IllegalArgumentException( @@ -263,6 +260,7 @@ private static CelZ3FunctionAxiom.UnaryTranslator createUninterpretedConversion( + conversion.celOverloadDecl().resultType()); } + sink.accept(ctx.mkOr(isValid, typeSystem.isError(res))); return Optional.of(res); }; } diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index 8759422c3..446f2776c 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -93,6 +93,7 @@ public final class CelVerifierZ3ImplTest { .addVar("b", SimpleType.BOOL) .addVar("role", SimpleType.STRING) .addVar("country", SimpleType.STRING) + .addVar("string_var", SimpleType.STRING) .addVar("port", SimpleType.INT) .addVar("dur", SimpleType.DURATION) .addVar("ts", SimpleType.TIMESTAMP) @@ -325,13 +326,6 @@ public void isSatisfiable_timeout_throwsException() throws Exception { private enum IsAlwaysTrueTestCase { LOGICAL_OR_CONSTANTS("true || false"), - DYNAMIC_EQUALITY_TIMESTAMP_INT_COLLISION( - "type(dyn_var) == int && dyn_var == 0 ? dyn_var != timestamp('1970-01-01T00:00:00Z') :" - + " true"), - TIMESTAMP_STRING_CONVERSION_VALID( - "timestamp('2023-01-01T00:00:00Z') == timestamp('2023-01-01T00:00:00Z')"), - DURATION_STRING_CONVERSION_VALID("duration('100s') == duration('100s')"), - TIMESTAMP_BOUNDS_VALID("timestamp('2023-01-01T00:00:00Z') <= timestamp(253402300799)"), TIMESTAMP_GREATER_EQUALS("timestamp(200) >= timestamp(100)"), DURATION_GREATER_EQUALS( "(timestamp(200) - timestamp(100)) >= (timestamp(150) - timestamp(100))"), @@ -675,29 +669,7 @@ private enum IsAlwaysTrueTestCase { TYPE_CONVERSION_DYN_IDENTITY("dyn(1) == 1"), TYPE_CONVERSION_UINT_TO_INT("int(1u) == 1"), TYPE_CONVERSION_INT_TO_UINT("uint(1) == 1u"), - TYPE_CONVERSION_INT_FROM_DOUBLE("int(1.0) == int(1.0)"), - TYPE_CONVERSION_INT_FROM_STRING("int('1') == int('1')"), - TYPE_CONVERSION_INT_FROM_TIMESTAMP( - "int(timestamp('1970-01-01T00:00:00Z')) == int(timestamp('1970-01-01T00:00:00Z'))"), - TYPE_CONVERSION_UINT_FROM_DOUBLE("uint(1.0) == uint(1.0)"), - TYPE_CONVERSION_UINT_FROM_STRING("uint('1') == uint('1')"), - TYPE_CONVERSION_DOUBLE_FROM_INT("double(1) == double(1)"), - TYPE_CONVERSION_DOUBLE_FROM_UINT("double(1u) == double(1u)"), - TYPE_CONVERSION_DOUBLE_FROM_STRING("double('1.0') == double('1.0')"), - TYPE_CONVERSION_STRING_FROM_INT("string(1) == string(1)"), - TYPE_CONVERSION_STRING_FROM_UINT("string(1u) == string(1u)"), - TYPE_CONVERSION_STRING_FROM_DOUBLE("string(1.0) == string(1.0)"), - TYPE_CONVERSION_STRING_FROM_BOOL("string(true) == string(true)"), - TYPE_CONVERSION_STRING_FROM_BYTES("string(b'foo') == string(b'foo')"), - TYPE_CONVERSION_STRING_FROM_TIMESTAMP( - "string(timestamp('1970-01-01T00:00:00Z')) == string(timestamp('1970-01-01T00:00:00Z'))"), - TYPE_CONVERSION_STRING_FROM_DURATION("string(duration('1s')) == string(duration('1s'))"), - TYPE_CONVERSION_BYTES_FROM_STRING("bytes('foo') == bytes('foo')"), - TYPE_CONVERSION_DURATION_FROM_STRING("duration('1s') == duration('1s')"), - TYPE_CONVERSION_TIMESTAMP_FROM_STRING( - "timestamp('1970-01-01T00:00:00Z') == timestamp('1970-01-01T00:00:00Z')"), TYPE_CONVERSION_TIMESTAMP_FROM_INT("timestamp(1) == timestamp(1)"), - TYPE_CONVERSION_BOOL_FROM_STRING("bool('true') == bool('true')"), TYPE_CONVERSION_INT_TO_UINT_ZERO("uint(0) == 0u"), TYPE_AXIOM_OPTIONAL("type(optional.of(1)) == optional_type"), @@ -1369,6 +1341,24 @@ private enum IsAlwaysTrueViolationTestCase { "Counterexample input:", "x = -9223372036854775808", "y = -1"), + UNINTERPRETED_CONVERSION_CAN_ERROR_INT_FROM_STRING( + "int(string_var) == int(string_var)", + "Condition is not always true\\.", + "Counterexample input:"), + UNINTERPRETED_CONVERSION_CAN_ERROR_TIMESTAMP_FROM_STRING( + "timestamp(string_var) == timestamp(string_var)", + "Condition is not always true\\.", + "Counterexample input:"), + UNINTERPRETED_CONVERSION_CAN_ERROR_DURATION_FROM_STRING( + "duration(string_var) == duration(string_var)", + "Condition is not always true\\.", + "Counterexample input:"), + // TODO: Implement RFC 3339 spec in conversion + TIMESTAMP_STRING_CONVERSION_VALID( + "timestamp('2023-01-01T00:00:00Z') == timestamp('2023-01-01T00:00:00Z')", + "Condition is not always true\\."), + DURATION_STRING_CONVERSION_VALID( + "duration('100s') == duration('100s')", "Condition is not always true\\."), ; final String expr; @@ -1395,6 +1385,11 @@ public void isAlwaysTrue_violation_returnsFalse( private enum IsInconclusiveTestCase { TIMESTAMP_ADD_DURATION_OVERFLOW("timestamp(253402300799) + duration('100s') > timestamp(0)"), + DYNAMIC_EQUALITY_TIMESTAMP_INT_COLLISION( + "type(dyn_var) == int && dyn_var == 0 ? dyn_var != timestamp('1970-01-01T00:00:00Z') :" + + " true"), + // TODO: Implement RFC 3339 spec in conversion + TIMESTAMP_BOUNDS_VALID("timestamp('2023-01-01T00:00:00Z') <= timestamp(253402300799)"), UNINTERPRETED_FUNCTION("request.matches('^[a-z]+$')"), INT_STRING_UNINTERPRETED("int('123') == 123"), LIST_WITH_APPROXIMATE_ELEMENT("[request.matches('a')]"), From 0b9ee2f252e453f001610caf34eaf200856ea40d Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Fri, 31 Jul 2026 18:05:59 -0700 Subject: [PATCH 155/204] Mark uninterpreted conversions with literals as approximate PiperOrigin-RevId: 957420089 --- .../cel/verifier/CelAstToZ3Translator.java | 4 +- .../cel/verifier/CelZ3OperatorTranslator.java | 3 +- .../dev/cel/verifier/CelZ3TypeSystem.java | 30 ++++ .../verifier/axioms/TypeConversionAxioms.java | 153 +++++++++--------- .../cel/verifier/CelVerifierZ3ImplTest.java | 17 +- 5 files changed, 119 insertions(+), 88 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java index 159a66a77..e3bb1bfaf 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java @@ -741,8 +741,10 @@ private TranslatedValue translateCall(CelExpr expr, CelAbstractSyntaxTree ast) { typeConstraints.add(ctx.mkNot(typeSystem.isUnknown(callRes))); typeConstraints.add(ctx.mkNot(typeSystem.isError(callRes))); + boolean isDynamic = ast.getType(exprId).map(SimpleType.DYN::equals).orElse(true); + BoolExpr isApprox = ctx.mkBool(!isDynamic); return TranslatedValue.propagateStrict( - ctx, typeSystem, callRes, Optional.of(expr), ctx.mkTrue(), args); + ctx, typeSystem, callRes, Optional.of(expr), isApprox, args); }); } diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java index 59795c6c5..3051fbd87 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java @@ -594,7 +594,8 @@ private TranslatedValue translateEquality( // because X == X is a tautology (or propagates errors/unknowns exactly). if (z3Arg0.equals(z3Arg1)) { Expr finalResult = typeSystem.propagateErrorAndUnknown(equalityExpr, z3Arg0); - return TranslatedValue.create(finalResult, typeSystem, ctx.mkFalse()); + return TranslatedValue.create( + finalResult, typeSystem, ctx.mkOr(arg0.isApproximate(), arg1.isApproximate())); } return TranslatedValue.propagateStrict(ctx, typeSystem, equalityExpr, arg0, arg1) diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java index 5c0b87fbe..e9a1872c9 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java @@ -26,6 +26,7 @@ import com.microsoft.z3.DatatypeSort; import com.microsoft.z3.Expr; import com.microsoft.z3.FPExpr; +import com.microsoft.z3.FPNum; import com.microsoft.z3.FuncDecl; import com.microsoft.z3.IntExpr; import com.microsoft.z3.SeqExpr; @@ -280,6 +281,35 @@ Constructor optionalCons() { return optionalCons; } + /** + * Checks if the given CelValue expression represents a statically known primitive constant. + * + *

This is useful for determining whether an uninterpreted function's result should be treated + * as an approximation. If the argument is a known constant, any resulting error is an + * approximation (e.g., parsing a literal string). If it's a variable, the error is an exact + * runtime failure. + */ + public boolean isPrimitiveConstant(Expr expr) { + if (!expr.isApp()) { + return false; + } + FuncDecl decl = expr.getFuncDecl(); + if (decl.equals(stringCons.ConstructorDecl()) || decl.equals(bytesCons.ConstructorDecl())) { + return expr.getArgs()[0].isString(); + } else if (decl.equals(intCons.ConstructorDecl()) + || decl.equals(uintCons.ConstructorDecl()) + || decl.equals(timestampCons.ConstructorDecl()) + || decl.equals(durationCons.ConstructorDecl())) { + return expr.getArgs()[0].isNumeral(); + } else if (decl.equals(doubleCons.ConstructorDecl())) { + return expr.getArgs()[0] instanceof FPNum; + } else if (decl.equals(boolCons.ConstructorDecl())) { + Expr inner = expr.getArgs()[0]; + return inner.isTrue() || inner.isFalse(); + } + return false; + } + /** Creates a CelValue containing a boolean. */ public Expr mkBool(boolean val) { return ctx.mkApp(boolCons.ConstructorDecl(), ctx.mkBool(val)); diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java b/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java index feccfd04e..8cd844214 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java @@ -19,11 +19,13 @@ import com.google.common.collect.ImmutableList; import com.microsoft.z3.BoolExpr; import com.microsoft.z3.Expr; +import com.microsoft.z3.FPExpr; import com.microsoft.z3.FuncDecl; import com.microsoft.z3.IntExpr; import com.microsoft.z3.Sort; import dev.cel.checker.CelStandardDeclarations.StandardFunction; import dev.cel.checker.CelStandardDeclarations.StandardFunction.Overload.Conversions; +import dev.cel.common.types.SimpleType; import java.util.Optional; /** Axiomatization for CEL's type conversion functions. */ @@ -42,14 +44,12 @@ final class TypeConversionAxioms { return Optional.of( typeSystem.withRuntimeError(typeSystem.wrapInt(uintVal), outOfBounds)); }) - .addUnaryOverloadTranslator( + .addOverloadTranslator( Conversions.DOUBLE_TO_INT64.celOverloadDecl(), - createUninterpretedConversion(Conversions.DOUBLE_TO_INT64), - /* isApproximated= */ true) - .addUnaryOverloadTranslator( + createUninterpretedConversion(Conversions.DOUBLE_TO_INT64)) + .addOverloadTranslator( Conversions.STRING_TO_INT64.celOverloadDecl(), - createUninterpretedConversion(Conversions.STRING_TO_INT64), - /* isApproximated= */ true) + createUninterpretedConversion(Conversions.STRING_TO_INT64)) .addUnaryOverloadTranslator( Conversions.TIMESTAMP_TO_INT64.celOverloadDecl(), (ctx, typeSystem, sink, arg) -> @@ -69,14 +69,12 @@ final class TypeConversionAxioms { return Optional.of( typeSystem.withRuntimeError(typeSystem.wrapUint(intVal), outOfBounds)); }) - .addUnaryOverloadTranslator( + .addOverloadTranslator( Conversions.DOUBLE_TO_UINT64.celOverloadDecl(), - createUninterpretedConversion(Conversions.DOUBLE_TO_UINT64), - /* isApproximated= */ true) - .addUnaryOverloadTranslator( + createUninterpretedConversion(Conversions.DOUBLE_TO_UINT64)) + .addOverloadTranslator( Conversions.STRING_TO_UINT64.celOverloadDecl(), - createUninterpretedConversion(Conversions.STRING_TO_UINT64), - /* isApproximated= */ true) + createUninterpretedConversion(Conversions.STRING_TO_UINT64)) .build(); private static final CelZ3FunctionAxiom DOUBLE_AXIOM = @@ -84,18 +82,15 @@ final class TypeConversionAxioms { .addUnaryOverloadTranslator( Conversions.DOUBLE_TO_DOUBLE.celOverloadDecl(), (ctx, typeSystem, sink, arg) -> Optional.of(arg)) - .addUnaryOverloadTranslator( + .addOverloadTranslator( Conversions.INT64_TO_DOUBLE.celOverloadDecl(), - createUninterpretedConversion(Conversions.INT64_TO_DOUBLE), - /* isApproximated= */ true) - .addUnaryOverloadTranslator( + createUninterpretedConversion(Conversions.INT64_TO_DOUBLE)) + .addOverloadTranslator( Conversions.UINT64_TO_DOUBLE.celOverloadDecl(), - createUninterpretedConversion(Conversions.UINT64_TO_DOUBLE), - /* isApproximated= */ true) - .addUnaryOverloadTranslator( + createUninterpretedConversion(Conversions.UINT64_TO_DOUBLE)) + .addOverloadTranslator( Conversions.STRING_TO_DOUBLE.celOverloadDecl(), - createUninterpretedConversion(Conversions.STRING_TO_DOUBLE), - /* isApproximated= */ true) + createUninterpretedConversion(Conversions.STRING_TO_DOUBLE)) .build(); private static final CelZ3FunctionAxiom STRING_AXIOM = @@ -103,34 +98,27 @@ final class TypeConversionAxioms { .addUnaryOverloadTranslator( Conversions.STRING_TO_STRING.celOverloadDecl(), (ctx, typeSystem, sink, arg) -> Optional.of(arg)) - .addUnaryOverloadTranslator( + .addOverloadTranslator( Conversions.INT64_TO_STRING.celOverloadDecl(), - createUninterpretedConversion(Conversions.INT64_TO_STRING), - /* isApproximated= */ true) - .addUnaryOverloadTranslator( + createUninterpretedConversion(Conversions.INT64_TO_STRING)) + .addOverloadTranslator( Conversions.UINT64_TO_STRING.celOverloadDecl(), - createUninterpretedConversion(Conversions.UINT64_TO_STRING), - /* isApproximated= */ true) - .addUnaryOverloadTranslator( + createUninterpretedConversion(Conversions.UINT64_TO_STRING)) + .addOverloadTranslator( Conversions.DOUBLE_TO_STRING.celOverloadDecl(), - createUninterpretedConversion(Conversions.DOUBLE_TO_STRING), - /* isApproximated= */ true) - .addUnaryOverloadTranslator( + createUninterpretedConversion(Conversions.DOUBLE_TO_STRING)) + .addOverloadTranslator( Conversions.BOOL_TO_STRING.celOverloadDecl(), - createUninterpretedConversion(Conversions.BOOL_TO_STRING), - /* isApproximated= */ true) - .addUnaryOverloadTranslator( + createUninterpretedConversion(Conversions.BOOL_TO_STRING)) + .addOverloadTranslator( Conversions.BYTES_TO_STRING.celOverloadDecl(), - createUninterpretedConversion(Conversions.BYTES_TO_STRING), - /* isApproximated= */ true) - .addUnaryOverloadTranslator( + createUninterpretedConversion(Conversions.BYTES_TO_STRING)) + .addOverloadTranslator( Conversions.TIMESTAMP_TO_STRING.celOverloadDecl(), - createUninterpretedConversion(Conversions.TIMESTAMP_TO_STRING), - /* isApproximated= */ true) - .addUnaryOverloadTranslator( + createUninterpretedConversion(Conversions.TIMESTAMP_TO_STRING)) + .addOverloadTranslator( Conversions.DURATION_TO_STRING.celOverloadDecl(), - createUninterpretedConversion(Conversions.DURATION_TO_STRING), - /* isApproximated= */ true) + createUninterpretedConversion(Conversions.DURATION_TO_STRING)) .build(); private static final CelZ3FunctionAxiom BYTES_AXIOM = @@ -138,10 +126,9 @@ final class TypeConversionAxioms { .addUnaryOverloadTranslator( Conversions.BYTES_TO_BYTES.celOverloadDecl(), (ctx, typeSystem, sink, arg) -> Optional.of(arg)) - .addUnaryOverloadTranslator( + .addOverloadTranslator( Conversions.STRING_TO_BYTES.celOverloadDecl(), - createUninterpretedConversion(Conversions.STRING_TO_BYTES), - /* isApproximated= */ true) + createUninterpretedConversion(Conversions.STRING_TO_BYTES)) .build(); private static final CelZ3FunctionAxiom DYN_AXIOM = @@ -156,10 +143,9 @@ final class TypeConversionAxioms { .addUnaryOverloadTranslator( Conversions.DURATION_TO_DURATION.celOverloadDecl(), (ctx, typeSystem, sink, arg) -> Optional.of(arg)) - .addUnaryOverloadTranslator( + .addOverloadTranslator( Conversions.STRING_TO_DURATION.celOverloadDecl(), - createUninterpretedConversion(Conversions.STRING_TO_DURATION), - /* isApproximated= */ true) + createUninterpretedConversion(Conversions.STRING_TO_DURATION)) .build(); private static final CelZ3FunctionAxiom TIMESTAMP_AXIOM = @@ -167,10 +153,9 @@ final class TypeConversionAxioms { .addUnaryOverloadTranslator( Conversions.TIMESTAMP_TO_TIMESTAMP.celOverloadDecl(), (ctx, typeSystem, sink, arg) -> Optional.of(arg)) - .addUnaryOverloadTranslator( + .addOverloadTranslator( Conversions.STRING_TO_TIMESTAMP.celOverloadDecl(), - createUninterpretedConversion(Conversions.STRING_TO_TIMESTAMP), - /* isApproximated= */ true) + createUninterpretedConversion(Conversions.STRING_TO_TIMESTAMP)) .addUnaryOverloadTranslator( Conversions.INT64_TO_TIMESTAMP.celOverloadDecl(), (ctx, typeSystem, sink, arg) -> { @@ -186,10 +171,9 @@ final class TypeConversionAxioms { .addUnaryOverloadTranslator( Conversions.BOOL_TO_BOOL.celOverloadDecl(), (ctx, typeSystem, sink, arg) -> Optional.of(arg)) - .addUnaryOverloadTranslator( + .addOverloadTranslator( Conversions.STRING_TO_BOOL.celOverloadDecl(), - createUninterpretedConversion(Conversions.STRING_TO_BOOL), - /* isApproximated= */ true) + createUninterpretedConversion(Conversions.STRING_TO_BOOL)) .build(); static final ImmutableList ALL_AXIOMS = @@ -204,9 +188,11 @@ final class TypeConversionAxioms { TIMESTAMP_AXIOM, BOOL_AXIOM); - private static CelZ3FunctionAxiom.UnaryTranslator createUninterpretedConversion( - Conversions conversion) { - return (ctx, typeSystem, sink, arg) -> { + private static CelZ3OverloadTranslator createUninterpretedConversion(Conversions conversion) { + return (ctx, typeSystem, sink, unwrappedArgs, argApproximations) -> { + Expr arg = unwrappedArgs.get(0); + BoolExpr baseApprox = argApproximations.get(0); + FuncDecl funcDecl = typeSystem.internFuncDecl( conversion.celOverloadDecl().overloadId(), @@ -214,45 +200,50 @@ private static CelZ3FunctionAxiom.UnaryTranslator createUninterpretedConversion( typeSystem.celValueSort()); Expr res = ctx.mkApp(funcDecl, arg); - BoolExpr isValid; switch (conversion.celOverloadDecl().resultType().kind()) { case INT: - isValid = - ctx.mkAnd( + sink.accept(ctx.mkOr(typeSystem.isInt(res), typeSystem.isError(res))); + sink.accept( + ctx.mkImplies( typeSystem.isInt(res), - ctx.mkNot(typeSystem.checkIntOverflow(typeSystem.getInt(res)))); + ctx.mkNot(typeSystem.checkIntOverflow(typeSystem.getInt(res))))); break; case TIMESTAMP: - isValid = - ctx.mkAnd( + sink.accept(ctx.mkOr(typeSystem.isTimestamp(res), typeSystem.isError(res))); + sink.accept( + ctx.mkImplies( typeSystem.isTimestamp(res), - ctx.mkNot(typeSystem.checkTimestampOverflow(typeSystem.getTimestamp(res)))); + ctx.mkNot(typeSystem.checkTimestampOverflow(typeSystem.getTimestamp(res))))); break; case DURATION: - isValid = - ctx.mkAnd( + sink.accept(ctx.mkOr(typeSystem.isDuration(res), typeSystem.isError(res))); + sink.accept( + ctx.mkImplies( typeSystem.isDuration(res), - ctx.mkNot(typeSystem.checkDurationOverflow(typeSystem.getDuration(res)))); + ctx.mkNot(typeSystem.checkDurationOverflow(typeSystem.getDuration(res))))); break; case UINT: - isValid = - ctx.mkAnd( + sink.accept(ctx.mkOr(typeSystem.isUint(res), typeSystem.isError(res))); + sink.accept( + ctx.mkImplies( typeSystem.isUint(res), - ctx.mkNot(typeSystem.checkUintOverflow(typeSystem.getUint(res)))); + ctx.mkNot(typeSystem.checkUintOverflow(typeSystem.getUint(res))))); break; case DOUBLE: - isValid = - ctx.mkAnd( - typeSystem.isDouble(res), ctx.mkNot(ctx.mkFPIsNaN(typeSystem.getDouble(res)))); + sink.accept(ctx.mkOr(typeSystem.isDouble(res), typeSystem.isError(res))); + sink.accept( + ctx.mkImplies( + typeSystem.isDouble(res), + ctx.mkNot(ctx.mkFPIsNaN((FPExpr) typeSystem.getDouble(res))))); break; case STRING: - isValid = typeSystem.isString(res); + sink.accept(ctx.mkOr(typeSystem.isString(res), typeSystem.isError(res))); break; case BYTES: - isValid = typeSystem.isBytes(res); + sink.accept(ctx.mkOr(typeSystem.isBytes(res), typeSystem.isError(res))); break; case BOOL: - isValid = typeSystem.isBool(res); + sink.accept(ctx.mkOr(typeSystem.isBool(res), typeSystem.isError(res))); break; default: throw new IllegalArgumentException( @@ -260,8 +251,14 @@ private static CelZ3FunctionAxiom.UnaryTranslator createUninterpretedConversion( + conversion.celOverloadDecl().resultType()); } - sink.accept(ctx.mkOr(isValid, typeSystem.isError(res))); - return Optional.of(res); + boolean isArgConstant = typeSystem.isPrimitiveConstant(arg); + boolean isStringParseConversion = + conversion.celOverloadDecl().parameterTypes().get(0).equals(SimpleType.STRING); + + BoolExpr finalApprox = + (!isArgConstant && isStringParseConversion) ? baseApprox : ctx.mkTrue(); + + return Optional.of(CelZ3OverloadResult.create(res, finalApprox)); }; } diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index 446f2776c..e8783af86 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -154,8 +154,6 @@ private enum IsSatisfiableTestCase { NULL_SATISFIABLE("unknown_var == null"), DYNAMIC_VAR_NUMERIC_EQUALITY("dyn_var == 1 && dyn_var == 1.0"), DYNAMIC_VAR_NOT_IN_LIST("dyn_var == 1.5 && !(dyn_var in dyn_list) && size(dyn_list) > 5"), - TIMESTAMP_EQUALITY_TAUTOLOGY( - "timestamp('2023-01-01T00:00:00Z') == timestamp('2023-01-01T00:00:00Z')"), CROSS_NUMERIC_EQUALITY_INT_DYN_EXACT("1 == request"), MACRO_LIMIT("dyn_list.all(x, x == 1)"), STRUCT_FIELD_MISSING_APPROXIMATE_SATISFIABLE("dyn_var.unknown_field"), @@ -1353,12 +1351,10 @@ private enum IsAlwaysTrueViolationTestCase { "duration(string_var) == duration(string_var)", "Condition is not always true\\.", "Counterexample input:"), - // TODO: Implement RFC 3339 spec in conversion - TIMESTAMP_STRING_CONVERSION_VALID( - "timestamp('2023-01-01T00:00:00Z') == timestamp('2023-01-01T00:00:00Z')", - "Condition is not always true\\."), - DURATION_STRING_CONVERSION_VALID( - "duration('100s') == duration('100s')", "Condition is not always true\\."), + UNINTERPRETED_CONVERSION_CAN_ERROR_BOOL_FROM_STRING( + "bool(string_var) == bool(string_var)", + "Condition is not always true\\.", + "Counterexample input:"), ; final String expr; @@ -1384,6 +1380,11 @@ public void isAlwaysTrue_violation_returnsFalse( } private enum IsInconclusiveTestCase { + // TODO: Implement RFC 3339 spec in conversion + TIMESTAMP_STRING_CONVERSION_VALID( + "timestamp('2023-01-01T00:00:00Z') == timestamp('2023-01-01T00:00:00Z')"), + DURATION_STRING_CONVERSION_VALID("duration('100s') == duration('100s')"), + BOOL_STRING_UNINTERPRETED("bool('true') == true"), TIMESTAMP_ADD_DURATION_OVERFLOW("timestamp(253402300799) + duration('100s') > timestamp(0)"), DYNAMIC_EQUALITY_TIMESTAMP_INT_COLLISION( "type(dyn_var) == int && dyn_var == 0 ? dyn_var != timestamp('1970-01-01T00:00:00Z') :" From e0e717bde8d7388aaf55f03183cf9e4a28c9a871 Mon Sep 17 00:00:00 2001 From: Stanley Cheung Date: Sun, 2 Aug 2026 07:06:27 -0700 Subject: [PATCH 156/204] Handle AccumulatedUnknowns in execution plan. PiperOrigin-RevId: 957942761 --- .../java/dev/cel/runtime/planner/BUILD.bazel | 2 + .../dev/cel/runtime/planner/EvalBinary.java | 21 +- .../dev/cel/runtime/planner/EvalFold.java | 20 +- .../dev/cel/runtime/planner/EvalUnary.java | 9 +- .../cel/runtime/planner/EvalVarArgsCall.java | 11 +- .../runtime/planner/ProgramPlannerTest.java | 179 ++++++++++++++++++ 6 files changed, 219 insertions(+), 23 deletions(-) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index 67a06ffb5..bdac6c95a 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -446,6 +446,7 @@ java_library( ":planned_interpretable", "//common/ast", "//common/values", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", "//runtime:resolved_overload", @@ -957,6 +958,7 @@ cel_android_library( "//runtime:evaluation_exception", "//runtime:interpretable_android", "//runtime:resolved_overload_android", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", ], ) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java index fcade7789..1713195ab 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java @@ -34,20 +34,17 @@ final class EvalBinary extends PlannedInterpretable { @Override Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + boolean isStrict = resolvedOverload.isStrict(); Object argVal1 = - resolvedOverload.isStrict() - ? evalStrictly(arg1, resolver, frame) - : evalNonstrictly(arg1, resolver, frame); + isStrict ? evalStrictly(arg1, resolver, frame) : evalNonstrictly(arg1, resolver, frame); Object argVal2 = - resolvedOverload.isStrict() - ? evalStrictly(arg2, resolver, frame) - : evalNonstrictly(arg2, resolver, frame); - - AccumulatedUnknowns unknowns = AccumulatedUnknowns.maybeMerge(null, argVal1); - unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVal2); - - if (unknowns != null) { - return unknowns; + isStrict ? evalStrictly(arg2, resolver, frame) : evalNonstrictly(arg2, resolver, frame); + if (isStrict) { + AccumulatedUnknowns unknowns = AccumulatedUnknowns.maybeMerge(null, argVal1); + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVal2); + if (unknowns != null) { + return unknowns; + } } return EvalHelpers.dispatch( diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java index 2de52e982..1cbe807c2 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java @@ -105,7 +105,15 @@ private Object evalMap(Map iterRange, Folder folder, ExecutionFrame frame) folder.iterVar2Val = entry.getValue(); } - boolean cond = (boolean) condition.eval(folder, frame); + Object condResult = condition.eval(folder, frame); + if (condResult instanceof AccumulatedUnknowns) { + return condResult; + } + if (!(condResult instanceof Boolean)) { + throw new IllegalArgumentException( + String.format("Expected boolean value, found :%s", condResult)); + } + boolean cond = (boolean) condResult; if (!cond) { folder.computeResult = true; return result.eval(folder, frame); @@ -131,7 +139,15 @@ private Object evalList(Collection iterRange, Folder folder, ExecutionFrame f folder.iterVar2Val = item; } - boolean cond = (boolean) condition.eval(folder, frame); + Object condResult = condition.eval(folder, frame); + if (condResult instanceof AccumulatedUnknowns) { + return condResult; + } + if (!(condResult instanceof Boolean)) { + throw new IllegalArgumentException( + String.format("Expected boolean value, found :%s", condResult)); + } + boolean cond = (boolean) condResult; if (!cond) { folder.computeResult = true; return maybeUnwrapAccumulator(result.eval(folder, frame)); diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java index 867371ff1..a612da9e5 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java @@ -19,6 +19,7 @@ import dev.cel.common.ast.CelExpr; import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.GlobalResolver; @@ -32,10 +33,12 @@ final class EvalUnary extends PlannedInterpretable { @Override Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + boolean isStrict = resolvedOverload.isStrict(); Object argVal = - resolvedOverload.isStrict() - ? evalStrictly(arg, resolver, frame) - : evalNonstrictly(arg, resolver, frame); + isStrict ? evalStrictly(arg, resolver, frame) : evalNonstrictly(arg, resolver, frame); + if (isStrict && argVal instanceof AccumulatedUnknowns) { + return argVal; + } return EvalHelpers.dispatch(functionName, resolvedOverload, celValueConverter, argVal); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java index 4b0171b8f..8046710e9 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java @@ -36,18 +36,17 @@ final class EvalVarArgsCall extends PlannedInterpretable { @Override Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + boolean isStrict = resolvedOverload.isStrict(); Object[] argVals = new Object[args.length]; AccumulatedUnknowns unknowns = null; for (int i = 0; i < args.length; i++) { PlannedInterpretable arg = args[i]; argVals[i] = - resolvedOverload.isStrict() - ? evalStrictly(arg, resolver, frame) - : evalNonstrictly(arg, resolver, frame); - - unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVals[i]); + isStrict ? evalStrictly(arg, resolver, frame) : evalNonstrictly(arg, resolver, frame); + if (isStrict) { + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVals[i]); + } } - if (unknowns != null) { return unknowns; } diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index c749028ff..57fa72162 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -35,6 +35,7 @@ import dev.cel.common.CelErrorCode; import dev.cel.common.CelOptions; import dev.cel.common.CelSource; +import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; import dev.cel.common.exceptions.CelDivideByZeroException; import dev.cel.common.internal.CelDescriptorPool; @@ -210,6 +211,20 @@ private static DefaultDispatcher newDispatcher() { CelFunctionBinding.from("neg_int", Long.class, arg -> -arg), CelFunctionBinding.from("neg_double", Double.class, arg -> -arg))); + addBindingsToDispatcher( + builder, + CelFunctionBinding.fromOverloads( + "add", CelFunctionBinding.from("add_int", Long.class, Long.class, (a, b) -> a + b))); + + addBindingsToDispatcher( + builder, + CelFunctionBinding.fromOverloads( + "func", + CelFunctionBinding.from( + "func_int", + ImmutableList.of(Long.class, Long.class, Long.class), + (args) -> (long) args.length))); + addBindingsToDispatcher( builder, CelFunctionBinding.fromOverloads( @@ -977,6 +992,170 @@ public void plan_partialEval_withWildcardQualification() throws Exception { ImmutableSet.of(2L, 5L, 7L))); } + @Test + public void plan_unaryFunction_withUnknownArg() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("unk", SimpleType.INT) + .addFunctionDeclarations( + newFunctionDeclaration( + "neg", newGlobalOverload("neg_int", SimpleType.INT, SimpleType.INT))) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "neg(unk)"); + + Program program = PLANNER.plan(ast); + + CelUnknownSet result = + (CelUnknownSet) program.eval(PartialVars.of(CelAttributePattern.create("unk"))); + assertThat(result) + .isEqualTo( + CelUnknownSet.create(ImmutableSet.of(CelAttribute.create("unk")), ImmutableSet.of(2L))); + } + + @Test + public void plan_fold_withUnknownCondition() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addVar("unk", SimpleType.BOOL) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "[1, 2].all(x, unk)"); + + Program program = PLANNER.plan(ast); + + CelUnknownSet result = + (CelUnknownSet) program.eval(PartialVars.of(CelAttributePattern.create("unk"))); + assertThat(result) + .isEqualTo( + CelUnknownSet.create(ImmutableSet.of(CelAttribute.create("unk")), ImmutableSet.of(6L))); + } + + @Test + public void plan_foldMap_withUnknownCondition() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addVar("unk", SimpleType.BOOL) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "{\"a\": 1, \"b\": 2}.exists(k, unk)"); + + Program program = PLANNER.plan(ast); + + CelUnknownSet result = + (CelUnknownSet) program.eval(PartialVars.of(CelAttributePattern.create("unk"))); + assertThat(result) + .isEqualTo( + CelUnknownSet.create( + ImmutableSet.of(CelAttribute.create("unk")), ImmutableSet.of(10L))); + } + + @Test + public void plan_foldList_withUnknownLoopCondition_earlyReturn() throws Exception { + CelExpr comprehensionExpr = + CelExpr.ofComprehension( + 1L, + "x", + "", + CelExpr.ofList( + 2L, + ImmutableList.of(CelExpr.ofConstant(3L, CelConstant.ofValue(1L))), + ImmutableList.of()), + "acc", + CelExpr.ofConstant(4L, CelConstant.ofValue(true)), + CelExpr.ofIdent(5L, "unk"), + CelExpr.ofIdent(6L, "acc"), + CelExpr.ofIdent(7L, "acc")); + CelAbstractSyntaxTree ast = + CelAbstractSyntaxTree.newParsedAst(comprehensionExpr, CelSource.newBuilder().build()); + + Program program = PLANNER.plan(ast); + + CelUnknownSet result = + (CelUnknownSet) program.eval(PartialVars.of(CelAttributePattern.create("unk"))); + assertThat(result) + .isEqualTo( + CelUnknownSet.create(ImmutableSet.of(CelAttribute.create("unk")), ImmutableSet.of(5L))); + } + + @Test + public void plan_foldMap_withUnknownLoopCondition_earlyReturn() throws Exception { + CelExpr comprehensionExpr = + CelExpr.ofComprehension( + 1L, + "k", + "", + CelExpr.ofMap( + 2L, + ImmutableList.of( + CelExpr.ofMapEntry( + 3L, + CelExpr.ofConstant(4L, CelConstant.ofValue("a")), + CelExpr.ofConstant(5L, CelConstant.ofValue(1L)), + false))), + "acc", + CelExpr.ofConstant(6L, CelConstant.ofValue(true)), + CelExpr.ofIdent(7L, "unk"), + CelExpr.ofIdent(8L, "acc"), + CelExpr.ofIdent(9L, "acc")); + CelAbstractSyntaxTree ast = + CelAbstractSyntaxTree.newParsedAst(comprehensionExpr, CelSource.newBuilder().build()); + + Program program = PLANNER.plan(ast); + + CelUnknownSet result = + (CelUnknownSet) program.eval(PartialVars.of(CelAttributePattern.create("unk"))); + assertThat(result) + .isEqualTo( + CelUnknownSet.create(ImmutableSet.of(CelAttribute.create("unk")), ImmutableSet.of(7L))); + } + + @Test + public void plan_binaryFunction_withUnknownArg() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("unk", SimpleType.INT) + .addFunctionDeclarations( + newFunctionDeclaration( + "add", + newGlobalOverload("add_int", SimpleType.INT, SimpleType.INT, SimpleType.INT))) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "add(1, unk)"); + + Program program = PLANNER.plan(ast); + + CelUnknownSet result = + (CelUnknownSet) program.eval(PartialVars.of(CelAttributePattern.create("unk"))); + assertThat(result) + .isEqualTo( + CelUnknownSet.create(ImmutableSet.of(CelAttribute.create("unk")), ImmutableSet.of(3L))); + } + + @Test + public void plan_varargsFunction_withUnknownArg() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("unk", SimpleType.INT) + .addFunctionDeclarations( + newFunctionDeclaration( + "func", + newGlobalOverload( + "func_int", + SimpleType.INT, + SimpleType.INT, + SimpleType.INT, + SimpleType.INT))) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "func(1, 2, unk)"); + + Program program = PLANNER.plan(ast); + + CelUnknownSet result = + (CelUnknownSet) program.eval(PartialVars.of(CelAttributePattern.create("unk"))); + assertThat(result) + .isEqualTo( + CelUnknownSet.create(ImmutableSet.of(CelAttribute.create("unk")), ImmutableSet.of(4L))); + } + @Test public void localShadowIdentifier_inSelect() throws Exception { CelCompiler celCompiler = From 989c8c503806c899f3ea02b763739805cec58e38 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Mon, 3 Aug 2026 11:05:08 -0700 Subject: [PATCH 157/204] Prevent ConstantFoldingOptimizer to fold x in [x] for dyn/double typed variables PiperOrigin-RevId: 958471719 --- .../optimizers/ConstantFoldingOptimizer.java | 78 +++++++++++++++-- .../ConstantFoldingOptimizerTest.java | 86 ++++++++++++++++++- .../cel/verifier/CelVerifierZ3ImplTest.java | 4 +- 3 files changed, 156 insertions(+), 12 deletions(-) diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java index b69f5ec52..1cf52bcbe 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java @@ -14,6 +14,7 @@ package dev.cel.optimizer.optimizers; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.MoreCollectors.onlyElement; import static dev.cel.checker.CelStandardDeclarations.StandardFunction.DURATION; @@ -46,6 +47,7 @@ import dev.cel.common.navigation.TraversalOrder; import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.OptionalType; import dev.cel.common.types.SimpleType; import dev.cel.common.types.StructType; import dev.cel.common.values.CelValue; @@ -541,16 +543,37 @@ private Optional maybePruneBranches( CelMutableExpr needle = call.args().get(0); if (needle.getKind().equals(Kind.CONSTANT) || needle.getKind().equals(Kind.IDENT)) { - Object needleValue = - needle.getKind().equals(Kind.CONSTANT) ? needle.constant() : needle.ident(); for (CelMutableExpr elem : haystack.elements()) { - if ((elem.getKind().equals(Kind.CONSTANT) && elem.constant().equals(needleValue)) - || (elem.getKind().equals(Kind.IDENT) && elem.ident().equals(needleValue))) { - return Optional.of( - astMutator.replaceSubtree( - mutableAst.expr(), - CelMutableExpr.ofConstant(CelConstant.ofValue(true)), - expr.id())); + if ((elem.getKind().equals(Kind.CONSTANT) + && needle.getKind().equals(Kind.CONSTANT) + && elem.constant().equals(needle.constant())) + || (elem.getKind().equals(Kind.IDENT) + && needle.getKind().equals(Kind.IDENT) + && elem.ident().equals(needle.ident()))) { + if (needle.getKind().equals(Kind.CONSTANT)) { + if (needle.constant().getKind().equals(CelConstant.Kind.DOUBLE_VALUE) + && Double.isNaN(needle.constant().doubleValue())) { + continue; + } + return Optional.of( + astMutator.replaceSubtree( + mutableAst.expr(), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + expr.id())); + } + + CelType needleType = + mutableAst + .getType(needle.id()) + .orElseGet(() -> identTypes.get(needle.ident().name())); + + if (needleType != null && isSafeForExactEquality(needleType)) { + return Optional.of( + astMutator.replaceSubtree( + mutableAst.expr(), + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), + expr.id())); + } } } } @@ -948,6 +971,43 @@ private static boolean isExprConstantOfKind(CelMutableExpr expr, CelConstant.Kin return expr.getKind().equals(Kind.CONSTANT) && expr.constant().getKind().equals(constantKind); } + private static boolean isSafeForExactEquality(CelType celType) { + switch (celType.kind()) { + case BOOL: + case INT: + case UINT: + case STRING: + case BYTES: + case DURATION: + case TIMESTAMP: + case NULL_TYPE: + case TYPE: + return true; + + case LIST: + return !celType.parameters().isEmpty() + && isSafeForExactEquality(celType.parameters().get(0)); + + case MAP: + return celType.parameters().size() >= 2 + && isSafeForExactEquality(celType.parameters().get(0)) + && isSafeForExactEquality(celType.parameters().get(1)); + + case OPAQUE: + if (celType instanceof OptionalType) { + checkState( + celType.parameters().size() == 1, + "Optional type must have exactly 1 parameter. Found %s", + celType.parameters().size()); + return isSafeForExactEquality(celType.parameters().get(0)); + } + return false; + + default: + return false; + } + } + private ConstantFoldingOptimizer(ConstantFoldingOptions constantFoldingOptions) { this.constantFoldingOptions = constantFoldingOptions; this.astMutator = AstMutator.newInstance(constantFoldingOptions.maxIterationLimit()); diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java index 3b503bc28..613b53ea3 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java @@ -32,6 +32,8 @@ import dev.cel.common.CelOverloadDecl; import dev.cel.common.types.ListType; import dev.cel.common.types.MapType; +import dev.cel.common.types.NullableType; +import dev.cel.common.types.OptionalType; import dev.cel.common.types.SimpleType; import dev.cel.common.types.StructTypeReference; import dev.cel.expr.conformance.proto2.TestAllTypes.NestedMessage; @@ -80,6 +82,28 @@ private static Cel setupEnv(CelBuilder celBuilder) { return celBuilder .addVar("x", SimpleType.DYN) .addVar("y", SimpleType.DYN) + .addVar("dyn_x", SimpleType.DYN) + .addVar("int_x", SimpleType.INT) + .addVar("double_x", SimpleType.DOUBLE) + .addVar("bool_x", SimpleType.BOOL) + .addVar("string_x", SimpleType.STRING) + .addVar("int_list_x", ListType.create(SimpleType.INT)) + .addVar("double_list_x", ListType.create(SimpleType.DOUBLE)) + .addVar("dyn_list_x", ListType.create(SimpleType.DYN)) + .addVar("map_string_int_x", MapType.create(SimpleType.STRING, SimpleType.INT)) + .addVar("map_string_double_x", MapType.create(SimpleType.STRING, SimpleType.DOUBLE)) + .addVar("optional_int_x", OptionalType.create(SimpleType.INT)) + .addVar("optional_double_x", OptionalType.create(SimpleType.DOUBLE)) + .addVar("nested_list_int_x", ListType.create(ListType.create(SimpleType.INT))) + .addVar("nested_list_double_x", ListType.create(ListType.create(SimpleType.DOUBLE))) + .addVar( + "nested_map_list_int_x", + MapType.create(SimpleType.STRING, ListType.create(SimpleType.INT))) + .addVar( + "nested_map_list_double_x", + MapType.create(SimpleType.STRING, ListType.create(SimpleType.DOUBLE))) + .addVar("nullable_int_x", NullableType.create(SimpleType.INT)) + .addVar("nullable_double_x", NullableType.create(SimpleType.DOUBLE)) .addVar("bool_var", SimpleType.BOOL) .addVar("list_var", ListType.create(SimpleType.STRING)) .addVar("map_var", MapType.create(SimpleType.STRING, SimpleType.STRING)) @@ -151,7 +175,48 @@ private static Cel setupEnv(CelBuilder celBuilder) { @TestParameters("{source: '5 in [1, 1 + 2, 1 + (2 + 3)]', expected: 'false'}") @TestParameters("{source: '5 in [1, x, y, 5]', expected: 'true'}") @TestParameters("{source: '!(5 in [1, x, y, 5])', expected: 'false'}") - @TestParameters("{source: 'x in [1, x, y, 5]', expected: 'true'}") + @TestParameters("{source: 'x in [1, x, y, 5]', expected: 'x in [1, x, y, 5]'}") + @TestParameters("{source: 'dyn_x in [1, 2, dyn_x]', expected: 'dyn_x in [1, 2, dyn_x]'}") + @TestParameters("{source: 'int_x in [1, 2, int_x]', expected: 'true'}") + @TestParameters("{source: 'bool_x in [true, false, bool_x]', expected: 'true'}") + @TestParameters("{source: 'string_x in [\"a\", \"b\", string_x]', expected: 'true'}") + @TestParameters( + "{source: 'double_x in [1.0, 2.0, double_x]', expected: 'double_x in [1.0, 2.0, double_x]'}") + @TestParameters("{source: 'int_list_x in [[1], [2], int_list_x]', expected: 'true'}") + @TestParameters( + "{source: 'double_list_x in [[1.0], double_list_x]', expected: 'double_list_x in [[1.0]," + + " double_list_x]'}") + @TestParameters( + "{source: 'dyn_list_x in [[1], dyn_list_x]', expected: 'dyn_list_x in [[1], dyn_list_x]'}") + @TestParameters( + "{source: 'map_string_int_x in [{\"a\": 1}, map_string_int_x]', expected: 'true'}") + @TestParameters( + "{source: 'map_string_double_x in [{\"a\": 1.0}, map_string_double_x]', expected:" + + " 'map_string_double_x in [{\"a\": 1.0}, map_string_double_x]'}") + @TestParameters( + "{source: 'optional_int_x in [optional.of(1), optional_int_x]', expected: 'true'}") + @TestParameters( + "{source: 'optional_double_x in [optional.of(1.0), optional_double_x]', expected:" + + " 'optional_double_x in [optional.of(1.0), optional_double_x]'}") + @TestParameters("{source: 'nullable_int_x in [1, 2, nullable_int_x]', expected: 'true'}") + @TestParameters( + "{source: 'nullable_double_x in [1.0, 2.0, nullable_double_x]', expected:" + + " 'nullable_double_x in [1.0, 2.0, nullable_double_x]'}") + @TestParameters( + "{source: 'double(\"NaN\") in [double(\"NaN\"), double_x]', expected: 'NaN in" + + " [NaN, double_x]'}") + @TestParameters( + "{source: 'nested_list_int_x in [[[1]], [[2]], nested_list_int_x]', expected: 'true'}") + @TestParameters( + "{source: 'nested_list_double_x in [[[1.0]], [[2.0]], nested_list_double_x]'," + + " expected: 'nested_list_double_x in [[[1.0]], [[2.0]], nested_list_double_x]'}") + @TestParameters( + "{source: 'nested_map_list_int_x in [{\"a\": [1]}, nested_map_list_int_x]', expected:" + + " 'true'}") + @TestParameters( + "{source: 'nested_map_list_double_x in [{\"a\": [1.0]}, nested_map_list_double_x]'," + + " expected: 'nested_map_list_double_x in [{\"a\": [1.0]}," + + " nested_map_list_double_x]'}") @TestParameters("{source: 'x in [1, 1 + 2, 1 + (2 + 3)]', expected: 'x in [1, 3, 6]'}") @TestParameters("{source: 'duration(string(7 * 24) + ''h'')', expected: 'duration(\"168h\")'}") @TestParameters("{source: '[1, ?optional.of(3)]', expected: '[1, 3]'}") @@ -395,7 +460,8 @@ public void constantFold_protoMessageLiteral_success(String source, String expec @TestParameters( "{source: 'cel.bind(myMap, {\"foo\": \"bar\"}, myMap[?\"foo\"].optMap(x, x + \"baz\"))', " + "expected: 'optional.of(\"barbaz\")'}") - @TestParameters("{source: '(1 + 2 + 3 == x) && (x in [1, 2, x])', expected: '6 == x'}") + @TestParameters( + "{source: '(1 + 2 + 3 == x) && (x in [1, 2, x])', expected: '6 == x && x in [1, 2, x]'}") public void constantFold_macros_macroCallMetadataPopulated(String source, String expected) throws Exception { Cel cel = @@ -862,4 +928,20 @@ public void iterationLimitReached_throws() throws Exception { assertThrows(CelOptimizationException.class, () -> optimizer.optimize(ast)); assertThat(e).hasMessageThat().contains("Optimization failure: Max iteration count reached."); } + + @Test + public void constantFold_inOperator_withoutMacros_skipsDoubleNan() throws Exception { + Cel celWithoutMacros = + setupEnv(runtimeFlavor.builder()).toCelBuilder().setStandardMacros().build(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(celWithoutMacros) + .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) + .build(); + CelAbstractSyntaxTree ast = + celWithoutMacros.compile("double('NaN') in [double('NaN'), double_x]").getAst(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo("NaN in [NaN, double_x]"); + } } diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index e8783af86..1a41ef743 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -1775,7 +1775,8 @@ private enum EquivalenceTestCase { JSON_VALUE_OPTIONAL_NULL_VALUE_OF("google.protobuf.Value{?null_value: optional.of(0)}", "null"), OPTIONAL_INDEX_LIST_UNWRAPPING("optional.of([1, 2, 3])[?0]", "optional.of(1)"), OPTIONAL_INDEX_MAP_UNWRAPPING("optional.of({'a': 1})[?'a']", "optional.of(1)"), - OPTIONAL_INDEX_UNWRAPPING_NONE("optional.none()[?0]", "optional.none()"); + OPTIONAL_INDEX_UNWRAPPING_NONE("optional.none()[?0]", "optional.none()"), + INT_IN_LIST_IDENTITY_EQUIVALENT("x in [1, 2, x]", "true"); private final String exprA; private final String exprB; @@ -1821,6 +1822,7 @@ private enum EquivalenceViolationTestCase { OPTIONAL_VALUE_VIOLATION("optional.of(x).value()", "y"), LIST_OPTIONAL_ELEMENTS_COLLISION("[1, ?opt_var]", "[1, opt_var]"), CROSS_NUMERIC_EQUALITY_INT_DYN_VIOLATION("1 == request", "false"), + DYN_IN_LIST_NOT_EQUIVALENT_TO_TRUE("dyn_var in [1, 2, dyn_var]", "true"), OPTIONAL_SELECTION_VS_DIRECT_ERROR( "{'a': 1}.?missing_key", "optional.of({'a': 1}.missing_key)"), OPTIONAL_NESTED_NONE_VS_FLAT_NONE("{'a': optional.none()}.?a", "optional.none()"), From ecc7a55787b92716fa5eaa9e694a91917e832035 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Wed, 5 Aug 2026 09:58:42 -0700 Subject: [PATCH 158/204] Prevent error values in map and list counterexamples PiperOrigin-RevId: 959720180 --- .../cel/verifier/CelAstToZ3Translator.java | 69 +++++++++++++---- .../CelZ3CounterexampleGenerator.java | 2 + .../dev/cel/verifier/CelZ3TypeSystem.java | 5 ++ .../cel/verifier/CelVerifierZ3ImplTest.java | 77 +++++++++++++++++++ 4 files changed, 137 insertions(+), 16 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java index e3bb1bfaf..c1a8848e2 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java @@ -1247,9 +1247,10 @@ private BoolExpr createTypeConstraintForType(Expr val, CelType type) { } Expr optRef = typeSystem.getOptionalRef(val); BoolExpr hasValue = typeSystem.optHasValue(optRef); - BoolExpr valConstraint = - createTypeConstraintForType(typeSystem.getOptionalValue(optRef), paramType); - return ctx.mkAnd(isOpt, ctx.mkImplies(hasValue, valConstraint)); + Expr optVal = typeSystem.getOptionalValue(optRef); + BoolExpr optValNotError = ctx.mkNot(typeSystem.isError(optVal)); + BoolExpr valConstraint = createTypeConstraintForType(optVal, paramType); + return ctx.mkAnd(isOpt, ctx.mkImplies(hasValue, ctx.mkAnd(optValNotError, valConstraint))); } if (type.equals(SimpleType.BOOL)) { return (BoolExpr) ctx.mkApp(typeSystem.boolCons().getTesterDecl(), val); @@ -1289,15 +1290,13 @@ private BoolExpr createTypeConstraintForType(Expr val, CelType type) { } if (type instanceof ListType) { - // Lists are explicitly bounded (sequence theory). We're safe in using for-all quantifiers - // here. + // Constrain list elements using bounded unrolling up to comprehensionUnrollLimit rather + // than Z3 forall quantifiers to prevent MBQI quantifier instantiation loops. + // Assert: isList(val) ∧ for all unrolled 0 <= i < length: ¬isError(seq[i]) ∧ + // typeConstraint(seq[i]) BoolExpr isList = typeSystem.isList(val); CelType elemType = ((ListType) type).elemType(); - if (elemType.equals(SimpleType.DYN)) { - return isList; - } - // isList(val) ∧ ∀i. (0 <= i < length) ⇒ elemType(seq[i]) Expr listRef = typeSystem.getListRef(val); SeqExpr seq = typeSystem.getSeq(listRef); Expr length = ctx.mkLength(seq); @@ -1307,20 +1306,58 @@ private BoolExpr createTypeConstraintForType(Expr val, CelType type) { for (int i = 0; i < comprehensionUnrollLimit; i++) { IntExpr idx = ctx.mkInt(i); Expr elem = ctx.mkNth(seq, idx); - BoolExpr elemConstraint = createTypeConstraintForType(elem, elemType); BoolExpr validIndex = ctx.mkLt(idx, length); + // Assert ¬isError(elem) as a domain invariant so Z3 never synthesizes an Error element in + // list(dyn). For concrete types, this is already implied by createTypeConstraintForType. + boundsAndTypes.add(ctx.mkImplies(validIndex, ctx.mkNot(typeSystem.isError(elem)))); + BoolExpr elemConstraint = createTypeConstraintForType(elem, elemType); boundsAndTypes.add(ctx.mkImplies(validIndex, elemConstraint)); - BoolExpr outOfBounds = ctx.mkGe(idx, length); - boundsAndTypes.add(ctx.mkImplies(outOfBounds, ctx.mkEq(elem, typeSystem.mkUnknown()))); } return CelZ3TypeSystem.mkAndFlattened(ctx, boundsAndTypes); } if (type instanceof MapType) { - // Do NOT emit a for-all quantifier over map keys here. - // Doing so forces MBQI into an infinite loop. Structural equivalence of dynamic keys is - // naturally constrained by the primitive key assertions in getStructuralEquality(). - return typeSystem.isMap(val); + // Do NOT emit a for-all quantifier over map keys or values here. + // Doing so forces MBQI into an infinite loop. Instead, constrain keys and values using + // bounded unrolling over the key sequence up to comprehensionUnrollLimit. + // Assert: isMap(val) ∧ for all unrolled 0 <= i < length: isPrimitiveKey(key) ∧ ¬isError(key) + // ∧ (presence(key) ⇒ ¬isError(val) ∧ typeConstraint(val)) + BoolExpr isMap = typeSystem.isMap(val); + MapType mapType = (MapType) type; + CelType keyType = mapType.keyType(); + CelType valType = mapType.valueType(); + + Expr mapRef = typeSystem.getMapRef(val); + SeqExpr seq = typeSystem.getMapKeys(mapRef); + Expr length = ctx.mkLength(seq); + ArrayExpr mapValues = (ArrayExpr) typeSystem.getMapValues(mapRef); + ArrayExpr mapPresence = (ArrayExpr) typeSystem.getMapPresence(mapRef); + + List boundsAndTypes = new ArrayList<>(); + boundsAndTypes.add(isMap); + + for (int i = 0; i < comprehensionUnrollLimit; i++) { + IntExpr idx = ctx.mkInt(i); + Expr key = ctx.mkNth(seq, idx); + BoolExpr validIndex = ctx.mkLt(idx, length); + + BoolExpr isKeyPrim = typeSystem.isPrimitiveKey(key); + BoolExpr keyNotError = ctx.mkNot(typeSystem.isError(key)); + // Assert isKeyPrim ∧ ¬isError(key) so Z3 never synthesizes a non-primitive or Error key in + // map(dyn, ...). For concrete map types, this is already implied by keyType constraints. + boundsAndTypes.add(ctx.mkImplies(validIndex, ctx.mkAnd(isKeyPrim, keyNotError))); + boundsAndTypes.add(ctx.mkImplies(validIndex, createTypeConstraintForType(key, keyType))); + + BoolExpr presence = (BoolExpr) ctx.mkSelect(mapPresence, key); + BoolExpr validEntry = ctx.mkAnd(validIndex, presence); + + Expr mapVal = ctx.mkSelect(mapValues, key); + BoolExpr valNotError = ctx.mkNot(typeSystem.isError(mapVal)); + boundsAndTypes.add(ctx.mkImplies(validEntry, valNotError)); + boundsAndTypes.add(ctx.mkImplies(validEntry, createTypeConstraintForType(mapVal, valType))); + } + + return CelZ3TypeSystem.mkAndFlattened(ctx, boundsAndTypes); } if (type.kind() == CelKind.STRUCT) { return ctx.mkAnd( diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java index 2355d36bf..f52886a42 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java @@ -127,6 +127,8 @@ private static String formatExpr( return "Error"; } else if (decl.equals(typeSystem.unknownCons().ConstructorDecl())) { return "Unknown"; + } else if (decl.equals(typeSystem.nullCons().ConstructorDecl())) { + return "null"; } else if (decl.equals(typeSystem.optionalCons().ConstructorDecl())) { Expr optRef = expr.getArgs()[0]; Expr hasValueExpr = diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java index e9a1872c9..1c1435e3b 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java @@ -685,6 +685,11 @@ public Expr getBytes(Expr val) { return ctx.mkApp(bytesCons.getAccessorDecls()[0], val); } + /** Checks if the given CelValue is a valid primitive map key type. */ + public BoolExpr isPrimitiveKey(Expr val) { + return ctx.mkOr(isBool(val), isInt(val), isUint(val), isString(val), isBytes(val)); + } + /** Checks if the given CelValue is a struct (message). */ public BoolExpr isStruct(Expr val) { return isMessage(val); diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index 1a41ef743..f230714b2 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -197,6 +197,80 @@ public void isSatisfiable_withVariable_returnsSatisfyingModel() throws Exception assertThat(result.message()).containsMatch("x = (?:[6-9]|[1-9]\\d+)"); } + @Test + public void isSatisfiable_mapNoContainerError_returnsSatisfyingModel() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("string_int_map.size() == 1").getAst(); + + CelVerificationResult result = VERIFIER.isSatisfiable(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + assertThat(result.message()).contains("Condition is satisfiable."); + assertThat(result.message()).contains("Satisfying input:"); + assertThat(result.message()).contains("string_int_map = {"); + assertThat(result.message()).doesNotContain("Error"); + } + + @Test + public void isSatisfiable_listNoContainerError_returnsSatisfyingModel() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("dyn_list.size() == 1").getAst(); + + CelVerificationResult result = VERIFIER.isSatisfiable(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + assertThat(result.message()).contains("Condition is satisfiable."); + assertThat(result.message()).contains("Satisfying input:"); + assertThat(result.message()).contains("dyn_list = ["); + assertThat(result.message()).doesNotContain("Error"); + } + + @Test + public void isSatisfiable_dynMapNoContainerError_returnsSatisfyingModel() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("dyn_map.size() == 1").getAst(); + + CelVerificationResult result = VERIFIER.isSatisfiable(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + assertThat(result.message()).contains("Condition is satisfiable."); + assertThat(result.message()).contains("Satisfying input:"); + assertThat(result.message()).contains("dyn_map = {"); + assertThat(result.message()).doesNotContain("Error"); + } + + @Test + public void counterexample_nullValueFormattedAsNull() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("unknown_var == 3u && request == null").getAst(); + + CelVerificationResult result = VERIFIER.isSatisfiable(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + assertThat(result.message()).contains("request = null"); + } + + private enum CounterexampleNeverErrorTestCase { + DYN_LIST_REFLEXIVITY("dyn_list.size() == 1 ? dyn_list[0] == dyn_list[0] : true"), + DYN_MAP_REFLEXIVITY("dyn_map.size() == 1 ? dyn_map[1] == dyn_map[1] : true"), + DYN_LIST_ELEMENT("size(dyn_list) == 1 && dyn_list[0] == 'impossible_value'"), + DYN_MAP_VALUE("size(dyn_map) == 1 && dyn_map['a'] == 'impossible_value'"), + ; + + final String expr; + + CounterexampleNeverErrorTestCase(String expr) { + this.expr = expr; + } + } + + @Test + public void isAlwaysTrue_counterexampleNeverContainsError( + @TestParameter CounterexampleNeverErrorTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(testCase.expr).getAst(); + + CelVerificationResult result = VERIFIER.isAlwaysTrue(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()).doesNotContain("Error"); + } + @Test public void isSatisfiable_unconditional_returnsUnconditionalMessage() throws Exception { CelAbstractSyntaxTree ast = CEL.compile("1 + 1 == 2").getAst(); @@ -747,6 +821,9 @@ private enum IsAlwaysTrueTestCase { UINT64_BOUNDS_ALWAYS_TRUE("u <= 18446744073709551615u && u >= 0u"), MODULO_INT64_MIN_INT_BY_NEG_ONE_ALWAYS_ZERO( "x == -9223372036854775808 && y == -1 ? x % y == 0 : true"), + DYNAMIC_VAR_TYPE_IDENTITY("type(dyn_var) == type(dyn_var)"), + DYNAMIC_MAP_KEY_COMPREHENSION_TYPE_IDENTITY( + "size(dyn_map) > 0 && size(dyn_map) <= 5 ? dyn_map.all(k, type(k) == type(k)) : true"), ; final String expr; From 83f57827da28700c44492d9be67c94eb3630a566 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Wed, 5 Aug 2026 13:13:32 -0700 Subject: [PATCH 159/204] Add CLI for Verifier PiperOrigin-RevId: 959829900 --- verifier/README.md | 4 + .../java/dev/cel/verifier/tools/BUILD.bazel | 77 +++ .../cel/verifier/tools/CelVerifierTool.java | 303 +++++++++ .../verifier/tools/CelVerifierToolCore.java | 161 +++++ .../dev/cel/verifier/tools/FormatUtils.java | 173 ++++++ .../verifier/tools/VerificationOptions.java | 219 +++++++ .../test/java/dev/cel/verifier/BUILD.bazel | 3 +- .../java/dev/cel/verifier/tools/BUILD.bazel | 31 + .../verifier/tools/CelVerifierToolTest.java | 587 ++++++++++++++++++ .../cel/verifier/tools/FormatUtilsTest.java | 118 ++++ .../tools/VerificationOptionsTest.java | 103 +++ verifier/tools/BUILD.bazel | 19 + verifier/tools/README.md | 107 ++++ 13 files changed, 1904 insertions(+), 1 deletion(-) create mode 100644 verifier/src/main/java/dev/cel/verifier/tools/BUILD.bazel create mode 100644 verifier/src/main/java/dev/cel/verifier/tools/CelVerifierTool.java create mode 100644 verifier/src/main/java/dev/cel/verifier/tools/CelVerifierToolCore.java create mode 100644 verifier/src/main/java/dev/cel/verifier/tools/FormatUtils.java create mode 100644 verifier/src/main/java/dev/cel/verifier/tools/VerificationOptions.java create mode 100644 verifier/src/test/java/dev/cel/verifier/tools/BUILD.bazel create mode 100644 verifier/src/test/java/dev/cel/verifier/tools/CelVerifierToolTest.java create mode 100644 verifier/src/test/java/dev/cel/verifier/tools/FormatUtilsTest.java create mode 100644 verifier/src/test/java/dev/cel/verifier/tools/VerificationOptionsTest.java create mode 100644 verifier/tools/BUILD.bazel create mode 100644 verifier/tools/README.md diff --git a/verifier/README.md b/verifier/README.md index f286a4d6f..bd9979390 100644 --- a/verifier/README.md +++ b/verifier/README.md @@ -433,3 +433,7 @@ What this means for verification: default unless you have a specific need and bounded inputs. --- + +## Tools & CLI + +For command-line verification and interactive execution, see the [CLI Tool documentation](tools/README.md). diff --git a/verifier/src/main/java/dev/cel/verifier/tools/BUILD.bazel b/verifier/src/main/java/dev/cel/verifier/tools/BUILD.bazel new file mode 100644 index 000000000..e4339f857 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/tools/BUILD.bazel @@ -0,0 +1,77 @@ +load("@rules_java//java:defs.bzl", "java_binary", "java_library") +load("//publish:cel_version.bzl", "CEL_VERSION") + +package( + default_applicable_licenses = [ + "//:license", + ], + default_visibility = [ + "//verifier:__subpackages__", + ], +) + +genrule( + name = "generate_version", + outs = ["CelVersion.java"], + cmd = """cat << 'EOF' > $@ +package dev.cel.verifier.tools; + +final class CelVersion { + static final String VERSION = "%s"; + + private CelVersion() {} +} +EOF +""" % CEL_VERSION, +) + +java_library( + name = "tools_lib", + srcs = [ + "CelVerifierTool.java", + "CelVerifierToolCore.java", + "FormatUtils.java", + "VerificationOptions.java", + ":generate_version", + ], + tags = [ + "alt_dep=//verifier/tools", + ], + deps = [ + "//bundle:cel", + "//common:cel_ast", + "//common:compiler_common", + "//common:options", + "//common/types", + "//common/types:type_providers", + "//compiler", + "//compiler:compiler_builder", + "//extensions", + "//parser:macro", + "//policy", + "//policy:compiler", + "//policy:compiler_factory", + "//policy:parser", + "//policy:parser_factory", + "//policy:validation_exception", + "//verifier", + "//verifier:policy_verifier", + "//verifier:policy_verifier_factory", + "//verifier:verifier_factory", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:info_picocli_picocli", + ], +) + +java_binary( + name = "cel_verifier_tool", + jvm_flags = ["-Dz3.skipLibraryLoad=true"], + main_class = "dev.cel.verifier.tools.CelVerifierTool", + tags = [ + "alt_dep=//verifier/tools:cel_verifier_tool", + ], + runtime_deps = [ + ":tools_lib", + ], +) diff --git a/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierTool.java b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierTool.java new file mode 100644 index 000000000..8289b9f77 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierTool.java @@ -0,0 +1,303 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.tools; + +import com.google.common.collect.ImmutableMap; +import dev.cel.common.CelValidationException; +import dev.cel.common.types.CelType; +import dev.cel.policy.CelPolicyValidationException; +import dev.cel.verifier.CelVerificationResult; +import dev.cel.verifier.CelVerificationResult.VerificationStatus; +import dev.cel.verifier.tools.VerificationOptions.OutputFormat; +import java.io.File; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.Callable; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.IVersionProvider; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.Spec; + +/** Main Picocli entrypoint for the CEL Formal Verification CLI. */ +@Command( + name = "cel-verifier", + mixinStandardHelpOptions = true, + versionProvider = CelVerifierTool.VersionProvider.class, + description = "CEL-Java Formal Verification CLI Tool", + subcommands = { + CelVerifierTool.CheckSatCommand.class, + CelVerifierTool.CheckValidCommand.class, + CelVerifierTool.VerifyEquivCommand.class, + CelVerifierTool.VerifyPolicyCommand.class + }) +public final class CelVerifierTool implements Runnable { + + static final int EXIT_CODE_VERIFIED = 0; + static final int EXIT_CODE_VIOLATED = 1; + static final int EXIT_CODE_INCONCLUSIVE = 2; + static final int EXIT_CODE_ERROR = 3; + + static final class VersionProvider implements IVersionProvider { + @Override + public String[] getVersion() { + return new String[] {"cel-verifier " + CelVersion.VERSION}; + } + } + + @Spec private CommandSpec spec; + + @Override + public void run() { + spec.commandLine().usage(spec.commandLine().getOut()); + } + + /** Options shared across all verification commands. */ + abstract static class BaseVerificationCommand implements Callable { + + @Spec private CommandSpec spec; + + PrintWriter out() { + return spec != null + ? spec.commandLine().getOut() + : new PrintWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8), true); + } + + PrintWriter err() { + return spec != null + ? spec.commandLine().getErr() + : new PrintWriter(new OutputStreamWriter(System.err, StandardCharsets.UTF_8), true); + } + + @Option( + names = {"--var", "-v"}, + description = + "Declared variable in 'name:type' format (e.g., --var role:string --var port:int)") + List variables = new ArrayList<>(); + + @Option( + names = {"--unknown", "-u"}, + description = + "Identifier to permit evaluating to Unknown (e.g., --unknown request.headers)") + List unknownIdentifiers = new ArrayList<>(); + + @Option( + names = {"--timeout"}, + description = "Solver timeout in seconds (default: 10)") + int timeoutSeconds = (int) VerificationOptions.DEFAULT_TIMEOUT.getSeconds(); + + @Option( + names = {"--unroll-limit"}, + description = "Comprehension unroll limit for BMC (default: 5)") + int comprehensionUnrollLimit = VerificationOptions.DEFAULT_COMPREHENSION_UNROLL_LIMIT; + + @Option( + names = {"--output_format", "-fmt"}, + description = "Output format: TEXT or JSON (default: TEXT)") + String outputFormatStr = VerificationOptions.DEFAULT_OUTPUT_FORMAT.name(); + + @FunctionalInterface + protected interface CommandAction { + int execute(VerificationOptions options, ImmutableMap vars) throws Exception; + } + + protected int executeCommand(CommandAction action) { + return executeCommand("Verification error", action); + } + + protected int executeCommand(String errorPrefix, CommandAction action) { + try { + VerificationOptions options = getOptions(); + ImmutableMap vars = VerificationOptions.parseVariables(variables); + return action.execute(options, vars); + } catch (CelValidationException e) { + err().println("Compilation error:\n" + e.getMessage()); + return EXIT_CODE_ERROR; + } catch (CelPolicyValidationException e) { + err().println("Policy compilation error:\n" + e.getMessage()); + return EXIT_CODE_ERROR; + } catch (Exception e) { + err().println(errorPrefix + ": " + e.getMessage()); + return EXIT_CODE_ERROR; + } + } + + protected VerificationOptions getOptions() { + OutputFormat format = OutputFormat.TEXT; + try { + format = OutputFormat.valueOf(outputFormatStr.toUpperCase(Locale.US)); + } catch (IllegalArgumentException e) { + err().println("Invalid output format '" + outputFormatStr + "'. Defaulting to TEXT."); + } + return VerificationOptions.builder() + .setTimeout(Duration.ofSeconds(timeoutSeconds)) + .setComprehensionUnrollLimit(comprehensionUnrollLimit) + .setUnknownIdentifiers(unknownIdentifiers) + .setOutputFormat(format) + .build(); + } + + protected int handleSingleResult(CelVerificationResult result, OutputFormat format) { + if (format == OutputFormat.JSON) { + out().println(FormatUtils.formatJsonResult(result)); + } else { + out().println(FormatUtils.formatTextResult(result)); + } + + if (result.status() == VerificationStatus.VERIFIED) { + return EXIT_CODE_VERIFIED; + } else if (result.status() == VerificationStatus.VIOLATED) { + return EXIT_CODE_VIOLATED; + } else { + return EXIT_CODE_INCONCLUSIVE; + } + } + } + + /** Base command for commands operating on a single CEL expression. */ + abstract static class SingleExpressionCommand extends BaseVerificationCommand { + @Option( + names = {"--expr", "-e"}, + required = true, + description = "CEL expression string to verify") + String expression = ""; + } + + @Command( + name = "check-sat", + description = "Verify satisfiability of a CEL expression & generate witness model") + static class CheckSatCommand extends SingleExpressionCommand { + + @Override + public Integer call() { + return executeCommand( + (options, vars) -> + handleSingleResult( + CelVerifierToolCore.checkSatisfiable(expression, vars, options), + options.getOutputFormat())); + } + } + + @Command( + name = "check-valid", + description = "Verify validity (isAlwaysTrue) of a CEL expression & generate counterexample") + static class CheckValidCommand extends SingleExpressionCommand { + + @Override + public Integer call() { + return executeCommand( + (options, vars) -> + handleSingleResult( + CelVerifierToolCore.checkValid(expression, vars, options), + options.getOutputFormat())); + } + } + + @Command( + name = "verify-equiv", + description = "Prove logical equivalence between two CEL expressions") + static class VerifyEquivCommand extends BaseVerificationCommand { + + @Option( + names = {"--expr1"}, + required = true, + description = "First CEL expression") + String expressionA = ""; + + @Option( + names = {"--expr2"}, + required = true, + description = "Second CEL expression") + String expressionB = ""; + + @Override + public Integer call() { + return executeCommand( + (options, vars) -> + handleSingleResult( + CelVerifierToolCore.verifyEquivalence(expressionA, expressionB, vars, options), + options.getOutputFormat())); + } + } + + @Command( + name = "verify-policy", + description = "Verify policy invariants defined in a YAML policy file") + static class VerifyPolicyCommand extends BaseVerificationCommand { + + @Option( + names = {"--file", "-f"}, + required = true, + description = "Path to policy YAML file") + String filePath = ""; + + @Override + public Integer call() { + return executeCommand( + "Policy verification error", + (options, vars) -> { + File file = new File(filePath); + if (!file.exists()) { + err().println("File not found: " + filePath); + return EXIT_CODE_ERROR; + } + String yamlContent = + new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + + ImmutableMap results = + CelVerifierToolCore.verifyPolicyInvariants(yamlContent, vars, options); + + if (options.getOutputFormat() == OutputFormat.JSON) { + out().println(FormatUtils.formatJsonPolicyResults(file.getName(), results)); + } else { + out().println(FormatUtils.formatTextPolicyResults(file.getName(), results)); + } + + return getPolicyExitCode(results); + }); + } + + private static int getPolicyExitCode(ImmutableMap results) { + boolean anyViolated = false; + boolean anyInconclusive = false; + for (CelVerificationResult res : results.values()) { + if (res.status() == VerificationStatus.VIOLATED) { + anyViolated = true; + } else if (res.status() == VerificationStatus.INCONCLUSIVE) { + anyInconclusive = true; + } + } + + if (anyViolated) { + return EXIT_CODE_VIOLATED; + } else if (anyInconclusive) { + return EXIT_CODE_INCONCLUSIVE; + } + return EXIT_CODE_VERIFIED; + } + } + + public static void main(String[] args) { + int exitCode = new CommandLine(new CelVerifierTool()).execute(args); + System.exit(exitCode); + } +} diff --git a/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierToolCore.java b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierToolCore.java new file mode 100644 index 000000000..89e842fb1 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierToolCore.java @@ -0,0 +1,161 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.tools; + +import com.google.common.collect.ImmutableMap; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelBuilder; +import dev.cel.bundle.CelFactory; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelOptions; +import dev.cel.common.types.CelType; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerBuilder; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.extensions.CelExtensions; +import dev.cel.parser.CelStandardMacro; +import dev.cel.policy.CelPolicy; +import dev.cel.policy.CelPolicyCompiler; +import dev.cel.policy.CelPolicyCompilerFactory; +import dev.cel.policy.CelPolicyParser; +import dev.cel.policy.CelPolicyParserFactory; +import dev.cel.verifier.CelPolicyVerifier; +import dev.cel.verifier.CelPolicyVerifierFactory; +import dev.cel.verifier.CelVerificationResult; +import dev.cel.verifier.CelVerifier; +import dev.cel.verifier.CelVerifierBuilder; +import dev.cel.verifier.CelVerifierFactory; +import java.util.Map; + +/** Core decoupled engine that executes formal verification operations. */ +final class CelVerifierToolCore { + + private CelVerifierToolCore() {} + + /** Checks if a single CEL expression is satisfiable. */ + static CelVerificationResult checkSatisfiable( + String expression, Map variables, VerificationOptions options) + throws Exception { + CelCompiler compiler = buildCompiler(variables); + CelAbstractSyntaxTree ast = compiler.compile(expression).getAst(); + CelVerifier verifier = buildVerifier(options); + return verifier.isSatisfiable(ast); + } + + /** Checks if a single CEL expression is valid (always true). */ + static CelVerificationResult checkValid( + String expression, Map variables, VerificationOptions options) + throws Exception { + CelCompiler compiler = buildCompiler(variables); + CelAbstractSyntaxTree ast = compiler.compile(expression).getAst(); + CelVerifier verifier = buildVerifier(options); + return verifier.isAlwaysTrue(ast); + } + + /** Proves logical equivalence between two CEL expressions. */ + static CelVerificationResult verifyEquivalence( + String expressionA, + String expressionB, + Map variables, + VerificationOptions options) + throws Exception { + CelCompiler compiler = buildCompiler(variables); + CelAbstractSyntaxTree astA = compiler.compile(expressionA).getAst(); + CelAbstractSyntaxTree astB = compiler.compile(expressionB).getAst(); + CelVerifier verifier = buildVerifier(options); + return verifier.verifyEquivalence(astA, astB); + } + + /** Verifies custom invariants in a YAML policy content string. */ + static ImmutableMap verifyPolicyInvariants( + String yamlContent, Map variables, VerificationOptions options) + throws Exception { + CelPolicyParser parser = CelPolicyParserFactory.newYamlParserBuilder().build(); + CelPolicy policy = parser.parse(yamlContent); + + CelPolicyVerifier policyVerifier = buildPolicyVerifier(variables, options); + return policyVerifier.verifyInvariants(policy); + } + + /** Verifies equivalence between two YAML policy content strings. */ + static CelVerificationResult verifyPolicyEquivalence( + String yamlContentA, + String yamlContentB, + Map variables, + VerificationOptions options) + throws Exception { + CelPolicyParser parser = CelPolicyParserFactory.newYamlParserBuilder().build(); + CelPolicy policyA = parser.parse(yamlContentA); + CelPolicy policyB = parser.parse(yamlContentB); + + CelPolicyVerifier policyVerifier = buildPolicyVerifier(variables, options); + return policyVerifier.verifyEquivalence(policyA, policyB); + } + + static CelCompiler buildCompiler(Map variables) { + CelCompilerBuilder builder = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addLibraries( + CelExtensions.bindings(), + CelExtensions.comprehensions(), + CelExtensions.encoders(CelOptions.DEFAULT), + CelExtensions.lists(), + CelExtensions.math(), + CelExtensions.optional(), + CelExtensions.protos(), + CelExtensions.regex(), + CelExtensions.sets(CelOptions.DEFAULT), + CelExtensions.strings()); + for (Map.Entry entry : variables.entrySet()) { + builder.addVar(entry.getKey(), entry.getValue()); + } + return builder.build(); + } + + static CelVerifier buildVerifier(VerificationOptions options) { + CelVerifierBuilder builder = + CelVerifierFactory.newVerifier() + .setTimeout(options.getTimeout()) + .setComprehensionUnrollLimit(options.getComprehensionUnrollLimit()); + + for (String unknown : options.getUnknownIdentifiers()) { + builder.addUnknownIdentifier(unknown); + } + return builder.build(); + } + + private static CelPolicyVerifier buildPolicyVerifier( + Map variables, VerificationOptions options) { + CelBuilder celBuilder = + CelFactory.plannerCelBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addCompilerLibraries( + CelExtensions.optional(), + CelExtensions.bindings(), + CelExtensions.encoders(CelOptions.DEFAULT), + CelExtensions.math(), + CelExtensions.strings()); + for (Map.Entry entry : variables.entrySet()) { + celBuilder.addVar(entry.getKey(), entry.getValue()); + } + Cel celBundle = celBuilder.build(); + CelPolicyCompiler policyCompiler = + CelPolicyCompilerFactory.newPolicyCompiler(celBundle).build(); + CelVerifier astVerifier = buildVerifier(options); + + return CelPolicyVerifierFactory.newVerifier(policyCompiler, astVerifier).build(); + } +} diff --git a/verifier/src/main/java/dev/cel/verifier/tools/FormatUtils.java b/verifier/src/main/java/dev/cel/verifier/tools/FormatUtils.java new file mode 100644 index 000000000..62d9889ad --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/tools/FormatUtils.java @@ -0,0 +1,173 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.tools; + +import com.google.common.collect.ImmutableMap; +import dev.cel.verifier.CelVerificationResult; +import dev.cel.verifier.CelVerificationResult.VerificationStatus; +import java.util.Map; + +/** Utilities for formatting verification output (ANSI text & JSON). */ +final class FormatUtils { + + // ANSI Escape Codes for formatting text + static final String ANSI_RESET = "\u001B[0m"; + static final String ANSI_BOLD = "\u001B[1m"; + static final String ANSI_GREEN = "\u001B[32m"; + static final String ANSI_RED = "\u001B[31m"; + static final String ANSI_YELLOW = "\u001B[33m"; + static final String ANSI_CYAN = "\u001B[36m"; + + private FormatUtils() {} + + /** Formats a single CelVerificationResult for human-readable console display with ANSI color. */ + static String formatTextResult(CelVerificationResult result) { + StringBuilder sb = new StringBuilder(); + String statusColor = getStatusColor(result.status()); + sb.append(statusColor) + .append(ANSI_BOLD) + .append("[") + .append(result.status()) + .append("]") + .append(ANSI_RESET); + + if (!result.message().isEmpty()) { + sb.append(" ").append(result.message()); + } + + return sb.toString(); + } + + /** Formats policy invariant verification results for human-readable console display. */ + static String formatTextPolicyResults( + String policyName, ImmutableMap results) { + StringBuilder sb = new StringBuilder(); + sb.append(ANSI_BOLD) + .append("Policy Invariant Verification for '") + .append(policyName) + .append("':\n") + .append(ANSI_RESET); + + for (Map.Entry entry : results.entrySet()) { + String id = entry.getKey(); + CelVerificationResult result = entry.getValue(); + String symbol = result.status() == VerificationStatus.VERIFIED ? "✓" : "✗"; + String color = getStatusColor(result.status()); + + sb.append(" ") + .append(color) + .append(symbol) + .append(" Invariant '") + .append(id) + .append("': ") + .append(result.status()) + .append(ANSI_RESET); + + if (!result.message().isEmpty()) { + sb.append("\n ").append(result.message().replace("\n", "\n ")); + } + sb.append("\n"); + } + return sb.toString().trim(); + } + + /** Formats a single CelVerificationResult as structured JSON. */ + static String formatJsonResult(CelVerificationResult result) { + StringBuilder sb = new StringBuilder(); + sb.append("{\n"); + sb.append(" \"status\": \"").append(result.status()).append("\",\n"); + sb.append(" \"message\": \"").append(escapeJson(result.message())).append("\"\n"); + sb.append("}"); + return sb.toString(); + } + + /** Formats policy invariant verification results as structured JSON. */ + static String formatJsonPolicyResults( + String policyName, ImmutableMap results) { + StringBuilder sb = new StringBuilder(); + sb.append("{\n"); + sb.append(" \"policyName\": \"").append(escapeJson(policyName)).append("\",\n"); + sb.append(" \"invariants\": [\n"); + + int count = 0; + for (Map.Entry entry : results.entrySet()) { + count++; + String id = entry.getKey(); + CelVerificationResult res = entry.getValue(); + sb.append(" {\n"); + sb.append(" \"id\": \"").append(escapeJson(id)).append("\",\n"); + sb.append(" \"status\": \"").append(res.status()).append("\",\n"); + sb.append(" \"message\": \"").append(escapeJson(res.message())).append("\"\n"); + sb.append(" }").append(count < results.size() ? "," : "").append("\n"); + } + + sb.append(" ]\n"); + sb.append("}"); + return sb.toString(); + } + + private static String getStatusColor(VerificationStatus status) { + switch (status) { + case VERIFIED: + return ANSI_GREEN; + case VIOLATED: + return ANSI_RED; + case INCONCLUSIVE: + return ANSI_YELLOW; + } + return ANSI_RESET; + } + + static String escapeJson(String input) { + if (input == null) { + return ""; + } + StringBuilder sb = new StringBuilder(input.length() + 16); + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + switch (c) { + case '\\': + sb.append("\\\\"); + break; + case '"': + sb.append("\\\""); + break; + case '\b': + sb.append("\\b"); + break; + case '\f': + sb.append("\\f"); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + default: + if (c < 0x20) { + sb.append(String.format("\\u%04x", (int) c)); + } else { + sb.append(c); + } + break; + } + } + return sb.toString(); + } +} diff --git a/verifier/src/main/java/dev/cel/verifier/tools/VerificationOptions.java b/verifier/src/main/java/dev/cel/verifier/tools/VerificationOptions.java new file mode 100644 index 000000000..91ec443a5 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/tools/VerificationOptions.java @@ -0,0 +1,219 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.tools; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import dev.cel.common.types.CelType; +import dev.cel.common.types.ListType; +import dev.cel.common.types.MapType; +import dev.cel.common.types.SimpleType; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** Configuration options for CEL verification CLI operations. */ +final class VerificationOptions { + + /** Output format for verification CLI results. */ + enum OutputFormat { + TEXT, + JSON + } + + static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(10); + static final int DEFAULT_COMPREHENSION_UNROLL_LIMIT = 5; + static final OutputFormat DEFAULT_OUTPUT_FORMAT = OutputFormat.TEXT; + + private final Duration timeout; + private final int comprehensionUnrollLimit; + private final ImmutableList unknownIdentifiers; + private final OutputFormat outputFormat; + + Duration getTimeout() { + return timeout; + } + + int getComprehensionUnrollLimit() { + return comprehensionUnrollLimit; + } + + ImmutableList getUnknownIdentifiers() { + return unknownIdentifiers; + } + + OutputFormat getOutputFormat() { + return outputFormat; + } + + static Builder builder() { + return new Builder(); + } + + /** A builder for {@link VerificationOptions}. */ + static final class Builder { + private Duration timeout = DEFAULT_TIMEOUT; + private int comprehensionUnrollLimit = DEFAULT_COMPREHENSION_UNROLL_LIMIT; + private ImmutableList unknownIdentifiers = ImmutableList.of(); + private OutputFormat outputFormat = DEFAULT_OUTPUT_FORMAT; + + @CanIgnoreReturnValue + Builder setTimeout(Duration timeout) { + this.timeout = Preconditions.checkNotNull(timeout); + return this; + } + + @CanIgnoreReturnValue + Builder setComprehensionUnrollLimit(int unrollLimit) { + Preconditions.checkArgument(unrollLimit >= 0, "unrollLimit must be non-negative"); + this.comprehensionUnrollLimit = unrollLimit; + return this; + } + + @CanIgnoreReturnValue + Builder setUnknownIdentifiers(List unknownIdentifiers) { + this.unknownIdentifiers = ImmutableList.copyOf(unknownIdentifiers); + return this; + } + + @CanIgnoreReturnValue + Builder setOutputFormat(OutputFormat outputFormat) { + this.outputFormat = Preconditions.checkNotNull(outputFormat); + return this; + } + + VerificationOptions build() { + return new VerificationOptions( + timeout, comprehensionUnrollLimit, unknownIdentifiers, outputFormat); + } + } + + private VerificationOptions( + Duration timeout, + int comprehensionUnrollLimit, + ImmutableList unknownIdentifiers, + OutputFormat outputFormat) { + this.timeout = timeout; + this.comprehensionUnrollLimit = comprehensionUnrollLimit; + this.unknownIdentifiers = unknownIdentifiers; + this.outputFormat = outputFormat; + } + + /** + * Helper utility to parse CLI variable definitions formatted as "name:type" (e.g. "x:int", + * "role:string", "is_admin:bool"). + */ + static ImmutableMap parseVariables(List varSpecs) { + if (varSpecs == null || varSpecs.isEmpty()) { + return ImmutableMap.of(); + } + Map vars = new HashMap<>(); + for (String varSpec : varSpecs) { + Preconditions.checkNotNull(varSpec, "Variable specification cannot be null."); + String[] parts = varSpec.split(":", 2); + if (parts.length != 2) { + throw new IllegalArgumentException( + "Invalid variable specification: '" + + varSpec + + "'. Expected format 'name:type' (e.g., 'x:int')."); + } + String name = parts[0].trim(); + String typeStr = parts[1].trim().toLowerCase(Locale.US); + CelType type = parseCelType(typeStr); + vars.put(name, type); + } + return ImmutableMap.copyOf(vars); + } + + static CelType parseCelType(String typeStr) { + Preconditions.checkNotNull(typeStr, "Type string cannot be null."); + String str = typeStr.trim().toLowerCase(Locale.US); + + if (str.startsWith("list<") && str.endsWith(">")) { + String inner = str.substring(5, str.length() - 1).trim(); + CelType elemType = parseCelType(inner); + return ListType.create(elemType); + } + + if (str.startsWith("map<") && str.endsWith(">")) { + String inner = str.substring(4, str.length() - 1).trim(); + List parts = splitGenericArgs(inner); + if (parts.size() != 2) { + throw new IllegalArgumentException( + "Invalid map type format: '" + + typeStr + + "'. Expected format 'map' (e.g., 'map')."); + } + CelType keyType = parseCelType(parts.get(0)); + CelType valueType = parseCelType(parts.get(1)); + return MapType.create(keyType, valueType); + } + + switch (str) { + case "int": + return SimpleType.INT; + case "uint": + return SimpleType.UINT; + case "string": + return SimpleType.STRING; + case "bool": + case "boolean": + return SimpleType.BOOL; + case "double": + case "float": + return SimpleType.DOUBLE; + case "bytes": + return SimpleType.BYTES; + case "dyn": + return SimpleType.DYN; + default: + throw new IllegalArgumentException( + "Unsupported type for CLI variable declaration: '" + + typeStr + + "'. Supported types: int, uint, string, bool, double, bytes, dyn, list, map."); + } + } + + private static List splitGenericArgs(String inner) { + List result = new ArrayList<>(); + int depth = 0; + StringBuilder current = new StringBuilder(); + for (int i = 0; i < inner.length(); i++) { + char c = inner.charAt(i); + if (c == '<') { + depth++; + current.append(c); + } else if (c == '>') { + depth--; + current.append(c); + } else if (c == ',' && depth == 0) { + result.add(current.toString().trim()); + current.setLength(0); + } else { + current.append(c); + } + } + if (current.length() > 0) { + result.add(current.toString().trim()); + } + return result; + } +} diff --git a/verifier/src/test/java/dev/cel/verifier/BUILD.bazel b/verifier/src/test/java/dev/cel/verifier/BUILD.bazel index 9e7f0ed15..de788ca5c 100644 --- a/verifier/src/test/java/dev/cel/verifier/BUILD.bazel +++ b/verifier/src/test/java/dev/cel/verifier/BUILD.bazel @@ -9,7 +9,7 @@ java_library( name = "tests", testonly = True, srcs = glob( - ["**/*.java"], + ["*.java"], ), compatible_with = [], data = [ @@ -53,6 +53,7 @@ java_library( "//verifier:verifier_factory", "//verifier:z3_impl", "//verifier/axioms", + "//verifier/tools", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@maven//:com_google_guava_guava", ], diff --git a/verifier/src/test/java/dev/cel/verifier/tools/BUILD.bazel b/verifier/src/test/java/dev/cel/verifier/tools/BUILD.bazel new file mode 100644 index 000000000..6077e4950 --- /dev/null +++ b/verifier/src/test/java/dev/cel/verifier/tools/BUILD.bazel @@ -0,0 +1,31 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:testing.bzl", "junit4_test_suites") + +package( + default_applicable_licenses = ["//:license"], +) + +java_library( + name = "tests", + testonly = True, + srcs = glob(["*.java"]), + deps = [ + "//:java_truth", + "//common/types", + "//common/types:type_providers", + "//verifier", + "//verifier/tools", + "@maven//:com_google_guava_guava", + "@maven//:info_picocli_picocli", + "@maven//:junit_junit", + ], +) + +junit4_test_suites( + name = "test_suites", + sizes = [ + "small", + ], + src_dir = "src/test/java", + deps = [":tests"], +) diff --git a/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierToolTest.java b/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierToolTest.java new file mode 100644 index 000000000..383604aa0 --- /dev/null +++ b/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierToolTest.java @@ -0,0 +1,587 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.tools; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import dev.cel.common.types.CelType; +import dev.cel.common.types.ListType; +import dev.cel.common.types.MapType; +import dev.cel.common.types.SimpleType; +import dev.cel.verifier.CelVerificationResult; +import dev.cel.verifier.CelVerificationResult.VerificationStatus; +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.time.Duration; +import java.util.Arrays; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import picocli.CommandLine; + +@RunWith(JUnit4.class) +public final class CelVerifierToolTest { + + @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Before + public void setUp() { + System.setProperty("z3.skipLibraryLoad", "true"); + } + + private String executeToolWithOutput(String... args) { + StringWriter out = new StringWriter(); + PrintWriter pw = new PrintWriter(out); + CommandLine cmd = new CommandLine(new CelVerifierTool()); + cmd.setOut(pw); + cmd.setErr(pw); + cmd.execute(args); + return out.toString(); + } + + @Test + public void celVerifierTool_checkSat_jsonOutputFormat() { + String output = + executeToolWithOutput( + "check-sat", "--expr", "x > 0", "--var", "x:int", "--output_format", "json"); + assertThat(output).startsWith("{\n"); + assertThat(output).contains("\"status\": \"VERIFIED\""); + assertThat(output).contains("satisfiable"); + assertThat(output.trim()).endsWith("}"); + } + + @Test + public void celVerifierTool_checkSat_textOutputFormat() { + String output = + executeToolWithOutput("check-sat", "--expr", "x > 0", "--var", "x:int", "-fmt", "text"); + assertThat(output).contains("[VERIFIED]"); + assertThat(output).contains("satisfiable"); + } + + @Test + public void celVerifierTool_checkSat_withDynVariable() { + String output = + executeToolWithOutput( + "check-sat", "--expr", "x == 'hello'", "--var", "x:dyn", "-fmt", "json"); + assertThat(output).contains("\"status\": \"VERIFIED\""); + } + + @Test + public void celVerifierTool_checkSat_withUnknownOption() { + String output = + executeToolWithOutput( + "check-sat", + "--expr", + "request.headers != null", + "--var", + "request:map", + "-u", + "request.headers", + "-fmt", + "json"); + assertThat(output).contains("\"status\": \"VERIFIED\""); + } + + @Test + public void celVerifierTool_checkSat_withTimeoutAndUnrollLimit() { + String output = + executeToolWithOutput( + "check-sat", + "--expr", + "[1, 2, 3].all(x, x > 0)", + "--timeout", + "5", + "--unroll-limit", + "5", + "-fmt", + "json"); + assertThat(output).contains("\"status\": \"VERIFIED\""); + } + + @Test + public void celVerifierTool_verifyPolicy_fileNotFound() { + String output = executeToolWithOutput("verify-policy", "--file", "non_existent_policy.yaml"); + assertThat(output).contains("File not found: non_existent_policy.yaml"); + } + + @Test + public void parseVariables_success() { + ImmutableMap vars = + VerificationOptions.parseVariables( + Arrays.asList( + "x:int", + "role:string", + "is_admin:bool", + "tags:list", + "scores:map")); + assertThat(vars).containsEntry("x", SimpleType.INT); + assertThat(vars).containsEntry("role", SimpleType.STRING); + assertThat(vars).containsEntry("is_admin", SimpleType.BOOL); + assertThat(vars).containsEntry("tags", ListType.create(SimpleType.STRING)); + assertThat(vars).containsEntry("scores", MapType.create(SimpleType.STRING, SimpleType.INT)); + } + + @Test + public void parseVariables_allTypesIncludingDyn() { + ImmutableMap vars = + VerificationOptions.parseVariables( + Arrays.asList( + "u:uint", + "d:double", + "fl:float", + "b:bytes", + "dyn_val:dyn", + "flag:boolean", + "nested_list:list", + "nested_map:map")); + assertThat(vars).containsEntry("u", SimpleType.UINT); + assertThat(vars).containsEntry("d", SimpleType.DOUBLE); + assertThat(vars).containsEntry("fl", SimpleType.DOUBLE); + assertThat(vars).containsEntry("b", SimpleType.BYTES); + assertThat(vars).containsEntry("dyn_val", SimpleType.DYN); + assertThat(vars).containsEntry("flag", SimpleType.BOOL); + assertThat(vars).containsEntry("nested_list", ListType.create(SimpleType.DYN)); + assertThat(vars).containsEntry("nested_map", MapType.create(SimpleType.STRING, SimpleType.DYN)); + } + + @Test + public void parseVariables_invalidFormat_throws() { + assertThrows( + IllegalArgumentException.class, + () -> VerificationOptions.parseVariables(Arrays.asList("x_no_colon"))); + } + + @Test + public void parseVariables_unsupportedType_throws() { + IllegalArgumentException ex = + assertThrows( + IllegalArgumentException.class, + () -> VerificationOptions.parseVariables(Arrays.asList("x:foo_bar"))); + assertThat(ex) + .hasMessageThat() + .contains("Supported types: int, uint, string, bool, double, bytes, dyn"); + } + + @Test + public void parseVariables_invalidMapFormat_throws() { + assertThrows( + IllegalArgumentException.class, + () -> VerificationOptions.parseVariables(Arrays.asList("x:map"))); + } + + @Test + public void parseVariables_emptyOrNull_returnsEmptyMap() { + assertThat(VerificationOptions.parseVariables(null)).isEmpty(); + assertThat(VerificationOptions.parseVariables(ImmutableList.of())).isEmpty(); + } + + @Test + public void parseVariables_nestedTypes() { + ImmutableMap vars = + VerificationOptions.parseVariables( + Arrays.asList( + "nested_map:map>", + "nested_list_map:map>")); + assertThat(vars) + .containsEntry( + "nested_map", + MapType.create(SimpleType.STRING, MapType.create(SimpleType.STRING, SimpleType.INT))); + assertThat(vars) + .containsEntry( + "nested_list_map", MapType.create(SimpleType.STRING, ListType.create(SimpleType.INT))); + } + + @Test + public void parseVariables_emptyString_throws() { + assertThrows( + IllegalArgumentException.class, + () -> VerificationOptions.parseVariables(Arrays.asList(""))); + assertThrows( + IllegalArgumentException.class, + () -> VerificationOptions.parseVariables(Arrays.asList(" "))); + } + + @Test + public void parseVariables_nullElement_throws() { + assertThrows( + NullPointerException.class, + () -> VerificationOptions.parseVariables(Arrays.asList((String) null))); + } + + @Test + public void checkSatisfiable_satisfiable() throws Exception { + VerificationOptions options = + VerificationOptions.builder().setTimeout(Duration.ofSeconds(5)).build(); + ImmutableMap vars = + ImmutableMap.of("role", SimpleType.STRING, "port", SimpleType.INT); + + CelVerificationResult result = + CelVerifierToolCore.checkSatisfiable("role == 'editor' && port > 1024", vars, options); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + assertThat(result.message()).contains("satisfiable"); + } + + @Test + public void checkValid_valid() throws Exception { + VerificationOptions options = + VerificationOptions.builder().setTimeout(Duration.ofSeconds(5)).build(); + ImmutableMap vars = ImmutableMap.of("x", SimpleType.INT); + + CelVerificationResult result = + CelVerifierToolCore.checkValid("x > 10 || x <= 10", vars, options); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void verifyEquivalence_equivalent() throws Exception { + VerificationOptions options = + VerificationOptions.builder().setTimeout(Duration.ofSeconds(5)).build(); + ImmutableMap vars = ImmutableMap.of("x", SimpleType.INT); + + CelVerificationResult result = + CelVerifierToolCore.verifyEquivalence("x > 10", "10 < x", vars, options); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void verifyPolicyInvariants_success() throws Exception { + String yamlPolicy = + "name: secure_access_policy\n" + + "rule:\n" + + " match:\n" + + " - condition: port == 80\n" + + " output: 'true'\n" + + " - output: 'false'\n" + + "verification:\n" + + " invariants:\n" + + " - id: port_check\n" + + " assert:\n" + + " - port == 80 || port != 80\n"; + + VerificationOptions options = + VerificationOptions.builder().setTimeout(Duration.ofSeconds(5)).build(); + ImmutableMap vars = ImmutableMap.of("port", SimpleType.INT); + + ImmutableMap results = + CelVerifierToolCore.verifyPolicyInvariants(yamlPolicy, vars, options); + + assertThat(results).containsKey("port_check"); + assertThat(results.get("port_check").status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void verifyPolicyEquivalence_equivalent() throws Exception { + String policyA = + "name: policy_a\n" + + "rule:\n" + + " match:\n" + + " - condition: port == 80\n" + + " output: 'true'\n" + + " - output: 'false'\n"; + + String policyB = + "name: policy_b\n" + + "rule:\n" + + " match:\n" + + " - condition: 80 == port\n" + + " output: 'true'\n" + + " - output: 'false'\n"; + + VerificationOptions options = + VerificationOptions.builder().setTimeout(Duration.ofSeconds(5)).build(); + ImmutableMap vars = ImmutableMap.of("port", SimpleType.INT); + + CelVerificationResult result = + CelVerifierToolCore.verifyPolicyEquivalence(policyA, policyB, vars, options); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } + + @Test + public void formatTextPolicyResults_verifiedAndViolated() throws Exception { + VerificationOptions options = + VerificationOptions.builder().setTimeout(Duration.ofSeconds(5)).build(); + CelVerificationResult verifiedRes = + CelVerifierToolCore.checkSatisfiable("true", ImmutableMap.of(), options); + CelVerificationResult violatedRes = + CelVerifierToolCore.checkValid("x > 0", ImmutableMap.of("x", SimpleType.INT), options); + + ImmutableMap results = + ImmutableMap.of("inv_1", verifiedRes, "inv_2", violatedRes); + + String text = FormatUtils.formatTextPolicyResults("test_policy", results); + assertThat(text).contains("Policy Invariant Verification for 'test_policy':"); + assertThat(text).contains("✓ Invariant 'inv_1': VERIFIED"); + assertThat(text).contains("✗ Invariant 'inv_2': VIOLATED"); + } + + @Test + public void formatJsonPolicyResults_structuredJson() throws Exception { + VerificationOptions options = + VerificationOptions.builder().setTimeout(Duration.ofSeconds(5)).build(); + CelVerificationResult result = + CelVerifierToolCore.checkSatisfiable("true", ImmutableMap.of(), options); + + ImmutableMap results = ImmutableMap.of("inv_1", result); + + String json = FormatUtils.formatJsonPolicyResults("my_policy", results); + assertThat(json).startsWith("{\n"); + assertThat(json).contains("\"policyName\": \"my_policy\""); + assertThat(json).contains("\"id\": \"inv_1\""); + assertThat(json).contains("\"status\": \"VERIFIED\""); + assertThat(json).endsWith("}"); + } + + @Test + public void celVerifierTool_verifyPolicy_success() throws Exception { + File policyFile = tempFolder.newFile("test_policy.yaml"); + String yamlContent = + "name: test_policy\n" + + "rule:\n" + + " match:\n" + + " - condition: port == 80\n" + + " output: 'true'\n" + + " - output: 'false'\n" + + "verification:\n" + + " invariants:\n" + + " - id: port_check\n" + + " assert:\n" + + " - port == 80 || port != 80\n"; + Files.write(policyFile.toPath(), yamlContent.getBytes(StandardCharsets.UTF_8)); + + String output = + executeToolWithOutput( + "verify-policy", + "--file", + policyFile.getAbsolutePath(), + "--var", + "port:int", + "-fmt", + "json"); + + assertThat(output).contains("\"policyName\": \"test_policy.yaml\""); + assertThat(output).contains("\"status\": \"VERIFIED\""); + } + + @Test + public void celVerifierTool_verifyPolicy_violated() throws Exception { + File policyFile = tempFolder.newFile("violated_policy.yaml"); + String yamlContent = + "name: violated_policy\n" + + "rule:\n" + + " match:\n" + + " - condition: port == 80\n" + + " output: 'true'\n" + + " - output: 'false'\n" + + "verification:\n" + + " invariants:\n" + + " - id: invalid_check\n" + + " assert:\n" + + " - port > 1024\n"; + Files.write(policyFile.toPath(), yamlContent.getBytes(StandardCharsets.UTF_8)); + + int exitCode = + new CommandLine(new CelVerifierTool()) + .execute("verify-policy", "--file", policyFile.getAbsolutePath(), "--var", "port:int"); + + assertThat(exitCode).isEqualTo(CelVerifierTool.EXIT_CODE_VIOLATED); + } + + @Test + public void celVerifierTool_verifyPolicy_multipleInvariants_oneViolated() throws Exception { + File policyFile = tempFolder.newFile("multi_invariant_policy.yaml"); + String yamlContent = + "name: multi_invariant_policy\n" + + "rule:\n" + + " match:\n" + + " - condition: port == 80\n" + + " output: 'true'\n" + + " - output: 'false'\n" + + "verification:\n" + + " invariants:\n" + + " - id: valid_check\n" + + " assert:\n" + + " - port == 80 || port != 80\n" + + " - id: invalid_check\n" + + " assert:\n" + + " - port > 1024\n"; + Files.write(policyFile.toPath(), yamlContent.getBytes(StandardCharsets.UTF_8)); + + int exitCode = + new CommandLine(new CelVerifierTool()) + .execute("verify-policy", "--file", policyFile.getAbsolutePath(), "--var", "port:int"); + + assertThat(exitCode).isEqualTo(CelVerifierTool.EXIT_CODE_VIOLATED); + } + + @Test + public void celVerifierTool_verifyPolicy_multipleInvariants_allVerified() throws Exception { + File policyFile = tempFolder.newFile("multi_verified_policy.yaml"); + String yamlContent = + "name: multi_verified_policy\n" + + "rule:\n" + + " match:\n" + + " - condition: port == 80\n" + + " output: 'true'\n" + + " - output: 'false'\n" + + "verification:\n" + + " invariants:\n" + + " - id: check_1\n" + + " assert:\n" + + " - port == 80 || port != 80\n" + + " - id: check_2\n" + + " assert:\n" + + " - port > 0 || port <= 0\n"; + Files.write(policyFile.toPath(), yamlContent.getBytes(StandardCharsets.UTF_8)); + + int exitCode = + new CommandLine(new CelVerifierTool()) + .execute("verify-policy", "--file", policyFile.getAbsolutePath(), "--var", "port:int"); + + assertThat(exitCode).isEqualTo(CelVerifierTool.EXIT_CODE_VERIFIED); + } + + @Test + public void formatUtils_jsonResult() throws Exception { + VerificationOptions options = + VerificationOptions.builder().setTimeout(Duration.ofSeconds(5)).build(); + CelVerificationResult result = + CelVerifierToolCore.checkSatisfiable("true", ImmutableMap.of(), options); + String json = FormatUtils.formatJsonResult(result); + assertThat(json).contains("\"status\": \"VERIFIED\""); + assertThat(json).contains("satisfiable"); + } + + @Test + public void celVerifierTool_checkSat_verified() { + int exitCode = + new CommandLine(new CelVerifierTool()) + .execute("check-sat", "--expr", "x > 0", "--var", "x:int"); + assertThat(exitCode).isEqualTo(CelVerifierTool.EXIT_CODE_VERIFIED); + } + + @Test + public void celVerifierTool_checkValid_violated() { + int exitCode = + new CommandLine(new CelVerifierTool()) + .execute("check-valid", "--expr", "x > 0", "--var", "x:int"); + assertThat(exitCode).isEqualTo(CelVerifierTool.EXIT_CODE_VIOLATED); + } + + @Test + public void celVerifierTool_verifyEquiv_verified() { + int exitCode = + new CommandLine(new CelVerifierTool()) + .execute("verify-equiv", "--expr1", "x > 10", "--expr2", "10 < x", "--var", "x:int"); + assertThat(exitCode).isEqualTo(CelVerifierTool.EXIT_CODE_VERIFIED); + } + + @Test + public void celVerifierTool_checkSat_compilationError() { + String output = + executeToolWithOutput("check-sat", "--expr", "invalid + + syntax", "--var", "x:int"); + assertThat(output).contains("Compilation error"); + } + + @Test + public void celVerifierTool_checkValid_withUnknownOption_violated() { + int exitCode = + new CommandLine(new CelVerifierTool()) + .execute("check-valid", "--expr", "x == x", "--var", "x:int", "-u", "x"); + assertThat(exitCode).isEqualTo(CelVerifierTool.EXIT_CODE_VIOLATED); + } + + @Test + public void celVerifierTool_checkValid_inconclusive() { + int exitCode = + new CommandLine(new CelVerifierTool()) + .execute("check-valid", "--expr", "int('123') == 123"); + assertThat(exitCode).isEqualTo(CelVerifierTool.EXIT_CODE_INCONCLUSIVE); + } + + @Test + public void celVerifierTool_verifyPolicy_inconclusive() throws Exception { + File policyFile = tempFolder.newFile("inconclusive_policy.yaml"); + String yamlContent = + "name: inconclusive_policy\n" + + "rule:\n" + + " match:\n" + + " - condition: port == 80\n" + + " output: 'true'\n" + + " - output: 'false'\n" + + "verification:\n" + + " invariants:\n" + + " - id: approx_check\n" + + " assert:\n" + + " - int('123') == 123\n"; + Files.write(policyFile.toPath(), yamlContent.getBytes(StandardCharsets.UTF_8)); + + int exitCode = + new CommandLine(new CelVerifierTool()) + .execute("verify-policy", "--file", policyFile.getAbsolutePath(), "--var", "port:int"); + + assertThat(exitCode).isEqualTo(CelVerifierTool.EXIT_CODE_INCONCLUSIVE); + } + + @Test + public void celVerifierTool_invalidOutputFormat_defaultsToText() { + String output = + executeToolWithOutput( + "check-sat", "--expr", "x > 0", "--var", "x:int", "-fmt", "invalid_fmt"); + assertThat(output).contains("[VERIFIED]"); + } + + @Test + public void formatTextPolicyResults_inconclusive() throws Exception { + VerificationOptions options = + VerificationOptions.builder().setTimeout(Duration.ofSeconds(5)).build(); + CelVerificationResult res = + CelVerifierToolCore.checkValid("int('123') == 123", ImmutableMap.of(), options); + + String text = FormatUtils.formatTextPolicyResults("test_policy", ImmutableMap.of("inv_1", res)); + assertThat(text).contains("Invariant 'inv_1': INCONCLUSIVE"); + } + + @Test + public void formatJson_escapesSpecialCharacters() throws Exception { + VerificationOptions options = + VerificationOptions.builder().setTimeout(Duration.ofSeconds(5)).build(); + CelVerificationResult res = + CelVerifierToolCore.checkValid("int('123') == 123", ImmutableMap.of(), options); + String json = + FormatUtils.formatJsonPolicyResults( + "policy_with_\"quote\"\nand_newline", ImmutableMap.of("inv\ttab", res)); + assertThat(json).contains("policy_with_\\\"quote\\\"\\nand_newline"); + assertThat(json).contains("inv\\ttab"); + } + + @Test + public void celVerifierTool_version() { + int exitCode = new CommandLine(new CelVerifierTool()).execute("--version"); + assertThat(exitCode).isEqualTo(0); + } +} diff --git a/verifier/src/test/java/dev/cel/verifier/tools/FormatUtilsTest.java b/verifier/src/test/java/dev/cel/verifier/tools/FormatUtilsTest.java new file mode 100644 index 000000000..1ab1cbc8a --- /dev/null +++ b/verifier/src/test/java/dev/cel/verifier/tools/FormatUtilsTest.java @@ -0,0 +1,118 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.tools; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableMap; +import dev.cel.common.types.SimpleType; +import dev.cel.verifier.CelVerificationResult; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class FormatUtilsTest { + + @Before + public void setUp() { + System.setProperty("z3.skipLibraryLoad", "true"); + } + + @Test + public void formatJsonResult_verified() throws Exception { + VerificationOptions options = VerificationOptions.builder().build(); + CelVerificationResult result = + CelVerifierToolCore.checkSatisfiable("true", ImmutableMap.of(), options); + + String json = FormatUtils.formatJsonResult(result); + + assertThat(json) + .isEqualTo( + "{\n" + + " \"status\": \"VERIFIED\",\n" + + " \"message\": \"Condition is satisfiable. (The expression is satisfiable" + + " unconditionally, regardless of input state)\"\n" + + "}"); + } + + @Test + public void formatJsonResult_violated() throws Exception { + VerificationOptions options = VerificationOptions.builder().build(); + CelVerificationResult result = + CelVerifierToolCore.checkValid("x > 0", ImmutableMap.of("x", SimpleType.INT), options); + + String json = FormatUtils.formatJsonResult(result); + + assertThat(json) + .startsWith( + "{\n \"status\": \"VIOLATED\",\n \"message\": \"Condition is not always true."); + assertThat(json).endsWith("\"\n}"); + } + + @Test + public void formatJsonPolicyResults_multipleInvariants() throws Exception { + VerificationOptions options = VerificationOptions.builder().build(); + CelVerificationResult verified = + CelVerifierToolCore.checkSatisfiable("true", ImmutableMap.of(), options); + CelVerificationResult inconclusive = + CelVerifierToolCore.checkValid("int('123') == 123", ImmutableMap.of(), options); + ImmutableMap results = + ImmutableMap.of("inv_1", verified, "inv_2", inconclusive); + + String json = FormatUtils.formatJsonPolicyResults("my_policy", results); + + assertThat(json).startsWith("{\n \"policyName\": \"my_policy\",\n \"invariants\": [\n"); + assertThat(json).contains(" {\n \"id\": \"inv_1\",\n \"status\": \"VERIFIED\""); + assertThat(json).contains(" },\n {\n \"id\": \"inv_2\","); + assertThat(json) + .contains(" {\n \"id\": \"inv_2\",\n \"status\": \"INCONCLUSIVE\""); + assertThat(json).endsWith(" }\n ]\n}"); + } + + @Test + public void formatTextResults_verified() throws Exception { + VerificationOptions options = VerificationOptions.builder().build(); + CelVerificationResult result = + CelVerifierToolCore.checkSatisfiable("true", ImmutableMap.of(), options); + + String text = FormatUtils.formatTextResult(result); + + assertThat(text).contains("[VERIFIED]"); + } + + @Test + public void formatTextPolicyResults_inconclusive() throws Exception { + VerificationOptions options = VerificationOptions.builder().build(); + CelVerificationResult result = + CelVerifierToolCore.checkValid("int('123') == 123", ImmutableMap.of(), options); + ImmutableMap results = ImmutableMap.of("inv_1", result); + + String text = FormatUtils.formatTextPolicyResults("test_policy", results); + + assertThat(text).contains("Policy Invariant Verification for 'test_policy':"); + assertThat(text).contains("Invariant 'inv_1': INCONCLUSIVE"); + } + + @Test + public void escapeJson_escapesControlCharactersAndQuotes() { + String input = "Hello \"world\"\nLine 2\t\u0000\u001b"; + + String escaped = FormatUtils.escapeJson(input); + + assertThat(escaped).isEqualTo("Hello \\\"world\\\"\\nLine 2\\t\\u0000\\u001b"); + } +} diff --git a/verifier/src/test/java/dev/cel/verifier/tools/VerificationOptionsTest.java b/verifier/src/test/java/dev/cel/verifier/tools/VerificationOptionsTest.java new file mode 100644 index 000000000..28aac751a --- /dev/null +++ b/verifier/src/test/java/dev/cel/verifier/tools/VerificationOptionsTest.java @@ -0,0 +1,103 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.tools; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import dev.cel.common.types.CelType; +import dev.cel.common.types.SimpleType; +import dev.cel.verifier.tools.VerificationOptions.OutputFormat; +import java.time.Duration; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class VerificationOptionsTest { + + @Test + public void defaultOptions() { + VerificationOptions options = VerificationOptions.builder().build(); + + assertThat(options.getTimeout()).isEqualTo(VerificationOptions.DEFAULT_TIMEOUT); + assertThat(options.getComprehensionUnrollLimit()) + .isEqualTo(VerificationOptions.DEFAULT_COMPREHENSION_UNROLL_LIMIT); + assertThat(options.getUnknownIdentifiers()).isEmpty(); + assertThat(options.getOutputFormat()).isEqualTo(VerificationOptions.DEFAULT_OUTPUT_FORMAT); + } + + @Test + public void customOptions_allFieldsSet() { + VerificationOptions options = + VerificationOptions.builder() + .setTimeout(Duration.ofSeconds(25)) + .setComprehensionUnrollLimit(12) + .setUnknownIdentifiers(ImmutableList.of("req.auth", "req.headers")) + .setOutputFormat(OutputFormat.JSON) + .build(); + + assertThat(options.getTimeout()).isEqualTo(Duration.ofSeconds(25)); + assertThat(options.getComprehensionUnrollLimit()).isEqualTo(12); + assertThat(options.getUnknownIdentifiers()) + .containsExactly("req.auth", "req.headers") + .inOrder(); + assertThat(options.getOutputFormat()).isEqualTo(OutputFormat.JSON); + } + + @Test + public void setTimeout_null_throwsException() { + VerificationOptions.Builder builder = VerificationOptions.builder(); + assertThrows(NullPointerException.class, () -> builder.setTimeout(null)); + } + + @Test + public void setComprehensionUnrollLimit_negative_throwsException() { + VerificationOptions.Builder builder = VerificationOptions.builder(); + assertThrows(IllegalArgumentException.class, () -> builder.setComprehensionUnrollLimit(-1)); + } + + @Test + public void setOutputFormat_null_throwsException() { + VerificationOptions.Builder builder = VerificationOptions.builder(); + assertThrows(NullPointerException.class, () -> builder.setOutputFormat(null)); + } + + @Test + public void parseVariables_validSpecs() { + ImmutableMap vars = + VerificationOptions.parseVariables(ImmutableList.of("x:int", "name:string", "flag:bool")); + + assertThat(vars) + .containsExactly( + "x", SimpleType.INT, + "name", SimpleType.STRING, + "flag", SimpleType.BOOL); + } + + @Test + public void parseVariables_nullOrEmpty_returnsEmptyMap() { + assertThat(VerificationOptions.parseVariables(null)).isEmpty(); + assertThat(VerificationOptions.parseVariables(ImmutableList.of())).isEmpty(); + } + + @Test + public void parseVariables_invalidSpec_throwsException() { + ImmutableList specs = ImmutableList.of("invalid_spec_without_colon"); + assertThrows(IllegalArgumentException.class, () -> VerificationOptions.parseVariables(specs)); + } +} diff --git a/verifier/tools/BUILD.bazel b/verifier/tools/BUILD.bazel new file mode 100644 index 000000000..a547c15b2 --- /dev/null +++ b/verifier/tools/BUILD.bazel @@ -0,0 +1,19 @@ +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//verifier:verifier_internal"], +) + +alias( + name = "tools", + actual = "//verifier/src/main/java/dev/cel/verifier/tools:tools_lib", +) + +alias( + name = "tools_lib", + actual = "//verifier/src/main/java/dev/cel/verifier/tools:tools_lib", +) + +alias( + name = "cel_verifier_tool", + actual = "//verifier/src/main/java/dev/cel/verifier/tools:cel_verifier_tool", +) diff --git a/verifier/tools/README.md b/verifier/tools/README.md new file mode 100644 index 000000000..f2cfa2528 --- /dev/null +++ b/verifier/tools/README.md @@ -0,0 +1,107 @@ +# CEL Java Verifier CLI & Interactive REPL Tool + +The CEL Java Verifier comes with a command-line tool (`cel-verifier`) and an +interactive REPL shell for testing satisfiability, validity, equivalence, +and policy invariants without writing Java code. + +## Running the CLI Tool + +### Running via Bazel + +```bash +# Run CLI verification commands +bazel run //verifier/tools:cel_verifier_tool -- \ + check-sat \ + --expr "role == 'editor' && port > 1024" \ + --var "role:string" \ + --var "port:int" + +# Run with JSON output format for CI/CD integrations +bazel run //verifier/tools:cel_verifier_tool -- \ + check-sat \ + --expr "role == 'editor'" \ + --var "role:string" \ + --output_format=json + +# Launch interactive REPL shell +bazel run //verifier/tools:cel_verifier_tool -- repl +``` + +### Running via Maven Central + +> **Note:** Executable binaries and Maven packages (`dev.cel:cel-verifier`) +> will be published to Maven Central in an upcoming release. + +## CLI Commands + +* `check-sat --expr "..."`: Verifies satisfiability of an expression and + prints witness inputs if satisfiable. +* `check-valid --expr "..."`: Proves validity (`isAlwaysTrue`) and prints + a counterexample if invalid. +* `verify-equiv --expr1 "..." --expr2 "..."`: Proves logical equivalence + between two CEL expressions. +* `verify-policy --file policy.yaml`: Verifies policy invariants defined + in a YAML policy file. +* `repl`: Enters interactive verification shell mode. + +## Command Options + +The verification commands (`check-sat`, `check-valid`, `verify-equiv`, +`verify-policy`) accept the following options: + +### Variable Declarations (`--var`, `-v`) + +Declare variables in `name:type` format. Multiple variables can be declared by +repeating the `--var` option. + +Supported types: + +* Primitive types: `int`, `uint`, `string`, `bool`, `double`, `bytes`, `dyn` +* List types: `list` (e.g., `--var "tags:list"`) +* Map types: `map` (e.g., `--var "scores:map"`) + +Examples: +```bash +--var "role:string" --var "port:int" --var "tags:list" +``` + +### Unknown Identifiers (`--unknown`, `-u`) + +Permit specific identifiers or attributes (e.g., `request.headers`) to +evaluate to `Unknown` during verification: + +```bash +--unknown "request.headers" --unknown "auth.credentials" +``` + +### Solver Timeout (`--timeout`) + +Set maximum Z3 SMT solver timeout in seconds (default: `10`): + +```bash +--timeout 15 +``` + +### Comprehension Unroll Limit (`--unroll-limit`) + +Set bounded unroll limit for comprehensions and loop macros like `.all()` and +`.exists()` (default: `5`): + +```bash +--unroll-limit 10 +``` + +### Output Format (`--output_format`, `-fmt`) + +Set CLI output format (`TEXT` or `JSON`, default: `TEXT`): + +```bash +--output_format json +``` + +## Exit Codes + +* `0`: Verification succeeded / condition verified. +* `1`: Violation or counterexample found. +* `2`: Inconclusive result (solver unknown or timeout). +* `3`: Error (syntax compilation error, missing file, or execution error). From 75e090067b4fcdbc69d9b65056d5323901d9bb17 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Wed, 5 Aug 2026 14:04:32 -0700 Subject: [PATCH 160/204] Add REPL for Verifier PiperOrigin-RevId: 959858257 --- BUILD.bazel | 8 + MODULE.bazel | 2 + .../java/dev/cel/verifier/tools/BUILD.bazel | 3 + .../cel/verifier/tools/CelVerifierRepl.java | 435 ++++++++++++++++++ .../cel/verifier/tools/CelVerifierTool.java | 14 +- .../verifier/tools/VerificationOptions.java | 4 + .../verifier/tools/CelVerifierReplTest.java | 188 ++++++++ verifier/tools/README.md | 84 ++++ 8 files changed, 736 insertions(+), 2 deletions(-) create mode 100644 verifier/src/main/java/dev/cel/verifier/tools/CelVerifierRepl.java create mode 100644 verifier/src/test/java/dev/cel/verifier/tools/CelVerifierReplTest.java diff --git a/BUILD.bazel b/BUILD.bazel index 024908625..d2bf2124b 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -95,6 +95,14 @@ java_library( ], ) +java_library( + name = "java_jline", + exports = [ + "@maven//:org_jline_jline_reader", + "@maven//:org_jline_jline_terminal", + ], +) + default_java_toolchain( name = "repository_default_toolchain", configuration = DEFAULT_TOOLCHAIN_CONFIGURATION, diff --git a/MODULE.bazel b/MODULE.bazel index ce9c67fde..3dcf8b0e5 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -95,6 +95,8 @@ maven.install( "info.picocli:picocli:4.7.7", "org.antlr:antlr4-runtime:4.13.2", "org.freemarker:freemarker:2.3.34", + "org.jline:jline-reader:3.26.1", + "org.jline:jline-terminal:3.26.1", "org.jspecify:jspecify:1.0.0", "org.threeten:threeten-extra:1.8.0", "org.yaml:snakeyaml:2.5", diff --git a/verifier/src/main/java/dev/cel/verifier/tools/BUILD.bazel b/verifier/src/main/java/dev/cel/verifier/tools/BUILD.bazel index e4339f857..28ce776cb 100644 --- a/verifier/src/main/java/dev/cel/verifier/tools/BUILD.bazel +++ b/verifier/src/main/java/dev/cel/verifier/tools/BUILD.bazel @@ -28,6 +28,7 @@ EOF java_library( name = "tools_lib", srcs = [ + "CelVerifierRepl.java", "CelVerifierTool.java", "CelVerifierToolCore.java", "FormatUtils.java", @@ -38,11 +39,13 @@ java_library( "alt_dep=//verifier/tools", ], deps = [ + "//:java_jline", "//bundle:cel", "//common:cel_ast", "//common:compiler_common", "//common:options", "//common/types", + "//common/types:cel_types", "//common/types:type_providers", "//compiler", "//compiler:compiler_builder", diff --git a/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierRepl.java b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierRepl.java new file mode 100644 index 000000000..25354480a --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierRepl.java @@ -0,0 +1,435 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.tools; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.base.Ascii; +import com.google.common.collect.ImmutableList; +import dev.cel.common.CelValidationException; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypes; +import dev.cel.policy.CelPolicyValidationException; +import dev.cel.verifier.CelVerificationResult; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import org.jline.reader.EndOfFileException; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.UserInterruptException; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; + +/** Interactive REPL shell for CEL formal verification. */ +final class CelVerifierRepl { + + private CelVerifierRepl() {} + + static int runInteractiveRepl() { + LineReader lineReader = null; + BufferedReader fallbackReader = null; + try { + Terminal terminal = TerminalBuilder.builder().system(true).build(); + lineReader = LineReaderBuilder.builder().terminal(terminal).build(); + } catch (Exception e) { + fallbackReader = new BufferedReader(new InputStreamReader(System.in, UTF_8)); + } + return runReplInternal(lineReader, fallbackReader, System.out, System.err); + } + + static int runRepl(BufferedReader reader, PrintStream out, PrintStream err) { + return runReplInternal(null, reader, out, err); + } + + private static int runReplInternal( + LineReader lineReader, BufferedReader fallbackReader, PrintStream out, PrintStream err) { + out.println("============================================================"); + out.println(" CEL Verification REPL"); + out.println(" Type :help for commands, :quit to exit."); + out.println("============================================================"); + + Map sessionVars = new HashMap<>(); + List unknownIdentifiers = new ArrayList<>(); + int timeoutSeconds = 10; + int unrollLimit = 5; + + String prompt = FormatUtils.ANSI_CYAN + "cel-verifier> " + FormatUtils.ANSI_RESET; + + while (true) { + String line; + try { + if (lineReader != null) { + line = lineReader.readLine(prompt); + } else if (fallbackReader != null) { + out.print(prompt); + out.flush(); + line = fallbackReader.readLine(); + if (line == null) { + break; // EOF + } + } else { + break; + } + } catch (UserInterruptException | EndOfFileException e) { + out.println("Goodbye!"); + break; + } catch (Exception e) { + err.println("Error reading input: " + e.getMessage()); + break; + } + + line = line.trim(); + if (line.isEmpty()) { + continue; + } + + if (line.startsWith(":")) { + if (Ascii.equalsIgnoreCase(line, ":quit") || Ascii.equalsIgnoreCase(line, ":exit")) { + out.println("Goodbye!"); + break; + } + + Optional helpArg = extractCommandArg(line, ":help"); + if (helpArg.isPresent()) { + printHelp(helpArg.get(), out); + continue; + } + + if (Ascii.equalsIgnoreCase(line, ":vars")) { + printVars(sessionVars, unknownIdentifiers, timeoutSeconds, unrollLimit, out); + continue; + } + + if (Ascii.equalsIgnoreCase(line, ":clear")) { + sessionVars.clear(); + unknownIdentifiers.clear(); + out.println("Session state reset."); + continue; + } + + Optional varArg = extractCommandArg(line, ":var"); + if (varArg.isPresent()) { + String arg = varArg.get(); + if (arg.isEmpty()) { + err.println( + "Usage: :var (e.g. :var role string, :var scores map)"); + } else { + handleVarCommand(arg, sessionVars, out, err); + } + continue; + } + + Optional unknownArg = extractCommandArg(line, ":unknown"); + if (unknownArg.isPresent()) { + String arg = unknownArg.get(); + if (arg.isEmpty()) { + err.println("Usage: :unknown "); + } else { + unknownIdentifiers.add(arg); + out.println("Added unknown identifier: '" + arg + "'"); + } + continue; + } + + Optional timeoutArg = extractCommandArg(line, ":timeout"); + if (timeoutArg.isPresent()) { + String arg = timeoutArg.get(); + if (arg.isEmpty()) { + err.println("Usage: :timeout "); + } else { + try { + int t = Integer.parseInt(arg); + if (t <= 0) { + err.println("Timeout must be a positive integer."); + } else { + timeoutSeconds = t; + out.println("Timeout set to " + timeoutSeconds + "s."); + } + } catch (NumberFormatException e) { + err.println("Invalid timeout value."); + } + } + continue; + } + + Optional unrollArg = extractCommandArg(line, ":unroll"); + if (unrollArg.isPresent()) { + String arg = unrollArg.get(); + if (arg.isEmpty()) { + err.println("Usage: :unroll "); + } else { + try { + int u = Integer.parseInt(arg); + if (u < 0) { + err.println("Unroll limit must be non-negative."); + } else { + unrollLimit = u; + out.println("Comprehension unroll limit set to " + unrollLimit + "."); + } + } catch (NumberFormatException e) { + err.println("Invalid unroll limit value."); + } + } + continue; + } + + err.println("Unknown command: " + line + ". Type :help for commands."); + continue; + } + + // Handle queries + VerificationOptions options = + VerificationOptions.builder() + .setTimeout(Duration.ofSeconds(timeoutSeconds)) + .setComprehensionUnrollLimit(unrollLimit) + .setUnknownIdentifiers(unknownIdentifiers) + .build(); + + try { + Optional satArg = extractCommandArg(line, "sat"); + Optional validArg = extractCommandArg(line, "valid"); + Optional equivArg = extractCommandArg(line, "equiv"); + + if (satArg.isPresent()) { + String arg = satArg.get(); + if (arg.isEmpty()) { + err.println("Usage: sat "); + } else { + CelVerificationResult res = + CelVerifierToolCore.checkSatisfiable(arg, sessionVars, options); + out.println(FormatUtils.formatTextResult(res)); + } + } else if (validArg.isPresent()) { + String arg = validArg.get(); + if (arg.isEmpty()) { + err.println("Usage: valid "); + } else { + CelVerificationResult res = CelVerifierToolCore.checkValid(arg, sessionVars, options); + out.println(FormatUtils.formatTextResult(res)); + } + } else if (equivArg.isPresent()) { + String arg = equivArg.get(); + ImmutableList parts = splitEquivQuery(arg); + if (parts.size() != 2 || parts.get(0).isEmpty() || parts.get(1).isEmpty()) { + err.println("Equivalence query format: equiv <=> "); + } else { + String exprA = parts.get(0).trim(); + String exprB = parts.get(1).trim(); + CelVerificationResult res = + CelVerifierToolCore.verifyEquivalence(exprA, exprB, sessionVars, options); + out.println(FormatUtils.formatTextResult(res)); + } + } else { + // Default: treat as sat query + CelVerificationResult res = + CelVerifierToolCore.checkSatisfiable(line, sessionVars, options); + out.println(FormatUtils.formatTextResult(res)); + } + } catch (CelValidationException e) { + err.println( + FormatUtils.ANSI_RED + + "Compilation error:\n" + + e.getMessage() + + FormatUtils.ANSI_RESET); + } catch (CelPolicyValidationException e) { + err.println( + FormatUtils.ANSI_RED + + "Policy compilation error:\n" + + e.getMessage() + + FormatUtils.ANSI_RESET); + } catch (Exception e) { + err.println( + FormatUtils.ANSI_RED + + "Verification failed: " + + e.getMessage() + + FormatUtils.ANSI_RESET); + } + } + return 0; + } + + private static void handleVarCommand( + String arg, Map sessionVars, PrintStream out, PrintStream err) { + String[] parts = arg.split("\\s+", 2); + if (parts.length != 2) { + err.println("Usage: :var (e.g. :var role string, :var scores map)"); + return; + } + String name = parts[0].trim(); + String typeStr = parts[1].trim(); + try { + CelType type = VerificationOptions.parseCelType(typeStr); + sessionVars.put(name, type); + out.println("Variable declared: " + name + " : " + CelTypes.format(type)); + } catch (IllegalArgumentException e) { + err.println(e.getMessage()); + } + } + + private static void printVars( + Map sessionVars, + List unknowns, + int timeoutSeconds, + int unrollLimit, + PrintStream out) { + out.println("--- Session State ---"); + out.println("Timeout: " + timeoutSeconds + "s | Unroll limit: " + unrollLimit); + out.println("Unknowns: " + (unknowns.isEmpty() ? "none" : unknowns)); + out.println("Variables (" + sessionVars.size() + "):"); + for (Map.Entry entry : sessionVars.entrySet()) { + out.println(" " + entry.getKey() + " : " + CelTypes.format(entry.getValue())); + } + } + + private static void printHelp(String topic, PrintStream out) { + String t = topic.toLowerCase(Locale.US).replace(":", "").trim(); + switch (t) { + case "var": + case "vars": + out.println("Command: :var "); + out.println("Declares a variable in the REPL session with a specific type."); + out.println(); + out.println("Supported Types:"); + out.println(" - Primitive types: int, uint, string, bool, double, bytes"); + out.println(" - List types: list (e.g., list, list)"); + out.println(" - Map types: map (e.g., map, map)"); + out.println(); + out.println("Examples:"); + out.println(" cel-verifier> :var role string"); + out.println(" cel-verifier> :var port int"); + out.println(" cel-verifier> :var scores map"); + out.println(" cel-verifier> :var tags list"); + break; + case "unknown": + out.println("Command: :unknown "); + out.println( + "Marks an identifier path as 'Unknown' during verification (partial evaluation)."); + out.println(); + out.println("Examples:"); + out.println(" cel-verifier> :unknown request.headers"); + out.println(" cel-verifier> :unknown request.auth.claims"); + break; + case "timeout": + out.println("Command: :timeout "); + out.println("Configures the Z3 solver soft timeout duration in seconds (default: 10s)."); + out.println(); + out.println("Examples:"); + out.println(" cel-verifier> :timeout 5"); + break; + case "unroll": + out.println("Command: :unroll "); + out.println("Configures the BMC loop unroll limit for comprehensions (default: 5)."); + out.println(); + out.println("Examples:"); + out.println(" cel-verifier> :unroll 3"); + break; + case "sat": + out.println("Query: sat "); + out.println( + "Checks if a CEL expression can evaluate to true for any possible input assignments."); + out.println("If satisfiable, outputs concrete satisfying witness values."); + out.println(); + out.println("Examples:"); + out.println(" cel-verifier> sat role == 'editor' && port > 1024"); + out.println(" cel-verifier> sat scores['alice'] > 90"); + break; + case "valid": + out.println("Query: valid "); + out.println( + "Proves whether a CEL expression evaluates to true for ALL possible input" + + " assignments."); + out.println("If invalid, outputs a counterexample showing inputs causing it to fail."); + out.println(); + out.println("Examples:"); + out.println(" cel-verifier> valid x > 10 || x <= 10"); + break; + case "equiv": + out.println("Query: equiv <=> "); + out.println( + "Proves whether two CEL expressions are semantically identical for all inputs."); + out.println( + "If not equivalent, outputs a counterexample showing inputs where they diverge."); + out.println(); + out.println("Use '<=>' as the recommended separator between expressions."); + out.println(); + out.println("Examples:"); + out.println(" cel-verifier> equiv x > 10 <=> 10 < x"); + out.println(" cel-verifier> equiv (a && b) || (a && c) <=> a && (b || c)"); + out.println( + " cel-verifier> equiv string_int_map == {'a': 1, 'b': 2} ? string_int_map.all(k, k ==" + + " 'a') : true <=> string_int_map == {'a': 1, 'b': 2} ? string_int_map.all(k, k ==" + + " 'a') : true"); + break; + default: + out.println("REPL Commands:"); + out.println( + " :var Declare variable (e.g. :var role string, :var m" + + " map)"); + out.println(" :unknown Mark identifier as unknown"); + out.println(" :timeout Set solver timeout (default: 10s)"); + out.println(" :unroll Set comprehension unroll limit (default: 5)"); + out.println(" :vars List session variables & options"); + out.println(" :clear Reset session state"); + out.println( + " :help [command] Display help message or specific command details"); + out.println(" :quit Exit REPL"); + out.println(); + out.println("Verification Queries:"); + out.println(" sat Check satisfiability"); + out.println(" valid Check validity (always true)"); + out.println(" equiv <=> Prove logical equivalence"); + out.println(" Check satisfiability (default)"); + out.println(); + out.println( + "Type ':help ' (e.g. ':help var', ':help sat') for detailed usage and" + + " examples."); + break; + } + } + + private static ImmutableList splitEquivQuery(String rest) { + if (rest == null || rest.trim().isEmpty()) { + return ImmutableList.of(); + } + String input = rest.trim(); + if (input.contains(" <=> ")) { + return ImmutableList.copyOf(input.split(" <=> ", 2)); + } + if (input.contains("<=>")) { + return ImmutableList.copyOf(input.split("<=>", 2)); + } + return ImmutableList.of(); + } + + private static Optional extractCommandArg(String line, String prefix) { + if (Ascii.equalsIgnoreCase(line, prefix)) { + return Optional.of(""); + } + String prefixLower = Ascii.toLowerCase(prefix); + String lineLower = Ascii.toLowerCase(line); + if (lineLower.startsWith(prefixLower + " ") || lineLower.startsWith(prefixLower + "\t")) { + return Optional.of(line.substring(prefix.length()).trim()); + } + return Optional.empty(); + } +} diff --git a/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierTool.java b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierTool.java index 8289b9f77..963e966eb 100644 --- a/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierTool.java +++ b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierTool.java @@ -43,12 +43,13 @@ name = "cel-verifier", mixinStandardHelpOptions = true, versionProvider = CelVerifierTool.VersionProvider.class, - description = "CEL-Java Formal Verification CLI Tool", + description = "CEL-Java Formal Verification CLI & REPL Tool", subcommands = { CelVerifierTool.CheckSatCommand.class, CelVerifierTool.CheckValidCommand.class, CelVerifierTool.VerifyEquivCommand.class, - CelVerifierTool.VerifyPolicyCommand.class + CelVerifierTool.VerifyPolicyCommand.class, + CelVerifierTool.ReplCommand.class }) public final class CelVerifierTool implements Runnable { @@ -296,6 +297,15 @@ private static int getPolicyExitCode(ImmutableMap } } + @Command(name = "repl", description = "Launch interactive CEL Formal Verification REPL shell") + static class ReplCommand implements Callable { + + @Override + public Integer call() { + return CelVerifierRepl.runInteractiveRepl(); + } + } + public static void main(String[] args) { int exitCode = new CommandLine(new CelVerifierTool()).execute(args); System.exit(exitCode); diff --git a/verifier/src/main/java/dev/cel/verifier/tools/VerificationOptions.java b/verifier/src/main/java/dev/cel/verifier/tools/VerificationOptions.java index 91ec443a5..f2b3bf742 100644 --- a/verifier/src/main/java/dev/cel/verifier/tools/VerificationOptions.java +++ b/verifier/src/main/java/dev/cel/verifier/tools/VerificationOptions.java @@ -135,6 +135,10 @@ static ImmutableMap parseVariables(List varSpecs) { + "'. Expected format 'name:type' (e.g., 'x:int')."); } String name = parts[0].trim(); + if (name.isEmpty()) { + throw new IllegalArgumentException( + "Invalid variable specification: '" + varSpec + "'. Variable name cannot be empty."); + } String typeStr = parts[1].trim().toLowerCase(Locale.US); CelType type = parseCelType(typeStr); vars.put(name, type); diff --git a/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierReplTest.java b/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierReplTest.java new file mode 100644 index 000000000..52124c6ed --- /dev/null +++ b/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierReplTest.java @@ -0,0 +1,188 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier.tools; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.StringReader; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class CelVerifierReplTest { + + @Before + public void setUp() { + System.setProperty("z3.skipLibraryLoad", "true"); + } + + @SuppressWarnings({"PreferCharsetOverload", "JdkObsolete"}) + private String[] runReplWithCommands(String... commands) throws Exception { + String input = String.join("\n", commands) + "\n"; + BufferedReader reader = new BufferedReader(new StringReader(input)); + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + ByteArrayOutputStream errStream = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(outStream, true, UTF_8.name()); + PrintStream err = new PrintStream(errStream, true, UTF_8.name()); + + CelVerifierRepl.runRepl(reader, out, err); + + return new String[] { + new String(outStream.toByteArray(), UTF_8), new String(errStream.toByteArray(), UTF_8) + }; + } + + @Test + public void repl_quitAndExit() throws Exception { + String[] output1 = runReplWithCommands(":quit"); + assertThat(output1[0]).contains("Goodbye!"); + + String[] output2 = runReplWithCommands(":exit"); + assertThat(output2[0]).contains("Goodbye!"); + } + + @Test + public void repl_helpCommands() throws Exception { + String[] output = + runReplWithCommands( + ":help", + ":help var", + ":help unknown", + ":help timeout", + ":help unroll", + ":help sat", + ":help valid", + ":help equiv", + ":help non_existent_topic", + ":quit"); + assertThat(output[0]).contains("REPL Commands:"); + assertThat(output[0]).contains("Command: :var "); + assertThat(output[0]).contains("Command: :unknown "); + assertThat(output[0]).contains("Command: :timeout "); + assertThat(output[0]).contains("Command: :unroll "); + assertThat(output[0]).contains("Query: sat "); + assertThat(output[0]).contains("Query: valid "); + assertThat(output[0]).contains("Query: equiv <=> "); + } + + @Test + public void repl_varDeclarations() throws Exception { + String[] output = + runReplWithCommands( + ":var role string", + ":var port int", + ":var scores map", + ":var tags list", + ":vars", + ":quit"); + assertThat(output[0]).contains("Variable declared: role : string"); + assertThat(output[0]).contains("Variable declared: port : int"); + assertThat(output[0]).contains("Variable declared: scores : map(string, int)"); + assertThat(output[0]).contains("Variable declared: tags : list(string)"); + assertThat(output[0]).contains("Variables (4):"); + } + + @Test + public void repl_unknownIdentifiers() throws Exception { + String[] output = + runReplWithCommands(":unknown request.headers", ":unknown request.auth", ":vars", ":quit"); + assertThat(output[0]).contains("Added unknown identifier: 'request.headers'"); + assertThat(output[0]).contains("Added unknown identifier: 'request.auth'"); + assertThat(output[0]).contains("Unknowns: [request.headers, request.auth]"); + } + + @Test + public void repl_timeoutConfiguration() throws Exception { + String[] output = + runReplWithCommands( + ":timeout 15", ":vars", ":timeout -5", ":timeout abc", ":timeout", ":quit"); + assertThat(output[0]).contains("Timeout set to 15s."); + assertThat(output[0]).contains("Timeout: 15s"); + assertThat(output[1]).contains("Timeout must be a positive integer."); + assertThat(output[1]).contains("Invalid timeout value."); + assertThat(output[1]).contains("Usage: :timeout "); + } + + @Test + public void repl_unrollConfiguration() throws Exception { + String[] output = + runReplWithCommands(":unroll 10", ":vars", ":unroll -1", ":unroll xyz", ":unroll", ":quit"); + assertThat(output[0]).contains("Comprehension unroll limit set to 10."); + assertThat(output[0]).contains("Unroll limit: 10"); + assertThat(output[1]).contains("Unroll limit must be non-negative."); + assertThat(output[1]).contains("Invalid unroll limit value."); + assertThat(output[1]).contains("Usage: :unroll "); + } + + @Test + public void repl_sessionStateAndClear() throws Exception { + String[] output = + runReplWithCommands( + ":var role string", ":unknown req.headers", ":vars", ":clear", ":vars", ":quit"); + assertThat(output[0]).contains("Variables (1):"); + assertThat(output[0]).contains("Session state reset."); + assertThat(output[0]).contains("Variables (0):"); + assertThat(output[0]).contains("Unknowns: none"); + } + + @Test + public void repl_satQueries() throws Exception { + String[] output = + runReplWithCommands(":var port int", "sat port > 1024", "port > 1024", "sat", ":quit"); + assertThat(output[0]).contains("[VERIFIED]"); + assertThat(output[1]).contains("Usage: sat "); + } + + @Test + public void repl_validQueries() throws Exception { + String[] output = + runReplWithCommands(":var x int", "valid x > 0 || x <= 0", "valid x > 0", "valid", ":quit"); + assertThat(output[0]).contains("[VERIFIED]"); + assertThat(output[0]).contains("[VIOLATED]"); + assertThat(output[1]).contains("Usage: valid "); + } + + @Test + public void repl_equivQueries() throws Exception { + String[] output = + runReplWithCommands(":var x int", "equiv x > 10 <=> 10 < x", "equiv x > 10", ":quit"); + assertThat(output[0]).contains("[VERIFIED]"); + assertThat(output[1]).contains("Equivalence query format: equiv <=> "); + } + + @Test + public void repl_unknownCommandsAndErrors() throws Exception { + String[] output = + runReplWithCommands( + ":unknowncommand", + ":var", + ":var invalid_spec", + ":var x foo_type", + ":unknown", + "invalid + + syntax", + ":quit"); + assertThat(output[1]).contains("Unknown command: :unknowncommand"); + assertThat(output[1]).contains("Usage: :var "); + assertThat(output[1]).contains("Unsupported type"); + assertThat(output[1]).contains("Usage: :unknown "); + assertThat(output[1]).contains("Compilation error"); + } +} diff --git a/verifier/tools/README.md b/verifier/tools/README.md index f2cfa2528..398cbad74 100644 --- a/verifier/tools/README.md +++ b/verifier/tools/README.md @@ -105,3 +105,87 @@ Set CLI output format (`TEXT` or `JSON`, default: `TEXT`): * `1`: Violation or counterexample found. * `2`: Inconclusive result (solver unknown or timeout). * `3`: Error (syntax compilation error, missing file, or execution error). + +## Interactive REPL Shell + +The REPL shell provides an interactive, stateful environment to execute CEL +formal verification queries without re-declaring variables or re-running CLI +parameters for every query. + +### Launching the REPL + +```bash +bazel run //verifier/tools:cel_verifier_tool -- repl +``` + +### REPL Commands + +| Command | Description | Example | +|---|---|---| +| `:var ` | Declare a variable in session state | `:var role string` | +| `:unknown ` | Mark identifier as Unknown | `:unknown request.headers` | +| `:timeout ` | Set Z3 solver timeout in seconds (default: 10s) | `:timeout 5` | +| `:unroll ` | Set comprehension unroll limit (default: 5) | `:unroll 3` | +| `:vars` | Display declared session variables & config | `:vars` | +| `:clear` | Reset session state (clears variables & unknowns) | `:clear` | +| `:help [cmd]` | Display built-in help or command details | `:help var` | +| `:quit` / `:exit` | Exit the interactive REPL shell | `:quit` | + +### Verification Queries in REPL + +* **Satisfiability (`sat ` or ``):** Checks if the expression + can evaluate to `true` for any assignment of session variables. Outputs + satisfying witness inputs if satisfiable. +* **Validity (`valid `):** Proves whether the expression evaluates + to `true` for ALL possible variable assignments. Outputs a counterexample + if invalid. +* **Equivalence (`equiv <=> `):** Proves whether two + expressions are logically identical across all inputs. Outputs a + counterexample if not equivalent. + +### Example REPL Session + +```text +============================================================ + CEL Verification REPL + Type :help for commands, :quit to exit. +============================================================ +cel-verifier> :var port int +Variable declared: port : int + +cel-verifier> sat role == 'admin' && port > 1024 + +cel-verifier> :var role string +Variable declared: role : string + +cel-verifier> sat role == 'admin' && port > 1024 +[VERIFIED] Condition is satisfiable. Satisfying input: + role = "admin" + port = 1025 + +cel-verifier> valid port > 0 || port <= 0 +[VERIFIED] + +cel-verifier> valid port > 1024 +[VIOLATED] Condition is violated. Counterexample input: + port = 0 + +cel-verifier> equiv port > 10 <=> 10 < port +[VERIFIED] + +cel-verifier> :vars +--- Session State --- +Timeout: 10s | Unroll limit: 5 +Unknowns: none +Variables (2): + role : string + port : int + +cel-verifier> :quit +Goodbye! +``` + +> **Note:** Inline help is built into the REPL shell. Type `:help` or +> `:help ` (e.g. `:help var`, `:help equiv`) at any prompt for +> detailed usage instructions and examples. + From 54ecf526783109a0ef6a2e54ae3fede1e5010e58 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Wed, 5 Aug 2026 14:24:55 -0700 Subject: [PATCH 161/204] Prevent spurious counterexamples on maps by tightening its domain PiperOrigin-RevId: 959869777 --- .../cel/verifier/CelAstToZ3Translator.java | 15 +++--- .../dev/cel/verifier/CelVerifierZ3Impl.java | 6 ++- .../CelZ3CounterexampleGenerator.java | 51 ++++++++++++------- .../dev/cel/verifier/axioms/GreaterAxiom.java | 13 ++--- .../verifier/axioms/GreaterEqualsAxiom.java | 13 ++--- .../dev/cel/verifier/axioms/LessAxiom.java | 13 ++--- .../cel/verifier/axioms/LessEqualsAxiom.java | 13 ++--- .../verifier/axioms/TypeConversionAxioms.java | 4 +- .../test/java/dev/cel/verifier/BUILD.bazel | 2 +- .../cel/verifier/CelVerifierZ3ImplTest.java | 47 +++++++++++++++-- 10 files changed, 108 insertions(+), 69 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java index c1a8848e2..e253b27ad 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java @@ -741,7 +741,7 @@ private TranslatedValue translateCall(CelExpr expr, CelAbstractSyntaxTree ast) { typeConstraints.add(ctx.mkNot(typeSystem.isUnknown(callRes))); typeConstraints.add(ctx.mkNot(typeSystem.isError(callRes))); - boolean isDynamic = ast.getType(exprId).map(SimpleType.DYN::equals).orElse(true); + boolean isDynamic = ast.getTypeOrThrow(exprId).equals(SimpleType.DYN); BoolExpr isApprox = ctx.mkBool(!isDynamic); return TranslatedValue.propagateStrict( ctx, typeSystem, callRes, Optional.of(expr), isApprox, args); @@ -877,10 +877,6 @@ private TranslatedValue translateDynamicComprehension( ArrayExpr mapPresence = isMap ? (ArrayExpr) typeSystem.getMapPresence(typeSystem.getMapRef(iterRange)) : null; - if (isMap) { - applyBoundedMapBijection(mapPresence, seq, lengthExpr); - } - BoolExpr isTruncated = ctx.mkGt(lengthExpr, ctx.mkInt(comprehensionUnrollLimit)); truncationConditions.add(isTruncated); @@ -893,14 +889,15 @@ private TranslatedValue translateDynamicComprehension( } } - private void applyBoundedMapBijection( + private BoolExpr getBoundedMapBijection( ArrayExpr mapPresence, SeqExpr seq, ArithExpr lengthExpr) { + List constraints = new ArrayList<>(); for (int i = 0; i < comprehensionUnrollLimit; i++) { for (int j = i + 1; j < comprehensionUnrollLimit; j++) { BoolExpr validPair = ctx.mkLt(ctx.mkInt(j), lengthExpr); BoolExpr notEqual = ctx.mkNot(ctx.mkEq(ctx.mkNth(seq, ctx.mkInt(i)), ctx.mkNth(seq, ctx.mkInt(j)))); - typeConstraints.add(ctx.mkImplies(validPair, notEqual)); + constraints.add(ctx.mkImplies(validPair, notEqual)); } } @@ -915,7 +912,8 @@ private void applyBoundedMapBijection( ctx.mkStore(seqMap, ctx.mkNth(seq, ctx.mkInt(i)), ctx.mkTrue()), seqMap); } - typeConstraints.add(ctx.mkImplies(isNotTruncated, ctx.mkEq(mapPresence, seqMap))); + constraints.add(ctx.mkImplies(isNotTruncated, ctx.mkEq(mapPresence, seqMap))); + return CelZ3TypeSystem.mkAndFlattened(ctx, constraints); } private TranslatedValue[] evaluateLoopCondAndStep( @@ -1335,6 +1333,7 @@ private BoolExpr createTypeConstraintForType(Expr val, CelType type) { List boundsAndTypes = new ArrayList<>(); boundsAndTypes.add(isMap); + boundsAndTypes.add(getBoundedMapBijection(mapPresence, seq, (ArithExpr) length)); for (int i = 0; i < comprehensionUnrollLimit; i++) { IntExpr idx = ctx.mkInt(i); diff --git a/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java b/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java index 510d88ec0..90d7238c2 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java +++ b/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java @@ -303,8 +303,10 @@ CelVerificationResult verifyImplication( /* isCounterexample= */ true)); case TRUNCATED: return CelVerificationResult.inconclusive( - String.format("Inconclusive: %s holds within the current loop unroll limit, but" - + " may be violated for larger collections.", subjectName.toLowerCase(Locale.US))); + String.format( + "Inconclusive: %s holds within the current loop unroll limit, but" + + " may be violated for larger collections.", + subjectName.toLowerCase(Locale.US))); case NO_MATCH: return CelVerificationResult.verified(); case SOLVER_UNKNOWN: diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java index f52886a42..104e224a6 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java @@ -179,14 +179,26 @@ private static String reconstructList( private static String reconstructMap( Context ctx, CelZ3TypeSystem typeSystem, Model model, Expr mapRef) { - Expr presenceArray = + List> keys = new ArrayList<>(); + Expr lenExpr = evaluateStrict( model, - typeSystem.getMapPresence(mapRef), - String.format("Z3 failed to evaluate presence array natively for map %s", mapRef)); - - List> keys = new ArrayList<>(); - extractKeys(presenceArray, keys); + ctx.mkLength(typeSystem.getMapKeys(mapRef)), + String.format("Z3 failed to evaluate length for map %s", mapRef)); + if (lenExpr instanceof IntNum) { + int length = ((IntNum) lenExpr).getInt(); + int printLimit = Math.min(length, 100); + for (int i = 0; i < printLimit; i++) { + Expr elem = + evaluateStrict( + model, + ctx.mkNth(typeSystem.getMapKeys(mapRef), ctx.mkInt(i)), + String.format("Z3 failed to evaluate map key at index %d for map %s", i, mapRef)); + if (!keys.contains(elem)) { + keys.add(elem); + } + } + } List entries = new ArrayList<>(); for (Expr key : keys) { @@ -215,11 +227,11 @@ private static String reconstructMap( private static String reconstructMessage( Context ctx, CelZ3TypeSystem typeSystem, Model model, Expr msgRef) { - Expr valuesArray = + Expr presenceArray = evaluateStrict( model, - typeSystem.getMsgValues(msgRef), - String.format("Z3 failed to evaluate values array natively for msg %s", msgRef)); + typeSystem.getMsgPresence(msgRef), + String.format("Z3 failed to evaluate presence array natively for msg %s", msgRef)); Expr typeNameExpr = evaluateStrict( @@ -230,7 +242,7 @@ private static String reconstructMessage( String typeName = formatExpr(ctx, typeSystem, model, typeNameExpr).replace("\"", ""); List> keys = new ArrayList<>(); - extractKeys(valuesArray, keys); + extractKeys(presenceArray, keys); List entries = new ArrayList<>(); for (Expr key : keys) { @@ -268,16 +280,17 @@ private static void extractKeys(Expr arrayExpr, List> keys) { FuncDecl decl = arrayExpr.getFuncDecl(); String declName = decl.getName().toString(); - if (!declName.equals("store")) { - break; + if (declName.equals("store")) { + Expr[] args = arrayExpr.getArgs(); + Preconditions.checkState( + args.length == 3, "Z3 store array operation must have exactly 3 arguments"); + if (!keys.contains(args[1])) { + keys.add(args[1]); + } + arrayExpr = args[0]; + continue; } - - Expr[] args = arrayExpr.getArgs(); - Preconditions.checkState( - args.length == 3, "Z3 store array operation must have exactly 3 arguments"); - keys.add(args[1]); - - arrayExpr = args[0]; + break; } } diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/GreaterAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/GreaterAxiom.java index 292b86135..2ccb1543a 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/GreaterAxiom.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/GreaterAxiom.java @@ -15,7 +15,6 @@ package dev.cel.verifier.axioms; import com.microsoft.z3.ArithExpr; -import com.microsoft.z3.FPExpr; import com.microsoft.z3.SeqExpr; import dev.cel.checker.CelStandardDeclarations.StandardFunction; import dev.cel.checker.CelStandardDeclarations.StandardFunction.Overload.Comparison; @@ -56,9 +55,7 @@ final class GreaterAxiom { (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkFPGt( - (FPExpr) typeSystem.getDouble(lhs), - (FPExpr) typeSystem.getDouble(rhs))))) + ctx.mkFPGt(typeSystem.getDouble(lhs), typeSystem.getDouble(rhs))))) .addBinaryOverloadTranslator( Comparison.GREATER_STRING.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> @@ -82,7 +79,7 @@ final class GreaterAxiom { typeSystem.wrapBool( AxiomHelpers.mkFpLtReal( ctx, - (FPExpr) typeSystem.getDouble(rhs), + typeSystem.getDouble(rhs), ctx.mkInt2Real(typeSystem.getInt(lhs)))))) .addBinaryOverloadTranslator( Comparison.GREATER_UINT64_DOUBLE.celOverloadDecl(), @@ -91,7 +88,7 @@ final class GreaterAxiom { typeSystem.wrapBool( AxiomHelpers.mkFpLtReal( ctx, - (FPExpr) typeSystem.getDouble(rhs), + typeSystem.getDouble(rhs), ctx.mkInt2Real(typeSystem.getUint(lhs)))))) .addBinaryOverloadTranslator( Comparison.GREATER_DOUBLE_INT64.celOverloadDecl(), @@ -101,7 +98,7 @@ final class GreaterAxiom { AxiomHelpers.mkRealLtFp( ctx, ctx.mkInt2Real(typeSystem.getInt(rhs)), - (FPExpr) typeSystem.getDouble(lhs))))) + typeSystem.getDouble(lhs))))) .addBinaryOverloadTranslator( Comparison.GREATER_DOUBLE_UINT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> @@ -110,7 +107,7 @@ final class GreaterAxiom { AxiomHelpers.mkRealLtFp( ctx, ctx.mkInt2Real(typeSystem.getUint(rhs)), - (FPExpr) typeSystem.getDouble(lhs))))) + typeSystem.getDouble(lhs))))) .addBinaryOverloadTranslator( Comparison.GREATER_INT64_UINT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/GreaterEqualsAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/GreaterEqualsAxiom.java index 4be0c23e2..d71f0f248 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/GreaterEqualsAxiom.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/GreaterEqualsAxiom.java @@ -15,7 +15,6 @@ package dev.cel.verifier.axioms; import com.microsoft.z3.ArithExpr; -import com.microsoft.z3.FPExpr; import com.microsoft.z3.SeqExpr; import dev.cel.checker.CelStandardDeclarations.StandardFunction; import dev.cel.checker.CelStandardDeclarations.StandardFunction.Overload.Comparison; @@ -56,9 +55,7 @@ final class GreaterEqualsAxiom { (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkFPGEq( - (FPExpr) typeSystem.getDouble(lhs), - (FPExpr) typeSystem.getDouble(rhs))))) + ctx.mkFPGEq(typeSystem.getDouble(lhs), typeSystem.getDouble(rhs))))) .addBinaryOverloadTranslator( Comparison.GREATER_EQUALS_STRING.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> @@ -82,7 +79,7 @@ final class GreaterEqualsAxiom { typeSystem.wrapBool( AxiomHelpers.mkFpLeReal( ctx, - (FPExpr) typeSystem.getDouble(rhs), + typeSystem.getDouble(rhs), ctx.mkInt2Real(typeSystem.getInt(lhs)))))) .addBinaryOverloadTranslator( Comparison.GREATER_EQUALS_UINT64_DOUBLE.celOverloadDecl(), @@ -91,7 +88,7 @@ final class GreaterEqualsAxiom { typeSystem.wrapBool( AxiomHelpers.mkFpLeReal( ctx, - (FPExpr) typeSystem.getDouble(rhs), + typeSystem.getDouble(rhs), ctx.mkInt2Real(typeSystem.getUint(lhs)))))) .addBinaryOverloadTranslator( Comparison.GREATER_EQUALS_DOUBLE_INT64.celOverloadDecl(), @@ -101,7 +98,7 @@ final class GreaterEqualsAxiom { AxiomHelpers.mkRealLeFp( ctx, ctx.mkInt2Real(typeSystem.getInt(rhs)), - (FPExpr) typeSystem.getDouble(lhs))))) + typeSystem.getDouble(lhs))))) .addBinaryOverloadTranslator( Comparison.GREATER_EQUALS_DOUBLE_UINT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> @@ -110,7 +107,7 @@ final class GreaterEqualsAxiom { AxiomHelpers.mkRealLeFp( ctx, ctx.mkInt2Real(typeSystem.getUint(rhs)), - (FPExpr) typeSystem.getDouble(lhs))))) + typeSystem.getDouble(lhs))))) .addBinaryOverloadTranslator( Comparison.GREATER_EQUALS_INT64_UINT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/LessAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/LessAxiom.java index e09484f28..31b1d3a21 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/LessAxiom.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/LessAxiom.java @@ -15,7 +15,6 @@ package dev.cel.verifier.axioms; import com.microsoft.z3.ArithExpr; -import com.microsoft.z3.FPExpr; import com.microsoft.z3.SeqExpr; import dev.cel.checker.CelStandardDeclarations.StandardFunction; import dev.cel.checker.CelStandardDeclarations.StandardFunction.Overload.Comparison; @@ -56,9 +55,7 @@ final class LessAxiom { (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkFPLt( - (FPExpr) typeSystem.getDouble(lhs), - (FPExpr) typeSystem.getDouble(rhs))))) + ctx.mkFPLt(typeSystem.getDouble(lhs), typeSystem.getDouble(rhs))))) .addBinaryOverloadTranslator( Comparison.LESS_STRING.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> @@ -83,7 +80,7 @@ final class LessAxiom { AxiomHelpers.mkRealLtFp( ctx, ctx.mkInt2Real(typeSystem.getInt(lhs)), - (FPExpr) typeSystem.getDouble(rhs))))) + typeSystem.getDouble(rhs))))) .addBinaryOverloadTranslator( Comparison.LESS_UINT64_DOUBLE.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> @@ -92,7 +89,7 @@ final class LessAxiom { AxiomHelpers.mkRealLtFp( ctx, ctx.mkInt2Real(typeSystem.getUint(lhs)), - (FPExpr) typeSystem.getDouble(rhs))))) + typeSystem.getDouble(rhs))))) .addBinaryOverloadTranslator( Comparison.LESS_DOUBLE_INT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> @@ -100,7 +97,7 @@ final class LessAxiom { typeSystem.wrapBool( AxiomHelpers.mkFpLtReal( ctx, - (FPExpr) typeSystem.getDouble(lhs), + typeSystem.getDouble(lhs), ctx.mkInt2Real(typeSystem.getInt(rhs)))))) .addBinaryOverloadTranslator( Comparison.LESS_DOUBLE_UINT64.celOverloadDecl(), @@ -109,7 +106,7 @@ final class LessAxiom { typeSystem.wrapBool( AxiomHelpers.mkFpLtReal( ctx, - (FPExpr) typeSystem.getDouble(lhs), + typeSystem.getDouble(lhs), ctx.mkInt2Real(typeSystem.getUint(rhs)))))) .addBinaryOverloadTranslator( Comparison.LESS_INT64_UINT64.celOverloadDecl(), diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/LessEqualsAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/LessEqualsAxiom.java index e27b47631..c2466cf1b 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/LessEqualsAxiom.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/LessEqualsAxiom.java @@ -15,7 +15,6 @@ package dev.cel.verifier.axioms; import com.microsoft.z3.ArithExpr; -import com.microsoft.z3.FPExpr; import com.microsoft.z3.SeqExpr; import dev.cel.checker.CelStandardDeclarations.StandardFunction; import dev.cel.checker.CelStandardDeclarations.StandardFunction.Overload.Comparison; @@ -56,9 +55,7 @@ final class LessEqualsAxiom { (ctx, typeSystem, constraintSink, lhs, rhs) -> Optional.of( typeSystem.wrapBool( - ctx.mkFPLEq( - (FPExpr) typeSystem.getDouble(lhs), - (FPExpr) typeSystem.getDouble(rhs))))) + ctx.mkFPLEq(typeSystem.getDouble(lhs), typeSystem.getDouble(rhs))))) .addBinaryOverloadTranslator( Comparison.LESS_EQUALS_STRING.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> @@ -83,7 +80,7 @@ final class LessEqualsAxiom { AxiomHelpers.mkRealLeFp( ctx, ctx.mkInt2Real(typeSystem.getInt(lhs)), - (FPExpr) typeSystem.getDouble(rhs))))) + typeSystem.getDouble(rhs))))) .addBinaryOverloadTranslator( Comparison.LESS_EQUALS_UINT64_DOUBLE.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> @@ -92,7 +89,7 @@ final class LessEqualsAxiom { AxiomHelpers.mkRealLeFp( ctx, ctx.mkInt2Real(typeSystem.getUint(lhs)), - (FPExpr) typeSystem.getDouble(rhs))))) + typeSystem.getDouble(rhs))))) .addBinaryOverloadTranslator( Comparison.LESS_EQUALS_DOUBLE_INT64.celOverloadDecl(), (ctx, typeSystem, constraintSink, lhs, rhs) -> @@ -100,7 +97,7 @@ final class LessEqualsAxiom { typeSystem.wrapBool( AxiomHelpers.mkFpLeReal( ctx, - (FPExpr) typeSystem.getDouble(lhs), + typeSystem.getDouble(lhs), ctx.mkInt2Real(typeSystem.getInt(rhs)))))) .addBinaryOverloadTranslator( Comparison.LESS_EQUALS_DOUBLE_UINT64.celOverloadDecl(), @@ -109,7 +106,7 @@ final class LessEqualsAxiom { typeSystem.wrapBool( AxiomHelpers.mkFpLeReal( ctx, - (FPExpr) typeSystem.getDouble(lhs), + typeSystem.getDouble(lhs), ctx.mkInt2Real(typeSystem.getUint(rhs)))))) .addBinaryOverloadTranslator( Comparison.LESS_EQUALS_INT64_UINT64.celOverloadDecl(), diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java b/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java index 8cd844214..f4dba5afc 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java @@ -19,7 +19,6 @@ import com.google.common.collect.ImmutableList; import com.microsoft.z3.BoolExpr; import com.microsoft.z3.Expr; -import com.microsoft.z3.FPExpr; import com.microsoft.z3.FuncDecl; import com.microsoft.z3.IntExpr; import com.microsoft.z3.Sort; @@ -233,8 +232,7 @@ private static CelZ3OverloadTranslator createUninterpretedConversion(Conversions sink.accept(ctx.mkOr(typeSystem.isDouble(res), typeSystem.isError(res))); sink.accept( ctx.mkImplies( - typeSystem.isDouble(res), - ctx.mkNot(ctx.mkFPIsNaN((FPExpr) typeSystem.getDouble(res))))); + typeSystem.isDouble(res), ctx.mkNot(ctx.mkFPIsNaN(typeSystem.getDouble(res))))); break; case STRING: sink.accept(ctx.mkOr(typeSystem.isString(res), typeSystem.isError(res))); diff --git a/verifier/src/test/java/dev/cel/verifier/BUILD.bazel b/verifier/src/test/java/dev/cel/verifier/BUILD.bazel index de788ca5c..6bf44cafd 100644 --- a/verifier/src/test/java/dev/cel/verifier/BUILD.bazel +++ b/verifier/src/test/java/dev/cel/verifier/BUILD.bazel @@ -61,7 +61,7 @@ java_library( junit4_test_suites( name = "test_suites", - shard_count = 4, + shard_count = 8, sizes = [ "small", "medium", diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index f230714b2..696a21387 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -157,12 +157,22 @@ private enum IsSatisfiableTestCase { CROSS_NUMERIC_EQUALITY_INT_DYN_EXACT("1 == request"), MACRO_LIMIT("dyn_list.all(x, x == 1)"), STRUCT_FIELD_MISSING_APPROXIMATE_SATISFIABLE("dyn_var.unknown_field"), - NULLABLE_INT_SATISFIABLE("nullable_int == 123"); + NULLABLE_INT_SATISFIABLE("nullable_int == 123"), + MAP_INDEX_SATISFIABLE("string_int_map['alice'] > 0", "\"alice\": [1-9]\\d*"), + MAP_SIZE_GREATER_THAN_ONE_WITH_KEY( + "string_int_map.size() > 1 && string_int_map['foo'] == 42", + "string_int_map = \\{[^}]*,[^}]*\\}"), + MAP_SIZE_GREATER_THAN_ONE_WITH_LIST_ELEMENT( + "string_int_map.size() > 1 && string_int_map['a'] == int_list[0] && int_list.size() == 1", + "string_int_map = \\{[^}]*,[^}]*\\}"), + ; final String expr; + final ImmutableList expectedFragments; - IsSatisfiableTestCase(String expr) { + IsSatisfiableTestCase(String expr, String... expectedFragments) { this.expr = expr; + this.expectedFragments = ImmutableList.copyOf(expectedFragments); } } @@ -183,6 +193,9 @@ public void isSatisfiable_success(@TestParameter IsSatisfiableTestCase testCase) CelVerificationResult result = VERIFIER.isSatisfiable(ast); assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + for (String fragment : testCase.expectedFragments) { + assertThat(result.message()).containsMatch(fragment); + } } @Test @@ -251,6 +264,7 @@ private enum CounterexampleNeverErrorTestCase { DYN_MAP_REFLEXIVITY("dyn_map.size() == 1 ? dyn_map[1] == dyn_map[1] : true"), DYN_LIST_ELEMENT("size(dyn_list) == 1 && dyn_list[0] == 'impossible_value'"), DYN_MAP_VALUE("size(dyn_map) == 1 && dyn_map['a'] == 'impossible_value'"), + STRUCT_FIELD_VALUE("test_all_types.single_int64 == 12345 && false"), ; final String expr; @@ -365,7 +379,28 @@ private enum IsUnsatisfiableTestCase { TIMESTAMP_INEQUALITY_CONTRADICTION( "timestamp('2023-01-01T00:00:00Z') != timestamp('2023-01-01T00:00:00Z')"), TYPE_TIMESTAMP_NOT_INT("type(timestamp('1970-01-01T00:00:00Z')) == int"), - DYN_INT_NOT_DURATION("dyn(1) == dyn(duration('1s'))"); + DYN_INT_NOT_DURATION("dyn(1) == dyn(duration('1s'))"), + DYNAMIC_MAP_DUPLICATE_KEYS_CONTRADICTION( + "size(string_int_map) == 2 && string_int_map.all(k, k == 'a')"), + EMPTY_MAP_WITH_KEY_IN("string_int_map.size() == 0 && 'foo' in string_int_map"), + KEY_IN_EMPTY_MAP("('x' in string_int_map) && string_int_map.size() == 0"), + EMPTY_MAP_AND_LIST_WITH_KEY_IN( + "int_list.size() == string_int_map.size() && int_list.size() == 0 && 'a' in" + + " string_int_map"), + MAP_SIZE_ONE_TWO_KEYS( + "string_int_map.size() == 1 && string_int_map['a'] == 1 && string_int_map['b'] == 2"), + MAP_SIZE_LESS_THAN_TWO_TWO_KEYS( + "string_int_map['foo'] == 10 && string_int_map['bar'] == 20 && string_int_map.size() < 2"), + MAP_SIZE_ONE_SUM_TWO_KEYS( + "string_int_map['a'] + string_int_map['b'] == 10 && string_int_map.size() == 1"), + MAP_SIZE_ONE_TWO_EQUAL_KEYS( + "string_int_map.size() == 1 && string_int_map['foo'] == 10 && string_int_map['bar'] == 10"), + MAP_SIZE_TWO_THREE_KEYS( + "string_int_map['k1'] == 1 && string_int_map['k2'] == 2 && string_int_map['k3'] == 3 &&" + + " string_int_map.size() == 2"), + EMPTY_MAP_KEY_LOOKUP("string_int_map['a'] > 100 && string_int_map.size() == 0"), + EMPTY_MAP_DYNAMIC_KEY_LOOKUP("string_int_map[string_var] == 100 && string_int_map.size() == 0"), + ; final String expr; @@ -1343,7 +1378,7 @@ private enum IsAlwaysTrueViolationTestCase { + "? dyn_map[1 + 1] == [] : true", "Condition is not always true\\.", "Counterexample input:", - "dyn_map = \\{\\}"), + "dyn_map = \\{.*\\}"), DYNAMIC_MAP_COMPREHENSION_NESTED_EQUALITY_VIOLATION( "cel.bind(r, request, r.l == [[1], [2], [3], [4], [5]] && r.m == {1: [1], 2: [2]," + " 3: [3]} ? r.l.all(x, r.m.exists(k, r.m[k] == x)) : true)", @@ -1601,6 +1636,9 @@ private enum EquivalenceTestCase { MACRO_EXISTS_ONE_EQUIVALENT( "[1, 2, 3].exists_one(x, x == 2)", "(1 == 2 ? 1 : 0) + (2 == 2 ? 1 : 0) + (3 == 2 ? 1 : 0) == 1"), + TIMESTAMP_CONVERSION_OVERFLOW_EQUIVALENCE( + "timestamp(string_var) <= timestamp(253402300799)", + "timestamp(string_var) == timestamp(string_var)"), TIMESTAMP_MATH_SUBTRACT_TS( "timestamp(900000) - timestamp(100)", "timestamp(899900) - timestamp(0)"), TIMESTAMP_MATH_COMMUTATIVITY( @@ -2937,3 +2975,4 @@ public void verifyImplication_symbolicNan_crossNumericComparisonReturnsFalse() t assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); } } + From 2da98c9e04f3ac19c49b80a78f2e8d1f65f249a0 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Wed, 5 Aug 2026 14:38:38 -0700 Subject: [PATCH 162/204] Tighten the counterexample domain for double/int and parameterized unknown PiperOrigin-RevId: 959876941 --- verifier/BUILD.bazel | 7 + .../main/java/dev/cel/verifier/BUILD.bazel | 15 ++ .../dev/cel/verifier/CelAstAlphaHasher.java | 16 +- .../cel/verifier/CelAstToZ3Translator.java | 19 +- .../dev/cel/verifier/CelNumericBounds.java | 105 ++++++++++ .../CelZ3CounterexampleGenerator.java | 53 ++--- .../verifier/CelZ3ExtensionalityAxioms.java | 8 +- .../cel/verifier/CelZ3OperatorTranslator.java | 99 ++++++--- .../dev/cel/verifier/CelZ3TypeSystem.java | 18 +- .../dev/cel/verifier/TranslatedValue.java | 17 +- .../java/dev/cel/verifier/axioms/BUILD.bazel | 1 + .../dev/cel/verifier/axioms/TypeAxiom.java | 2 +- .../verifier/axioms/TypeConversionAxioms.java | 2 +- .../cel/verifier/tools/CelVerifierRepl.java | 6 +- .../test/java/dev/cel/verifier/BUILD.bazel | 2 +- .../cel/verifier/CelVerifierZ3ImplTest.java | 188 +++++++++++++++--- .../verifier/tools/CelVerifierReplTest.java | 6 + 17 files changed, 441 insertions(+), 123 deletions(-) create mode 100644 verifier/src/main/java/dev/cel/verifier/CelNumericBounds.java diff --git a/verifier/BUILD.bazel b/verifier/BUILD.bazel index cc2f01810..9ec441ed4 100644 --- a/verifier/BUILD.bazel +++ b/verifier/BUILD.bazel @@ -41,6 +41,13 @@ java_library( exports = ["//verifier/src/main/java/dev/cel/verifier:verifier_factory"], ) +java_library( + name = "numeric_bounds", + compatible_with = [], + visibility = [":verifier_internal"], + exports = ["//verifier/src/main/java/dev/cel/verifier:numeric_bounds"], +) + java_library( name = "type_system", compatible_with = [], diff --git a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel index 4ca9794cc..ab341fba2 100644 --- a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel +++ b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel @@ -91,6 +91,19 @@ java_library( ], ) +java_library( + name = "numeric_bounds", + srcs = ["CelNumericBounds.java"], + compatible_with = [], + tags = [ + ], + deps = [ + "//:auto_value", + "//common/annotations", + "@maven//:com_google_guava_guava", + ], +) + java_library( name = "type_system", srcs = ["CelZ3TypeSystem.java"], @@ -98,6 +111,7 @@ java_library( tags = [ ], deps = [ + ":numeric_bounds", "//common/internal:proto_time_utils", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -121,6 +135,7 @@ java_library( tags = [ ], deps = [ + ":numeric_bounds", ":type_system", ":verifier", "//:auto_value", diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstAlphaHasher.java b/verifier/src/main/java/dev/cel/verifier/CelAstAlphaHasher.java index c0491085f..a7e2be8b7 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelAstAlphaHasher.java +++ b/verifier/src/main/java/dev/cel/verifier/CelAstAlphaHasher.java @@ -24,7 +24,9 @@ import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.jspecify.annotations.Nullable; /** @@ -83,16 +85,11 @@ private static void hashAst(CelExpr expr, @Nullable Scope scope, HasherContext c context.hasher.putByte((byte) 0); // 0 = bound context.hasher.putInt(bIdx); } else { - int fIdx = -1; - for (int i = 0; i < context.freeVars.size(); i++) { - if (context.freeVars.get(i).ident().name().equals(name)) { - fIdx = i; - break; - } - } - if (fIdx == -1) { + Integer fIdx = context.freeVarIndices.get(name); + if (fIdx == null) { context.freeVars.add(expr); fIdx = context.freeVars.size() - 1; + context.freeVarIndices.put(name, fIdx); } context.hasher.putByte((byte) 1); // 1 = free context.hasher.putInt(fIdx); @@ -100,12 +97,10 @@ private static void hashAst(CelExpr expr, @Nullable Scope scope, HasherContext c break; case SELECT: hashAst(expr.select().operand(), scope, context); - context.hasher.putInt(expr.select().field().length()); context.hasher.putString(expr.select().field(), UTF_8); context.hasher.putBoolean(expr.select().testOnly()); break; case CALL: - context.hasher.putInt(expr.call().function().length()); context.hasher.putString(expr.call().function(), UTF_8); context.hasher.putBoolean(expr.call().target().isPresent()); if (expr.call().target().isPresent()) { @@ -210,6 +205,7 @@ private static void hashConstant(CelConstant constant, HasherContext context) { private static final class HasherContext { final Hasher hasher; + final Map freeVarIndices = new HashMap<>(); final List freeVars = new ArrayList<>(); HasherContext(HashFunction hashFunction) { diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java index e253b27ad..3964d68a1 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java @@ -1228,7 +1228,7 @@ private BoolExpr createTypeConstraint(Expr val, long exprId, CelAbstractSynta .orElseThrow( () -> new IllegalArgumentException("Type not found for expr ID: " + exprId)); BoolExpr typeConstraint = createTypeConstraintForType(val, type); - return ctx.mkOr(typeSystem.isError(val), typeSystem.isUnknown(val), typeConstraint); + return ctx.mkOr(typeSystem.isErrorOrUnknown(val), typeConstraint); } private BoolExpr createTypeConstraintForType(Expr val, CelType type) { @@ -1257,15 +1257,15 @@ private BoolExpr createTypeConstraintForType(Expr val, CelType type) { Expr unwrapped = ctx.mkApp(typeSystem.intCons().getAccessorDecls()[0], val); return ctx.mkAnd( ctx.mkApp(typeSystem.intCons().getTesterDecl(), val), - ctx.mkGe((ArithExpr) unwrapped, ctx.mkInt(CelZ3TypeSystem.MIN_INT64)), - ctx.mkLe((ArithExpr) unwrapped, ctx.mkInt(CelZ3TypeSystem.MAX_INT64))); + ctx.mkGe((ArithExpr) unwrapped, ctx.mkInt(CelNumericBounds.MIN_INT64)), + ctx.mkLe((ArithExpr) unwrapped, ctx.mkInt(CelNumericBounds.MAX_INT64))); } if (type.equals(SimpleType.UINT)) { Expr unwrapped = ctx.mkApp(typeSystem.uintCons().getAccessorDecls()[0], val); return ctx.mkAnd( ctx.mkApp(typeSystem.uintCons().getTesterDecl(), val), ctx.mkGe((ArithExpr) unwrapped, ctx.mkInt(0)), - ctx.mkLe((ArithExpr) unwrapped, ctx.mkInt(CelZ3TypeSystem.MAX_UINT64))); + ctx.mkLe((ArithExpr) unwrapped, ctx.mkInt(CelNumericBounds.MAX_UINT64))); } if (type.equals(SimpleType.DOUBLE)) { return (BoolExpr) ctx.mkApp(typeSystem.doubleCons().getTesterDecl(), val); @@ -1351,7 +1351,10 @@ private BoolExpr createTypeConstraintForType(Expr val, CelType type) { BoolExpr validEntry = ctx.mkAnd(validIndex, presence); Expr mapVal = ctx.mkSelect(mapValues, key); - BoolExpr valNotError = ctx.mkNot(typeSystem.isError(mapVal)); + BoolExpr valNotError = + unknownIdentifiers.isEmpty() + ? ctx.mkNot(typeSystem.isErrorOrUnknown(mapVal)) + : ctx.mkNot(typeSystem.isError(mapVal)); boundsAndTypes.add(ctx.mkImplies(validEntry, valNotError)); boundsAndTypes.add(ctx.mkImplies(validEntry, createTypeConstraintForType(mapVal, valType))); } @@ -1409,6 +1412,12 @@ private Optional toCacheKey(CelExpr expr) { case CONSTANT: return Optional.of(expr.constant()); case LIST: + if (!expr.list().optionalIndices().isEmpty()) { + // Do not cache lists with optional elements. Optional elements conditionally alter + // sequence length and presence via ITE branches at runtime; caching would collide + // [1, 2] with [?1, 2] and freeze conditional evaluations to a static reference. + return Optional.empty(); + } ImmutableList.Builder builder = ImmutableList.builder(); for (CelExpr elem : expr.list().elements()) { Optional elemKey = toCacheKey(elem); diff --git a/verifier/src/main/java/dev/cel/verifier/CelNumericBounds.java b/verifier/src/main/java/dev/cel/verifier/CelNumericBounds.java new file mode 100644 index 000000000..8d2184c72 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelNumericBounds.java @@ -0,0 +1,105 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import com.google.auto.value.AutoValue; +import com.google.common.primitives.UnsignedLong; +import dev.cel.common.annotations.Internal; +import java.util.Optional; + +/** + * Utility for computing matching integer and unsigned integer ranges for IEEE-754 double-precision + * floating-point constants in Z3 verification. + */ +@Internal +public final class CelNumericBounds { + + /** Minimum representable signed 64-bit integer string. */ + public static final String MIN_INT64 = "-9223372036854775808"; + + /** Maximum representable signed 64-bit integer string. */ + public static final String MAX_INT64 = "9223372036854775807"; + + /** Maximum representable unsigned 64-bit integer string. */ + public static final String MAX_UINT64 = "18446744073709551615"; + + private static final double TWO_TO_63 = Math.scalb(1.0, 63); + private static final double TWO_TO_64 = Math.scalb(1.0, 64); + + @AutoValue + abstract static class IntRange { + abstract long min(); + + abstract long max(); + + static IntRange of(long min, long max) { + return new AutoValue_CelNumericBounds_IntRange(min, max); + } + } + + @AutoValue + abstract static class UintRange { + abstract String min(); + + abstract String max(); + + static UintRange of(String min, String max) { + return new AutoValue_CelNumericBounds_UintRange(min, max); + } + } + + private static boolean isMathematicalInteger(double vDouble) { + return Double.isFinite(vDouble) && vDouble == Math.rint(vDouble); + } + + static Optional getMatchingIntRange(double vDouble) { + if (!isMathematicalInteger(vDouble) || vDouble < -TWO_TO_63 || vDouble > TWO_TO_63) { + return Optional.empty(); + } + long minL = (long) vDouble; + while (minL > Long.MIN_VALUE && (double) (minL - 1) == vDouble) { + minL--; + } + long maxL = (long) vDouble; + while (maxL < Long.MAX_VALUE && (double) (maxL + 1) == vDouble) { + maxL++; + } + return Optional.of(IntRange.of(minL, maxL)); + } + + static Optional getMatchingUintRange(double vDouble) { + if (!isMathematicalInteger(vDouble) || vDouble < 0 || vDouble > TWO_TO_64) { + return Optional.empty(); + } + // XOR with Long.MIN_VALUE (0x8000000000000000L) flips bit 63 to 1, encoding unsigned values + // >= 2^63 into Java's two's-complement signed long representation. + long uBits = + vDouble < TWO_TO_63 ? (long) vDouble : (long) (vDouble - TWO_TO_63) ^ Long.MIN_VALUE; + UnsignedLong uVal = UnsignedLong.fromLongBits(uBits); + UnsignedLong minU = uVal; + while (!minU.equals(UnsignedLong.ZERO) + && minU.minus(UnsignedLong.ONE).doubleValue() == vDouble) { + minU = minU.minus(UnsignedLong.ONE); + } + UnsignedLong maxU = uVal; + while (!maxU.equals(UnsignedLong.MAX_VALUE) + && maxU.plus(UnsignedLong.ONE).doubleValue() == vDouble) { + maxU = maxU.plus(UnsignedLong.ONE); + } + return Optional.of(UintRange.of(minU.toString(), maxU.toString())); + } + + private CelNumericBounds() {} +} diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java index 104e224a6..6e5c519fe 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java @@ -24,14 +24,17 @@ import com.microsoft.z3.Model; import com.microsoft.z3.RatNum; import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import org.jspecify.annotations.Nullable; /** Generates human-readable counterexample strings from Z3 models. */ @SuppressWarnings({"unchecked", "rawtypes"}) // Z3 Java API uses raw types. final class CelZ3CounterexampleGenerator { - private static final int MAX_LIST_ELEMENTS_TO_PRINT = 15; + private static final int MAX_ELEMENTS_TO_PRINT = 15; private CelZ3CounterexampleGenerator() {} @@ -158,8 +161,10 @@ private static String reconstructList( model, ctx.mkLength(typeSystem.getSeq(listRef)), String.format("Z3 failed to evaluate length for list %s", listRef)); - int length = ((IntNum) lenExpr).getInt(); - int printLimit = Math.min(length, MAX_LIST_ELEMENTS_TO_PRINT); + Preconditions.checkState( + lenExpr instanceof IntNum, "Expected IntNum length for list %s, got %s", listRef, lenExpr); + long length = ((IntNum) lenExpr).getInt64(); + int printLimit = (int) Math.min(length, (long) MAX_ELEMENTS_TO_PRINT); List elements = new ArrayList<>(); for (int i = 0; i < printLimit; i++) { Expr elem = @@ -179,36 +184,33 @@ private static String reconstructList( private static String reconstructMap( Context ctx, CelZ3TypeSystem typeSystem, Model model, Expr mapRef) { - List> keys = new ArrayList<>(); Expr lenExpr = evaluateStrict( model, ctx.mkLength(typeSystem.getMapKeys(mapRef)), String.format("Z3 failed to evaluate length for map %s", mapRef)); - if (lenExpr instanceof IntNum) { - int length = ((IntNum) lenExpr).getInt(); - int printLimit = Math.min(length, 100); - for (int i = 0; i < printLimit; i++) { - Expr elem = - evaluateStrict( - model, - ctx.mkNth(typeSystem.getMapKeys(mapRef), ctx.mkInt(i)), - String.format("Z3 failed to evaluate map key at index %d for map %s", i, mapRef)); - if (!keys.contains(elem)) { - keys.add(elem); - } - } - } + Preconditions.checkState( + lenExpr instanceof IntNum, "Expected IntNum length for map %s, got %s", mapRef, lenExpr); + long length = ((IntNum) lenExpr).getInt64(); + int printLimit = (int) Math.min(length, (long) MAX_ELEMENTS_TO_PRINT); List entries = new ArrayList<>(); - for (Expr key : keys) { + Set> seenKeys = new HashSet<>(); + for (int i = 0; i < printLimit; i++) { + Expr key = + evaluateStrict( + model, + ctx.mkNth(typeSystem.getMapKeys(mapRef), ctx.mkInt(i)), + String.format("Z3 failed to evaluate map key at index %d for map %s", i, mapRef)); + if (!seenKeys.add(key)) { + continue; + } Expr presence = evaluateStrict( model, ctx.mkSelect((ArrayExpr) typeSystem.getMapPresence(mapRef), key), String.format( "Z3 failed to evaluate map presence for key %s in map %s", key, mapRef)); - if (presence.isTrue()) { Expr value = evaluateStrict( @@ -221,6 +223,9 @@ private static String reconstructMap( + formatExpr(ctx, typeSystem, model, value)); } } + if (length > printLimit) { + entries.add("... (" + (length - printLimit) + " more entries)"); + } return "{" + String.join(", ", entries) + "}"; } @@ -241,7 +246,7 @@ private static String reconstructMessage( String typeName = formatExpr(ctx, typeSystem, model, typeNameExpr).replace("\"", ""); - List> keys = new ArrayList<>(); + Set> keys = new LinkedHashSet<>(); extractKeys(presenceArray, keys); List entries = new ArrayList<>(); @@ -268,7 +273,7 @@ private static String reconstructMessage( return typeName + "{" + String.join(", ", entries) + "}"; } - private static void extractKeys(Expr arrayExpr, List> keys) { + private static void extractKeys(Expr arrayExpr, Set> keys) { int iterations = 0; while (true) { if (++iterations > 100_000) { @@ -284,9 +289,7 @@ private static void extractKeys(Expr arrayExpr, List> keys) { Expr[] args = arrayExpr.getArgs(); Preconditions.checkState( args.length == 3, "Z3 store array operation must have exactly 3 arguments"); - if (!keys.contains(args[1])) { - keys.add(args[1]); - } + keys.add(args[1]); arrayExpr = args[0]; continue; } diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3ExtensionalityAxioms.java b/verifier/src/main/java/dev/cel/verifier/CelZ3ExtensionalityAxioms.java index 2303abcaf..be1ec1475 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3ExtensionalityAxioms.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3ExtensionalityAxioms.java @@ -79,7 +79,8 @@ private static void addListAxioms( Sort listRefSort = typeSystem.listRefSort(); Sort seqSort = ctx.mkSeqSort(typeSystem.celValueSort()); - FuncDecl mkListRef = ctx.mkFuncDecl(FUNC_MK_LIST_REF, new Sort[] {seqSort}, listRefSort); + FuncDecl mkListRef = + typeSystem.internFuncDecl(FUNC_MK_LIST_REF, new Sort[] {seqSort}, listRefSort); for (Expr ref : refs) { if (isAppOf(ref, FUNC_MK_LIST_REF)) { @@ -109,7 +110,8 @@ private static void addMapAxioms( Sort presenceSort = ctx.mkArraySort(typeSystem.celValueSort(), ctx.getBoolSort()); FuncDecl mkMapRef = - ctx.mkFuncDecl(FUNC_MK_MAP_REF, new Sort[] {valuesSort, presenceSort}, mapRefSort); + typeSystem.internFuncDecl( + FUNC_MK_MAP_REF, new Sort[] {valuesSort, presenceSort}, mapRefSort); for (Expr ref : refs) { if (isAppOf(ref, FUNC_MK_MAP_REF)) { @@ -141,7 +143,7 @@ private static void addMessageAxioms( Sort presenceSort = ctx.mkArraySort(ctx.getStringSort(), ctx.getBoolSort()); FuncDecl mkMsgRef = - ctx.mkFuncDecl( + typeSystem.internFuncDecl( FUNC_MK_MSG_REF, new Sort[] {typeNameSort, valuesSort, presenceSort}, msgRefSort); for (Expr ref : refs) { diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java index 3051fbd87..bd5c8874e 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java @@ -43,7 +43,6 @@ import dev.cel.common.types.SimpleType; import dev.cel.verifier.axioms.CelZ3OverloadResult; import dev.cel.verifier.axioms.CelZ3OverloadTranslator; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -224,6 +223,8 @@ private TranslatedValue translateOperatorCall( return translateLogicalAndOr(args, false); case LOGICAL_NOT: return translateLogicalNot(args, ast); + case NEGATE: + return translateNegate(args.get(0), ast); case EQUALS: return translateEquality(args.get(0), args.get(1), ast, /* isEquals= */ true); case NOT_EQUALS: @@ -237,7 +238,6 @@ private TranslatedValue translateOperatorCall( case MULTIPLY: case DIVIDE: case MODULO: - case NEGATE: case IN: // Indicates a type-mismatch in an operator that's not handled // by our axioms @@ -330,72 +330,84 @@ private TranslatedValue translateLogicalNot( return TranslatedValue.propagateStrict(ctx, typeSystem, baseResult, args); } + private TranslatedValue translateNegate(TranslatedValue arg, CelAbstractSyntaxTree ast) { + CelType type = extractAstTypeOrDefault(arg, ast); + Expr z3Expr = arg.z3Expr(); + + Expr result; + if (type.equals(SimpleType.INT)) { + ArithExpr intNeg = ctx.mkUnaryMinus(typeSystem.getInt(z3Expr)); + result = + typeSystem.withRuntimeError( + typeSystem.wrapInt((IntExpr) intNeg), typeSystem.checkIntOverflow(intNeg)); + } else if (type.equals(SimpleType.DOUBLE)) { + result = typeSystem.wrapDouble(ctx.mkFPNeg(typeSystem.getDouble(z3Expr))); + } else { + ArithExpr intNeg = ctx.mkUnaryMinus(typeSystem.getInt(z3Expr)); + Expr intResult = + typeSystem.withRuntimeError( + typeSystem.wrapInt((IntExpr) intNeg), typeSystem.checkIntOverflow(intNeg)); + Expr doubleResult = typeSystem.wrapDouble(ctx.mkFPNeg(typeSystem.getDouble(z3Expr))); + result = + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(typeSystem.isInt(z3Expr), intResult) + .addCase(typeSystem.isDouble(z3Expr), doubleResult) + .build(typeSystem.mkError()); + } + return TranslatedValue.propagateStrict(ctx, typeSystem, result, arg); + } + private BoolExpr isNumeric(Expr arg) { return ctx.mkOr(typeSystem.isInt(arg), typeSystem.isUint(arg), typeSystem.isDouble(arg)); } private BoolExpr getNumericEqualityWithConstant( Expr symVal, CelConstant constant, CelType symType) { - Long intVal = null; - String uintVal = null; + Optional intRange = Optional.empty(); + Optional uintRange = Optional.empty(); double doubleVal; switch (constant.getKind()) { case INT64_VALUE: long vInt = constant.int64Value(); - intVal = vInt; - // Z3's infinite precision automatically evaluates `uint == -1` to false, - // but pruning it here keeps the formula smaller. + intRange = Optional.of(CelNumericBounds.IntRange.of(vInt, vInt)); if (vInt >= 0) { - uintVal = Long.toString(vInt); + uintRange = + Optional.of(CelNumericBounds.UintRange.of(Long.toString(vInt), Long.toString(vInt))); } doubleVal = (double) vInt; break; case UINT64_VALUE: long vUint = constant.uint64Value().longValue(); if (vUint >= 0) { - intVal = vUint; + intRange = Optional.of(CelNumericBounds.IntRange.of(vUint, vUint)); } - uintVal = constant.uint64Value().toString(); + String uStr = constant.uint64Value().toString(); + uintRange = Optional.of(CelNumericBounds.UintRange.of(uStr, uStr)); doubleVal = constant.uint64Value().doubleValue(); break; case DOUBLE_VALUE: double vDouble = constant.doubleValue(); doubleVal = vDouble; - if (vDouble == Math.floor(vDouble) && !Double.isInfinite(vDouble)) { - if (vDouble >= Long.MIN_VALUE && vDouble <= Long.MAX_VALUE) { - intVal = (long) vDouble; - } - if (vDouble >= 0 && vDouble <= Double.parseDouble(CelZ3TypeSystem.MAX_UINT64)) { - uintVal = BigDecimal.valueOf(vDouble).toBigInteger().toString(); - } - } + intRange = CelNumericBounds.getMatchingIntRange(vDouble); + uintRange = CelNumericBounds.getMatchingUintRange(vDouble); break; default: throw new IllegalArgumentException( "Unexpected numeric constant kind: " + constant.getKind()); } - if (isStaticallyKnown(symType)) { if (symType.kind() == CelKind.INT) { - return (intVal != null) - ? ctx.mkEq(typeSystem.getInt(symVal), ctx.mkInt(intVal)) - : ctx.mkFalse(); + return buildIntRangeExpr(intRange, typeSystem.getInt(symVal)); } else if (symType.kind() == CelKind.UINT) { - return (uintVal != null) - ? ctx.mkEq(typeSystem.getUint(symVal), ctx.mkInt(uintVal)) - : ctx.mkFalse(); + return buildUintRangeExpr(uintRange, typeSystem.getUint(symVal)); } else if (symType.kind() == CelKind.DOUBLE) { return ctx.mkFPEq(typeSystem.getDouble(symVal), typeSystem.mkFpDouble(doubleVal)); } } - BoolExpr intEq = - (intVal != null) ? ctx.mkEq(typeSystem.getInt(symVal), ctx.mkInt(intVal)) : ctx.mkFalse(); - BoolExpr uintEq = - (uintVal != null) - ? ctx.mkEq(typeSystem.getUint(symVal), ctx.mkInt(uintVal)) - : ctx.mkFalse(); + BoolExpr intEq = buildIntRangeExpr(intRange, typeSystem.getInt(symVal)); + BoolExpr uintEq = buildUintRangeExpr(uintRange, typeSystem.getUint(symVal)); BoolExpr doubleEq = ctx.mkFPEq(typeSystem.getDouble(symVal), typeSystem.mkFpDouble(doubleVal)); return (BoolExpr) @@ -406,6 +418,27 @@ private BoolExpr getNumericEqualityWithConstant( .build(ctx.mkFalse()); } + private BoolExpr buildIntRangeExpr(Optional rangeOpt, IntExpr symInt) { + return rangeOpt + .map(range -> buildIntRangeExpr(range.min(), range.max(), symInt)) + .orElseGet(ctx::mkFalse); + } + + private BoolExpr buildIntRangeExpr(long min, long max, IntExpr symInt) { + return ctx.mkAnd(ctx.mkGe(symInt, ctx.mkInt(min)), ctx.mkLe(symInt, ctx.mkInt(max))); + } + + private BoolExpr buildUintRangeExpr( + Optional rangeOpt, IntExpr symUint) { + return rangeOpt + .map(range -> buildUintRangeExpr(range.min(), range.max(), symUint)) + .orElseGet(ctx::mkFalse); + } + + private BoolExpr buildUintRangeExpr(String min, String max, IntExpr symUint) { + return ctx.mkAnd(ctx.mkGe(symUint, ctx.mkInt(min)), ctx.mkLe(symUint, ctx.mkInt(max))); + } + private BoolExpr getNumericEquality( TranslatedValue arg0, TranslatedValue arg1, CelAbstractSyntaxTree ast) { if (arg0.isNumericConstant()) { @@ -418,7 +451,7 @@ private BoolExpr getNumericEquality( CelType type0 = extractAstTypeOrDefault(arg0, ast); CelType type1 = extractAstTypeOrDefault(arg1, ast); - if (isStaticallyKnown(type0) && isStaticallyKnown(type1)) { + if (isStaticallyKnown(type0) && isStaticallyKnown(type1) && type0.kind() == type1.kind()) { return getStaticallyKnownNumericEquality(arg0.z3Expr(), type0, arg1.z3Expr()); } @@ -733,7 +766,7 @@ private Expr buildMapIndex( // Uint probes IntExpr rawUint = (IntExpr) ctx.mkITE(isUint, typeSystem.getUint(rhsTrans), ctx.mkInt(0)); - BoolExpr uintHasInt = ctx.mkLe(rawUint, ctx.mkInt(CelZ3TypeSystem.MAX_INT64)); + BoolExpr uintHasInt = ctx.mkLe(rawUint, ctx.mkInt(CelNumericBounds.MAX_INT64)); Expr uintIntKey = typeSystem.wrapInt(rawUint); BoolExpr uintHasDouble = hasExactDouble ? isUint : ctx.mkFalse(); diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java index 1c1435e3b..dc19a8d3a 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java @@ -55,10 +55,6 @@ @SuppressWarnings({"unchecked", "rawtypes", "AvoidObjectArrays"}) // Z3 Java API uses raw types. public final class CelZ3TypeSystem { - public static final String MIN_INT64 = "-9223372036854775808"; - public static final String MAX_INT64 = "9223372036854775807"; - public static final String MAX_UINT64 = "18446744073709551615"; - private static final String TYPE_CEL_VALUE = "CelValue"; private static final String CONS_BOOL = "Bool"; private static final String IS_BOOL = "isBool"; @@ -357,7 +353,7 @@ public Expr wrapDuration(IntExpr expr) { /** Creates a CelValue containing an integer. */ public Expr mkInt(long val) { - return ctx.mkApp(intCons.ConstructorDecl(), ctx.mkInt(val)); + return ctx.mkApp(intCons.ConstructorDecl(), ctx.mkInt(Long.toString(val))); } /** Creates a CelValue containing an unsigned integer from a string representation. */ @@ -575,6 +571,11 @@ public BoolExpr isUnknown(Expr val) { return (BoolExpr) ctx.mkApp(unknownCons.getTesterDecl(), val); } + /** Checks if the given CelValue is either an error or an unknown value. */ + public BoolExpr isErrorOrUnknown(Expr val) { + return ctx.mkOr(isError(val), isUnknown(val)); + } + /** Checks if the given CelValue is a boolean. */ public BoolExpr isBool(Expr val) { return (BoolExpr) ctx.mkApp(boolCons.getTesterDecl(), val); @@ -804,7 +805,9 @@ public Expr getMsgTypeName(Expr msgRef) { /** Checks if the given arithmetic expression overflows a 64-bit integer. */ public BoolExpr checkIntOverflow(ArithExpr result) { - return ctx.mkOr(ctx.mkGt(result, ctx.mkInt(MAX_INT64)), ctx.mkLt(result, ctx.mkInt(MIN_INT64))); + return ctx.mkOr( + ctx.mkGt(result, ctx.mkInt(CelNumericBounds.MAX_INT64)), + ctx.mkLt(result, ctx.mkInt(CelNumericBounds.MIN_INT64))); } /** Checks if the given arithmetic expression overflows CEL Timestamp bounds. */ @@ -823,7 +826,8 @@ public BoolExpr checkDurationOverflow(ArithExpr result) { /** Checks if the given arithmetic expression overflows a 64-bit unsigned integer. */ public BoolExpr checkUintOverflow(ArithExpr result) { - return ctx.mkOr(ctx.mkGt(result, ctx.mkInt(MAX_UINT64)), ctx.mkLt(result, ctx.mkInt(0))); + return ctx.mkOr( + ctx.mkGt(result, ctx.mkInt(CelNumericBounds.MAX_UINT64)), ctx.mkLt(result, ctx.mkInt(0))); } /** Safely concatenates two Z3 sequences. */ diff --git a/verifier/src/main/java/dev/cel/verifier/TranslatedValue.java b/verifier/src/main/java/dev/cel/verifier/TranslatedValue.java index 032c9dcdc..506f0bbc7 100644 --- a/verifier/src/main/java/dev/cel/verifier/TranslatedValue.java +++ b/verifier/src/main/java/dev/cel/verifier/TranslatedValue.java @@ -144,7 +144,6 @@ static TranslatedValue propagateStrict( Collection args) { List exactErrors = new ArrayList<>(); List exactUnknowns = new ArrayList<>(); - List errors = new ArrayList<>(); List unknowns = new ArrayList<>(); List taints = new ArrayList<>(); taints.add(baseTaint); @@ -164,7 +163,6 @@ static TranslatedValue propagateStrict( BoolExpr isError = ts.isError(z3Expr); BoolExpr isUnknown = ts.isUnknown(z3Expr); - errors.add(isError); unknowns.add(isUnknown); exactErrors.add( @@ -180,17 +178,18 @@ static TranslatedValue propagateStrict( return create(baseResult, celExpr, ts, anyTaint); } + List> z3Args = new ArrayList<>(); + for (TranslatedValue arg : argsList) { + if (!arg.isLiteral(ExprKind.Kind.CONSTANT)) { + z3Args.add(arg.z3Expr()); + } + } + Expr finalResult = ts.propagateErrorAndUnknown(baseResult, z3Args); + BoolExpr hasExactError = CelZ3TypeSystem.mkOrFlattened(ctx, exactErrors); BoolExpr hasExactUnknown = CelZ3TypeSystem.mkOrFlattened(ctx, exactUnknowns); - BoolExpr hasError = CelZ3TypeSystem.mkOrFlattened(ctx, errors); BoolExpr hasUnknown = CelZ3TypeSystem.mkOrFlattened(ctx, unknowns); - Expr finalResult = - CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) - .addCase(hasUnknown, ts.mkUnknown()) - .addCase(hasError, ts.mkError()) - .build(baseResult); - BoolExpr isSafe = CelZ3TypeSystem.mkOrFlattened( ctx, diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/BUILD.bazel b/verifier/src/main/java/dev/cel/verifier/axioms/BUILD.bazel index 02752c10e..c397f1b45 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/BUILD.bazel +++ b/verifier/src/main/java/dev/cel/verifier/axioms/BUILD.bazel @@ -22,6 +22,7 @@ java_library( "//common/types", "//extensions:comprehensions", "//extensions:optional_library", + "//verifier:numeric_bounds", "//verifier:type_system", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/TypeAxiom.java b/verifier/src/main/java/dev/cel/verifier/axioms/TypeAxiom.java index 9c49ef958..61e941258 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/TypeAxiom.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/TypeAxiom.java @@ -47,7 +47,7 @@ final class TypeAxiom { // Custom approximation logic for type(): it is only approximate if the argument // is approximate AND the argument is an Error or Unknown. - BoolExpr isErrOrUnk = ctx.mkOr(typeSystem.isError(val), typeSystem.isUnknown(val)); + BoolExpr isErrOrUnk = typeSystem.isErrorOrUnknown(val); BoolExpr typeApprox = ctx.mkAnd(argApprox, isErrOrUnk); return Optional.of(CelZ3OverloadResult.create(result, typeApprox)); diff --git a/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java b/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java index f4dba5afc..2064047fd 100644 --- a/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java +++ b/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java @@ -14,7 +14,7 @@ package dev.cel.verifier.axioms; -import static dev.cel.verifier.CelZ3TypeSystem.MAX_INT64; +import static dev.cel.verifier.CelNumericBounds.MAX_INT64; import com.google.common.collect.ImmutableList; import com.microsoft.z3.BoolExpr; diff --git a/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierRepl.java b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierRepl.java index 25354480a..94348ff15 100644 --- a/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierRepl.java +++ b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierRepl.java @@ -50,7 +50,11 @@ static int runInteractiveRepl() { BufferedReader fallbackReader = null; try { Terminal terminal = TerminalBuilder.builder().system(true).build(); - lineReader = LineReaderBuilder.builder().terminal(terminal).build(); + lineReader = + LineReaderBuilder.builder() + .terminal(terminal) + .option(LineReader.Option.DISABLE_EVENT_EXPANSION, true) + .build(); } catch (Exception e) { fallbackReader = new BufferedReader(new InputStreamReader(System.in, UTF_8)); } diff --git a/verifier/src/test/java/dev/cel/verifier/BUILD.bazel b/verifier/src/test/java/dev/cel/verifier/BUILD.bazel index 6bf44cafd..f1669c486 100644 --- a/verifier/src/test/java/dev/cel/verifier/BUILD.bazel +++ b/verifier/src/test/java/dev/cel/verifier/BUILD.bazel @@ -47,13 +47,13 @@ java_library( "//:java_truth", "@maven//:tools_aqua_z3_turnkey", "//verifier", + "//verifier:numeric_bounds", "//verifier:policy_verifier", "//verifier:policy_verifier_factory", "//verifier:type_system", "//verifier:verifier_factory", "//verifier:z3_impl", "//verifier/axioms", - "//verifier/tools", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@maven//:com_google_guava_guava", ], diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index 696a21387..cea14e910 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -139,25 +139,25 @@ public void setUp() { } private enum IsSatisfiableTestCase { - SATISFIABLE("x > 5"), - DYNAMIC_ARITHMETIC("request == 1 && request + 2 == 3"), - DYNAMIC_ARITHMETIC_UNARY("request == 1 && -request == -1"), - GREATER_DOUBLE("d > 1.5"), - LESS_EQUALS_UINT64("u <= 5u"), - LESS_EQUALS_DOUBLE("d <= 5.5"), - LESS_EQUALS_STRING("role <= 'admin'"), - LESS_EQUALS_BYTES("by <= b'bytes'"), - GREATER_STRING("role > 'admin'"), - GREATER_BYTES("by > b'bytes'"), - DYNAMIC_LIST_COMPREHENSION_EXISTS("int_list.exists(x, x > 5)"), - DYNAMIC_MAP_COMPREHENSION_EXISTS("string_int_map.exists(k, k == 'test')"), - NULL_SATISFIABLE("unknown_var == null"), - DYNAMIC_VAR_NUMERIC_EQUALITY("dyn_var == 1 && dyn_var == 1.0"), - DYNAMIC_VAR_NOT_IN_LIST("dyn_var == 1.5 && !(dyn_var in dyn_list) && size(dyn_list) > 5"), - CROSS_NUMERIC_EQUALITY_INT_DYN_EXACT("1 == request"), - MACRO_LIMIT("dyn_list.all(x, x == 1)"), - STRUCT_FIELD_MISSING_APPROXIMATE_SATISFIABLE("dyn_var.unknown_field"), - NULLABLE_INT_SATISFIABLE("nullable_int == 123"), + SATISFIABLE("x > 5", "x = (?:[6-9]|[1-9]\\d+)"), + DYNAMIC_ARITHMETIC("request == 1 && request + 2 == 3", "request = 1"), + DYNAMIC_ARITHMETIC_UNARY("request == 1 && -request == -1", "request = 1"), + GREATER_DOUBLE("d > 1.5", "d = "), + LESS_EQUALS_UINT64("u <= 5u", "u = [0-5]u"), + LESS_EQUALS_DOUBLE("d <= 5.5", "d = "), + LESS_EQUALS_STRING("role <= 'admin'", "role = "), + LESS_EQUALS_BYTES("by <= b'bytes'", "by = "), + GREATER_STRING("role > 'admin'", "role = "), + GREATER_BYTES("by > b'bytes'", "by = "), + DYNAMIC_LIST_COMPREHENSION_EXISTS("int_list.exists(x, x > 5)", "int_list = "), + DYNAMIC_MAP_COMPREHENSION_EXISTS("string_int_map.exists(k, k == 'test')", "string_int_map = "), + NULL_SATISFIABLE("unknown_var == null", "unknown_var = null"), + DYNAMIC_VAR_NUMERIC_EQUALITY("dyn_var == 1 && dyn_var == 1.0", "dyn_var = 1"), + DYNAMIC_VAR_NOT_IN_LIST( + "dyn_var == 1.5 && !(dyn_var in dyn_list) && size(dyn_list) > 5", "dyn_var = 1\\.5"), + CROSS_NUMERIC_EQUALITY_INT_DYN_EXACT("1 == request", "request = 1"), + MACRO_LIMIT("dyn_list.all(x, x == 1)", "Satisfying input:"), + STRUCT_FIELD_MISSING_APPROXIMATE_SATISFIABLE("dyn_var.unknown_field", "dyn_var = "), MAP_INDEX_SATISFIABLE("string_int_map['alice'] > 0", "\"alice\": [1-9]\\d*"), MAP_SIZE_GREATER_THAN_ONE_WITH_KEY( "string_int_map.size() > 1 && string_int_map['foo'] == 42", @@ -165,6 +165,25 @@ private enum IsSatisfiableTestCase { MAP_SIZE_GREATER_THAN_ONE_WITH_LIST_ELEMENT( "string_int_map.size() > 1 && string_int_map['a'] == int_list[0] && int_list.size() == 1", "string_int_map = \\{[^}]*,[^}]*\\}"), + DISTINCT_TRUNCATED_COMPREHENSIONS( + "dyn_list.all(x, x == 1) != dyn_list.all(x, x == 2)", "dyn_list = "), + DISTINCT_TRUNCATED_COMPREHENSIONS_FREE_VARS( + "dyn_list.all(e, x == x) != dyn_list.all(e, x == y)", "dyn_list = "), + DISTINCT_TRUNCATED_COMPREHENSIONS_STRUCTS( + "dyn_list.all(e, e == TestAllTypes{single_int64: 1}) !=" + + " dyn_list.all(e, e == TestAllTypes{single_int32: 1})", + "dyn_list = "), + DISTINCT_TRUNCATED_COMPREHENSIONS_STRUCT_MESSAGE_NAMES( + "dyn_list.all(e, e == TestAllTypes{single_int64: 1}) !=" + + " dyn_list.all(e, e == TestAllTypes.NestedMessage{bb: 1})", + "dyn_list = "), + MAP_SIZE_GREATER_THAN_CONSTRAINED_KEYS( + "string_int_map.size() == 5 && string_int_map['a'] == 10", "\"a\": 10"), + INT_MIN_DOUBLE_EQUALITY( + "dyn(request) == -9223372036854775808.0", "request = -9223372036854775[2-8]\\d+"), + INT_MAX_DOUBLE_EQUALITY( + "dyn(request) == 9223372036854775808.0", "request = 9223372036854775[2-8]\\d+"), + UINT_MAX_DOUBLE_EQUALITY("dyn(u) == 18446744073709551616.0", "u = 1844674407370955\\d+u"), ; final String expr; @@ -259,6 +278,18 @@ public void counterexample_nullValueFormattedAsNull() throws Exception { assertThat(result.message()).contains("request = null"); } + @Test + public void counterexample_mapFormattedCorrectly() throws Exception { + CelAbstractSyntaxTree ast = + CEL.compile("string_int_map.size() == 2 && string_int_map['a'] == 1").getAst(); + + CelVerificationResult result = VERIFIER.isSatisfiable(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + assertThat(result.message()).containsMatch("string_int_map = \\{[^}]*,[^}]*\\}"); + assertThat(result.message()).contains("\"a\": 1"); + } + private enum CounterexampleNeverErrorTestCase { DYN_LIST_REFLEXIVITY("dyn_list.size() == 1 ? dyn_list[0] == dyn_list[0] : true"), DYN_MAP_REFLEXIVITY("dyn_map.size() == 1 ? dyn_map[1] == dyn_map[1] : true"), @@ -400,6 +431,15 @@ private enum IsUnsatisfiableTestCase { + " string_int_map.size() == 2"), EMPTY_MAP_KEY_LOOKUP("string_int_map['a'] > 100 && string_int_map.size() == 0"), EMPTY_MAP_DYNAMIC_KEY_LOOKUP("string_int_map[string_var] == 100 && string_int_map.size() == 0"), + INT_NON_INTEGER_DOUBLE_EQUALITY("dyn(x) == 1.5"), + INT_OUT_OF_BOUNDS_POS_DOUBLE_EQUALITY("dyn(x) == 9223372036854777856.0"), + INT_OUT_OF_BOUNDS_NEG_DOUBLE_EQUALITY("dyn(x) == -9223372036854777856.0"), + UINT_NEGATIVE_DOUBLE_EQUALITY("dyn(u) == -1.0"), + UINT_NON_INTEGER_DOUBLE_EQUALITY("dyn(u) == 1.5"), + UINT_OUT_OF_BOUNDS_POS_DOUBLE_EQUALITY("dyn(u) == 18446744073709555712.0"), + INT_OUT_OF_BOUNDS_LARGE_DOUBLE_EQUALITY("dyn(x) == 1e100"), + INT_OUT_OF_BOUNDS_LARGE_NEG_DOUBLE_EQUALITY("dyn(x) == -1e100"), + UINT_OUT_OF_BOUNDS_LARGE_DOUBLE_EQUALITY("dyn(u) == 1e100"), ; final String expr; @@ -518,7 +558,7 @@ private enum IsAlwaysTrueTestCase { "{'a': x, 'b': y}.all(z, {'a': x, 'b': y}.exists(z, z == 'a' || z == 'b'))"), MAP_LITERAL_VARIABLE_VALUE("{'a': x}['a'] == x"), HETEROGENEOUS_LARGE_UINT_INT_VARIABLE_NEQ( - "unknown_var == " + CelZ3TypeSystem.MAX_UINT64 + "u ? unknown_var != -1 : true"), + "unknown_var == " + CelNumericBounds.MAX_UINT64 + "u ? unknown_var != -1 : true"), MAP_LITERAL_VARIABLE_KEY("x != y ? {x: 1, y: 2}[x] == 1 : true"), MAP_MACRO_LIST_RETURN("{'a': 1, 'b': 2}.map(x, x + 'a') == ['aa', 'ba']"), MAP_LITERAL_NESTED_LIST("{'a': [1, 2]} == {'a': [1, 2]}"), @@ -695,12 +735,27 @@ private enum IsAlwaysTrueTestCase { "unknown_var == 9223372036854775807 ? unknown_var != 1e100 : true"), HETEROGENEOUS_MAX_EXACT_INT("dyn(9007199254740992) == 9007199254740992.0"), HETEROGENEOUS_MIN_EXACT_INT("dyn(-9007199254740992) == -9007199254740992.0"), - HETEROGENEOUS_INT_PRECISION_LOSS_POS("dyn(9007199254740993) != 9007199254740992.0"), - HETEROGENEOUS_INT_PRECISION_LOSS_NEG("dyn(-9007199254740993) != -9007199254740992.0"), - HETEROGENEOUS_UINT_PRECISION_LOSS("dyn(9007199254740993u) != 9007199254740992.0"), + HETEROGENEOUS_INT_PRECISION_LOSS_POS("dyn(9007199254740993) == 9007199254740992.0"), + HETEROGENEOUS_INT_PRECISION_LOSS_NEG("dyn(-9007199254740993) == -9007199254740992.0"), + HETEROGENEOUS_UINT_PRECISION_LOSS("dyn(9007199254740993u) == 9007199254740992.0"), HETEROGENEOUS_LONG_MAX_VS_DOUBLE("dyn(9223372036854775807) == 9223372036854775808.0"), - HETEROGENEOUS_LONG_MIN_VS_DOUBLE("dyn(-9223372036854775808) == -9223372036854775808.0"), - HETEROGENEOUS_UINT_MAX_VS_DOUBLE("dyn(18446744073709551615u) != 18446744073709551616.0"), + HETEROGENEOUS_STATIC_INT_DOUBLE_EQ("dyn(1) == 1.0"), + HETEROGENEOUS_STATIC_UINT_DOUBLE_EQ("dyn(1u) == 1.0"), + HETEROGENEOUS_STATIC_INT_UINT_EQ("dyn(1) == 1u"), + HETEROGENEOUS_LONG_MIN_VS_DOUBLE( + "dyn_var == -9223372036854775808.0 ? dyn_var == -9223372036854775808.0 : true"), + HETEROGENEOUS_UINT_MAX_VS_DOUBLE("dyn(18446744073709551615u) == 18446744073709551616.0"), + HETEROGENEOUS_UINT_OVERFLOW_VS_DOUBLE("dyn_var == 1e100 ? type(dyn_var) != uint : true"), + HETEROGENEOUS_UINT_NEG_VS_DOUBLE("dyn_var == -1.0 ? type(dyn_var) != uint : true"), + HETEROGENEOUS_INT_OVERFLOW_VS_DOUBLE("dyn_var == 1e100 ? type(dyn_var) != int : true"), + HETEROGENEOUS_INT_UNDERFLOW_VS_DOUBLE( + "dyn_var == -9223372036854777856.0 ? type(dyn_var) != int : true"), + HETEROGENEOUS_INT_NON_INTEGER_DOUBLE_INEQUALITY("dyn(x) != 1.5"), + HETEROGENEOUS_INT_OUT_OF_BOUNDS_POS_DOUBLE_INEQUALITY("dyn(x) != 9223372036854777856.0"), + HETEROGENEOUS_INT_OUT_OF_BOUNDS_NEG_DOUBLE_INEQUALITY("dyn(x) != -9223372036854777856.0"), + HETEROGENEOUS_UINT_NON_INTEGER_DOUBLE_INEQUALITY("dyn(u) != 1.5"), + HETEROGENEOUS_UINT_OUT_OF_BOUNDS_POS_DOUBLE_INEQUALITY("dyn(u) != 18446744073709555712.0"), + HETEROGENEOUS_UINT_OUT_OF_BOUNDS_NEG_DOUBLE_INEQUALITY("dyn(u) != -1.0"), HETEROGENEOUS_DYNAMIC_PRECISION( "type(dyn_var) == int && type(dyn_var2) == double && dyn_var == 9007199254740993 &&" + " dyn_var2 == 9007199254740992.0 ? dyn_var != dyn_var2 : true"), @@ -727,6 +782,8 @@ private enum IsAlwaysTrueTestCase { DYNAMIC_LIST_RESOLVES_CORRELATED_NESTING( "int_list == [1, 2] && int_list_2 == [2, 3] ? int_list.exists(x, int_list_2.exists(y, x" + " == y)) : true"), + DYNAMIC_LIST_ELEMENT_NEVER_ERROR( + "size(dyn_list) > 0 ? (dyn_list[0] == 1 || dyn_list[0] != 1) : true"), DYNAMIC_MAP_EXISTS( "string_int_map == {'a': 1, 'b': 2} ? string_int_map.exists(k, string_int_map[k] == 2)" + " : true"), @@ -859,6 +916,15 @@ private enum IsAlwaysTrueTestCase { DYNAMIC_VAR_TYPE_IDENTITY("type(dyn_var) == type(dyn_var)"), DYNAMIC_MAP_KEY_COMPREHENSION_TYPE_IDENTITY( "size(dyn_map) > 0 && size(dyn_map) <= 5 ? dyn_map.all(k, type(k) == type(k)) : true"), + DYNAMIC_MAP_VALUE_NOT_ERROR( + "size(dyn_map) == 1 && 1 in dyn_map ? dyn_map.all(k, v, v == 1 || v != 1) : true"), + MAP_KEY_TYPE_CONSTRAINT( + "size(string_int_map) > 0 && size(string_int_map) <= 5 ?" + + " string_int_map.all(k, type(k) == string) : true"), + MAP_VALUE_TYPE_CONSTRAINT( + "size(string_int_map) > 0 && size(string_int_map) <= 5 ?" + + " string_int_map.all(k, v, v >= -9223372036854775808 &&" + + " v <= 9223372036854775807) : true"), ; final String expr; @@ -1011,6 +1077,18 @@ public void verifyEquivalence_unknownPrecedenceOverError() throws Exception { assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); } + @Test + public void verifyEquivalence_freeVariableIndicesDeduplicated() throws Exception { + CelAbstractSyntaxTree astA = + CEL.compile("x == y && y == port ? dyn_list.all(e, x == x) : false").getAst(); + CelAbstractSyntaxTree astB = + CEL.compile("x == y && y == port ? dyn_list.all(e, y == port) : false").getAst(); + + CelVerificationResult result = VERIFIER.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + } + @Test public void isSatisfiable_approximateIterRangeInMap_inconclusive() throws Exception { Cel celWithCustomFunc = @@ -1532,7 +1610,10 @@ private enum IsInconclusiveTestCase { COMPREHENSION_NULL_CONSTANT("size(int_list) == 6 ? size(int_list.map(x, null)) == 6 : true"), COMPREHENSION_UINT_CONSTANT("size(int_list) == 6 ? size(int_list.map(x, 1u)) == 6 : true"), COMPREHENSION_DOUBLE_CONSTANT("size(int_list) == 6 ? size(int_list.map(x, 1.0)) == 6 : true"), - COMPREHENSION_BYTES_CONSTANT("size(int_list) == 6 ? size(int_list.map(x, b'abc')) == 6 : true"); + COMPREHENSION_BYTES_CONSTANT("size(int_list) == 6 ? size(int_list.map(x, b'abc')) == 6 : true"), + COMPREHENSION_FREE_VAR_INDEX_DEDUPLICATION( + "x == y && y == port ? dyn_list.all(e, x == x) == dyn_list.all(e, y == port) : true"), + ; final String expr; @@ -1891,7 +1972,16 @@ private enum EquivalenceTestCase { OPTIONAL_INDEX_LIST_UNWRAPPING("optional.of([1, 2, 3])[?0]", "optional.of(1)"), OPTIONAL_INDEX_MAP_UNWRAPPING("optional.of({'a': 1})[?'a']", "optional.of(1)"), OPTIONAL_INDEX_UNWRAPPING_NONE("optional.none()[?0]", "optional.none()"), - INT_IN_LIST_IDENTITY_EQUIVALENT("x in [1, 2, x]", "true"); + INT_IN_LIST_IDENTITY_EQUIVALENT("x in [1, 2, x]", "true"), + HETEROGENEOUS_INT_NON_INTEGER_DOUBLE_EQUIVALENCE("dyn(x) == 1.5", "false"), + HETEROGENEOUS_INT_OUT_OF_BOUNDS_DOUBLE_EQUIVALENCE("dyn(x) == 9223372036854777856.0", "false"), + HETEROGENEOUS_UINT_NON_INTEGER_DOUBLE_EQUIVALENCE("dyn(u) == 1.5", "false"), + HETEROGENEOUS_UINT_OUT_OF_BOUNDS_DOUBLE_EQUIVALENCE( + "dyn(u) == 18446744073709555712.0", "false"), + HETEROGENEOUS_UINT_NEGATIVE_DOUBLE_EQUIVALENCE("dyn(u) == -1.0", "false"), + HETEROGENEOUS_UINT_ZERO_DOUBLE_EQUIVALENCE("dyn(u) == 0.0", "u == 0u"), + DYNAMIC_LIST_ELEMENT_NEVER_ERROR_EQUIVALENCE( + "size(dyn_list) > 0 ? (dyn_list[0] == 1 || dyn_list[0] != 1) : true", "true"); private final String exprA; private final String exprB; @@ -2737,6 +2827,50 @@ public void isAlwaysTrue_largeListCounterexample_truncatesOutput() throws Except assertThat(result.message()).contains("... (5 more elements)"); } + @Test + public void isAlwaysTrue_largeMapCounterexample_truncatesOutput() throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .addVar("large_map", MapType.create(SimpleType.INT, SimpleType.INT)) + .build(); + StringBuilder mapLiteral = new StringBuilder("{"); + for (int i = 0; i < 20; i++) { + mapLiteral.append(i).append(": 1"); + if (i < 19) { + mapLiteral.append(", "); + } + } + mapLiteral.append("}"); + + CelAbstractSyntaxTree ast = cel.compile("!(large_map == " + mapLiteral + ")").getAst(); + CelVerifier verifier = + CelVerifierFactory.newVerifier().setTimeout(Duration.ofSeconds(10)).build(); + + CelVerificationResult result = verifier.isAlwaysTrue(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()).contains("... (5 more entries)"); + } + + @Test + public void counterexample_messageTwoFieldsFormatted() throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .addMessageTypes(TestAllTypes.getDescriptor()) + .addVar("msg", StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")) + .setTypeProvider(TYPE_PROVIDER) + .build(); + CelAbstractSyntaxTree ast = + cel.compile("!(msg == TestAllTypes{single_int32: 1, single_int64: 2})").getAst(); + + CelVerificationResult result = VERIFIER.isAlwaysTrue(ast); + + assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); + assertThat(result.message()).contains("single_int32: 1"); + assertThat(result.message()).contains("single_int64: 2"); + } + @Test public void isAlwaysTrue_customComprehensionWithTrueAccuInit() throws Exception { Cel cel = diff --git a/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierReplTest.java b/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierReplTest.java index 52124c6ed..5b289c39a 100644 --- a/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierReplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierReplTest.java @@ -168,6 +168,12 @@ public void repl_equivQueries() throws Exception { assertThat(output[1]).contains("Equivalence query format: equiv <=> "); } + @Test + public void repl_equivDoubleNegation() throws Exception { + String[] output = runReplWithCommands(":var x int", "equiv !!(x == 10) <=> (x == 10)", ":quit"); + assertThat(output[0]).contains("[VERIFIED]"); + } + @Test public void repl_unknownCommandsAndErrors() throws Exception { String[] output = From 81064672ad10dfc9bff5566a411f9082edd37d0a Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Thu, 6 Aug 2026 20:41:03 -0700 Subject: [PATCH 163/204] Add a canonicalization pass to CEL verifier PiperOrigin-RevId: 960663448 --- verifier/BUILD.bazel | 7 + .../main/java/dev/cel/verifier/BUILD.bazel | 34 + .../verifier/CanonicalizationOptimizer.java | 670 ++++++++++++++++++ .../dev/cel/verifier/CelVerifierFactory.java | 31 +- .../dev/cel/verifier/CelVerifierZ3Impl.java | 41 +- .../verifier/tools/CelVerifierToolCore.java | 42 +- .../test/java/dev/cel/verifier/BUILD.bazel | 3 + .../CanonicalizationOptimizerTest.java | 566 +++++++++++++++ .../cel/verifier/CelVerifierZ3ImplTest.java | 75 +- .../verifier/tools/CelVerifierReplTest.java | 14 + .../verifier/tools/CelVerifierToolTest.java | 17 + 11 files changed, 1450 insertions(+), 50 deletions(-) create mode 100644 verifier/src/main/java/dev/cel/verifier/CanonicalizationOptimizer.java create mode 100644 verifier/src/test/java/dev/cel/verifier/CanonicalizationOptimizerTest.java diff --git a/verifier/BUILD.bazel b/verifier/BUILD.bazel index 9ec441ed4..ef1316ca2 100644 --- a/verifier/BUILD.bazel +++ b/verifier/BUILD.bazel @@ -61,3 +61,10 @@ java_library( visibility = [":verifier_internal"], exports = ["//verifier/src/main/java/dev/cel/verifier:z3_impl"], ) + +java_library( + name = "canonicalization_optimizer", + compatible_with = [], + visibility = [":verifier_internal"], + exports = ["//verifier/src/main/java/dev/cel/verifier:canonicalization_optimizer"], +) diff --git a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel index ab341fba2..a0de7948a 100644 --- a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel +++ b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel @@ -35,6 +35,12 @@ java_library( deps = [ ":verifier", ":z3_impl", + "//bundle:cel", + "//checker:checker_builder", + "//compiler", + "//compiler:compiler_builder", + "//parser:parser_builder", + "//runtime", ], ) @@ -119,6 +125,29 @@ java_library( ], ) +java_library( + name = "canonicalization_optimizer", + srcs = ["CanonicalizationOptimizer.java"], + tags = [ + ], + deps = [ + "//:auto_value", + "//bundle:cel", + "//common:cel_ast", + "//common:mutable_ast", + "//common:mutable_source", + "//common:operator", + "//common/ast", + "//common/ast:mutable_expr", + "//common/navigation:common", + "//common/navigation:mutable_navigation", + "//common/values:cel_byte_string", + "//optimizer:ast_optimizer", + "//optimizer:mutable_ast", + "@maven//:com_google_guava_guava", + ], +) + java_library( name = "z3_impl", srcs = [ @@ -135,10 +164,12 @@ java_library( tags = [ ], deps = [ + ":canonicalization_optimizer", ":numeric_bounds", ":type_system", ":verifier", "//:auto_value", + "//bundle:cel", "//common:cel_ast", "//common:compiler_common", "//common:operator", @@ -147,6 +178,9 @@ java_library( "//common/types", "//common/types:cel_types", "//common/types:type_providers", + "//optimizer", + "//optimizer:optimization_exception", + "//optimizer:optimizer_builder", "//verifier/axioms", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", diff --git a/verifier/src/main/java/dev/cel/verifier/CanonicalizationOptimizer.java b/verifier/src/main/java/dev/cel/verifier/CanonicalizationOptimizer.java new file mode 100644 index 000000000..6532afd7f --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CanonicalizationOptimizer.java @@ -0,0 +1,670 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ComparisonChain; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import dev.cel.bundle.Cel; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelMutableAst; +import dev.cel.common.CelMutableSource; +import dev.cel.common.Operator; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import dev.cel.common.ast.CelMutableExpr; +import dev.cel.common.ast.CelMutableExpr.CelMutableCall; +import dev.cel.common.ast.CelMutableExpr.CelMutableComprehension; +import dev.cel.common.ast.CelMutableExpr.CelMutableMap; +import dev.cel.common.ast.CelMutableExpr.CelMutableSelect; +import dev.cel.common.ast.CelMutableExpr.CelMutableStruct; +import dev.cel.common.navigation.CelNavigableMutableAst; +import dev.cel.common.navigation.CelNavigableMutableExpr; +import dev.cel.common.navigation.TraversalOrder; +import dev.cel.common.values.CelByteString; +import dev.cel.optimizer.AstMutator; +import dev.cel.optimizer.CelAstOptimizer; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Standalone AST canonicalization pass that normalizes commutative operator ordering and De Morgan + * quantifier/logical identities. + * + *

This optimizer performs: + * + *

    + *
  • Deterministic sorting of symmetric/commutative operators ({@code LOGICAL_AND}, {@code + * LOGICAL_OR}, {@code EQUALS}, {@code NOT_EQUALS}). + *
  • Negation Normal Form (NNF) and De Morgan quantifier normalization ({@code !exists(x, P) -> + * all(x, !P)} and {@code !all(x, P) -> exists(x, !P)}). + *
+ * + *

Caveat: This is a structural normalizer intended for comparison purposes (such as + * formal equivalence verification) and as a pre-processor for helping other optimizers (such as + * Common Subexpression Elimination). It is not a runtime cost optimizer; lexicographical ordering + * of calls or Negation Normal Form expansions are not designed to optimize runtime execution or + * short-circuit latency. + */ +final class CanonicalizationOptimizer implements CelAstOptimizer { + + private final CanonicalizationOptions canonicalizationOptions; + + private static final Comparator EXPR_COMPARATOR = + new Comparator() { + @Override + public int compare(CelMutableExpr e1, CelMutableExpr e2) { + int kindCmp = + Integer.compare(getKindPriority(e1.getKind()), getKindPriority(e2.getKind())); + if (kindCmp != 0) { + return kindCmp; + } + switch (e1.getKind()) { + case CONSTANT: + return compareConstants(e1.constant(), e2.constant()); + case IDENT: + return e1.ident().name().compareTo(e2.ident().name()); + case SELECT: + return compareSelect(e1.select(), e2.select()); + case CALL: + return compareCall(e1.call(), e2.call()); + case LIST: + return compareList(e1.list().elements(), e2.list().elements()); + case MAP: + return compareMap(e1.map(), e2.map()); + case STRUCT: + return compareStruct(e1.struct(), e2.struct()); + case COMPREHENSION: + return compareComprehension(e1.comprehension(), e2.comprehension()); + case NOT_SET: + return 0; + } + throw new UnsupportedOperationException("Unsupported expression kind: " + e1.getKind()); + } + + private int compareConstants(CelConstant c1, CelConstant c2) { + int constKindCmp = c1.getKind().name().compareTo(c2.getKind().name()); + if (constKindCmp != 0) { + return constKindCmp; + } + switch (c1.getKind()) { + case NULL_VALUE: + case NOT_SET: + return 0; + case BOOLEAN_VALUE: + return Boolean.compare(c1.booleanValue(), c2.booleanValue()); + case INT64_VALUE: + return Long.compare(c1.int64Value(), c2.int64Value()); + case UINT64_VALUE: + return c1.uint64Value().compareTo(c2.uint64Value()); + case DOUBLE_VALUE: + return Double.compare(c1.doubleValue(), c2.doubleValue()); + case STRING_VALUE: + return c1.stringValue().compareTo(c2.stringValue()); + case BYTES_VALUE: + return CelByteString.unsignedLexicographicalComparator() + .compare(c1.bytesValue(), c2.bytesValue()); + default: + throw new UnsupportedOperationException("Unsupported constant kind: " + c1.getKind()); + } + } + + private int compareSelect(CelMutableSelect s1, CelMutableSelect s2) { + return ComparisonChain.start() + .compare(s1.operand(), s2.operand(), this) + .compare(s1.field(), s2.field()) + .compareFalseFirst(s1.testOnly(), s2.testOnly()) + .result(); + } + + private int compareCall(CelMutableCall c1, CelMutableCall c2) { + int fnCmp = c1.function().compareTo(c2.function()); + if (fnCmp != 0) { + return fnCmp; + } + boolean hasT1 = c1.target().isPresent(); + boolean hasT2 = c2.target().isPresent(); + if (hasT1 != hasT2) { + return Boolean.compare(hasT1, hasT2); + } + if (hasT1) { + int tCmp = compare(c1.target().get(), c2.target().get()); + if (tCmp != 0) { + return tCmp; + } + } + return compareList(c1.args(), c2.args()); + } + + private int compareMap(CelMutableMap m1, CelMutableMap m2) { + int mapSizeCmp = Integer.compare(m1.entries().size(), m2.entries().size()); + if (mapSizeCmp != 0) { + return mapSizeCmp; + } + Iterator it2 = m2.entries().iterator(); + for (CelMutableMap.Entry entry1 : m1.entries()) { + CelMutableMap.Entry entry2 = it2.next(); + int cmp = + ComparisonChain.start() + .compare(entry1.key(), entry2.key(), this) + .compare(entry1.value(), entry2.value(), this) + .result(); + if (cmp != 0) { + return cmp; + } + } + return 0; + } + + private int compareStruct(CelMutableStruct s1, CelMutableStruct s2) { + int msgCmp = s1.messageName().compareTo(s2.messageName()); + if (msgCmp != 0) { + return msgCmp; + } + int structSizeCmp = Integer.compare(s1.entries().size(), s2.entries().size()); + if (structSizeCmp != 0) { + return structSizeCmp; + } + Iterator it2 = s2.entries().iterator(); + for (CelMutableStruct.Entry entry1 : s1.entries()) { + CelMutableStruct.Entry entry2 = it2.next(); + int cmp = + ComparisonChain.start() + .compare(entry1.fieldKey(), entry2.fieldKey()) + .compare(entry1.value(), entry2.value(), this) + .result(); + if (cmp != 0) { + return cmp; + } + } + return 0; + } + + private int compareComprehension(CelMutableComprehension c1, CelMutableComprehension c2) { + return ComparisonChain.start() + .compare(c1.iterVar(), c2.iterVar()) + .compare(c1.iterVar2(), c2.iterVar2()) + .compare(c1.accuVar(), c2.accuVar()) + .compare(c1.iterRange(), c2.iterRange(), this) + .compare(c1.accuInit(), c2.accuInit(), this) + .compare(c1.loopCondition(), c2.loopCondition(), this) + .compare(c1.loopStep(), c2.loopStep(), this) + .compare(c1.result(), c2.result(), this) + .result(); + } + + private int compareList(List l1, List l2) { + int sizeCmp = Integer.compare(l1.size(), l2.size()); + if (sizeCmp != 0) { + return sizeCmp; + } + Iterator it2 = l2.iterator(); + for (CelMutableExpr elem1 : l1) { + int cmp = compare(elem1, it2.next()); + if (cmp != 0) { + return cmp; + } + } + return 0; + } + + private int getKindPriority(Kind kind) { + switch (kind) { + case IDENT: + return 1; + case SELECT: + return 2; + case CALL: + return 3; + case LIST: + return 4; + case MAP: + return 5; + case STRUCT: + return 6; + case COMPREHENSION: + return 7; + case CONSTANT: + return 8; + default: + return 99; + } + } + }; + + /** + * Returns a new instance of canonicalization optimizer configured with the provided {@link + * CanonicalizationOptions}. + */ + static CanonicalizationOptimizer newInstance(CanonicalizationOptions canonicalizationOptions) { + return new CanonicalizationOptimizer(canonicalizationOptions); + } + + @Override + public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) { + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + mutableAst = runCanonicalizationLoop(mutableAst); + for (Map.Entry entry : + new HashMap<>(mutableAst.source().getMacroCalls()).entrySet()) { + CelMutableExpr canonicalMacro = canonicalize(entry.getValue()); + mutableAst.source().addMacroCalls(entry.getKey(), canonicalMacro); + } + CelAbstractSyntaxTree optimizedAst = + AstMutator.newInstance(canonicalizationOptions.maxIterationLimit()) + .renumberIdsConsecutively(mutableAst) + .toParsedAst(); + return OptimizationResult.create(optimizedAst); + } + + /** Canonicalizes a single CelMutableExpr subtree. */ + private CelMutableExpr canonicalize(CelMutableExpr root) { + CelMutableAst mutableAst = CelMutableAst.of(root, CelMutableSource.newInstance()); + mutableAst = runCanonicalizationLoop(mutableAst); + return mutableAst.expr(); + } + + private CelMutableAst runCanonicalizationLoop(CelMutableAst mutableAst) { + AstMutator astMutator = AstMutator.newInstance(canonicalizationOptions.maxIterationLimit()); + int iterCount = 0; + boolean continueCanonicalizing = true; + while (continueCanonicalizing) { + if (iterCount >= canonicalizationOptions.maxIterationLimit()) { + throw new IllegalStateException( + "Max iteration count reached in CanonicalizationOptimizer."); + } + iterCount++; + continueCanonicalizing = false; + ImmutableList candidateExprs = + CelNavigableMutableAst.fromAst(mutableAst) + .getRoot() + .allNodes(TraversalOrder.POST_ORDER) + .filter(CanonicalizationOptimizer::canCanonicalize) + .collect(toImmutableList()); + for (CelNavigableMutableExpr candidate : candidateExprs) { + iterCount++; + Optional newExpr = maybeCanonicalize(mutableAst, candidate); + if (newExpr.isPresent()) { + continueCanonicalizing = true; + mutableAst = astMutator.replaceSubtree(mutableAst, newExpr.get(), candidate.id()); + break; + } + } + } + return mutableAst; + } + + private static boolean canCanonicalize(CelNavigableMutableExpr navigable) { + CelMutableExpr expr = navigable.expr(); + return isCallWithArgCount(expr, Operator.LOGICAL_AND.getFunction(), 2) + || isCallWithArgCount(expr, Operator.LOGICAL_OR.getFunction(), 2) + || isCallWithArgCount(expr, Operator.EQUALS.getFunction(), 2) + || isCallWithArgCount(expr, Operator.NOT_EQUALS.getFunction(), 2) + || isCallWithArgCount(expr, Operator.LOGICAL_NOT.getFunction(), 1); + } + + private static boolean isComprehensionAccuVar(CelNavigableMutableExpr expr) { + return expr.allNodes() + .filter(node -> node.getKind().equals(Kind.IDENT)) + .anyMatch( + identNode -> { + String identName = identNode.expr().ident().name(); + CelNavigableMutableExpr curr = identNode; + Optional maybeParent = curr.parent(); + while (maybeParent.isPresent()) { + CelNavigableMutableExpr parent = maybeParent.get(); + if (parent.getKind().equals(Kind.COMPREHENSION)) { + CelMutableComprehension compre = parent.expr().comprehension(); + if (compre.accuVar().equals(identName) + && curr.id() != compre.iterRange().id() + && curr.id() != compre.accuInit().id()) { + return true; + } + } + curr = parent; + maybeParent = parent.parent(); + } + return false; + }); + } + + private static Optional maybeCanonicalize( + CelMutableAst mutableAst, CelNavigableMutableExpr navigableExpr) { + CelMutableExpr expr = navigableExpr.expr(); + if (expr.getKind() != Kind.CALL) { + return Optional.empty(); + } + CelMutableCall call = expr.call(); + String functionName = call.function(); + List args = call.args(); + + if ((functionName.equals(Operator.LOGICAL_AND.getFunction()) + || functionName.equals(Operator.LOGICAL_OR.getFunction())) + && args.size() == 2) { + List navigableOperands = + flattenNavigableOperands(navigableExpr, functionName); + if (navigableOperands.stream().anyMatch(CanonicalizationOptimizer::isComprehensionAccuVar)) { + return Optional.empty(); + } + List operands = new ArrayList<>(); + for (CelNavigableMutableExpr navOp : navigableOperands) { + operands.add(navOp.expr()); + } + operands.sort(EXPR_COMPARATOR); + List uniqueSorted = new ArrayList<>(); + for (CelMutableExpr op : operands) { + if (uniqueSorted.isEmpty() + || EXPR_COMPARATOR.compare(op, Iterables.getLast(uniqueSorted)) != 0) { + uniqueSorted.add(op); + } + } + CelMutableExpr rebuilt = uniqueSorted.get(0); + for (int i = 1; i < uniqueSorted.size(); i++) { + rebuilt = + CelMutableExpr.ofCall( + expr.id(), CelMutableCall.create(functionName, rebuilt, uniqueSorted.get(i))); + } + if (EXPR_COMPARATOR.compare(rebuilt, expr) == 0) { + return Optional.empty(); + } + return Optional.of(rebuilt); + } + + if ((functionName.equals(Operator.EQUALS.getFunction()) + || functionName.equals(Operator.NOT_EQUALS.getFunction())) + && args.size() == 2) { + CelMutableExpr arg0 = args.get(0); + CelMutableExpr arg1 = args.get(1); + if (EXPR_COMPARATOR.compare(arg0, arg1) > 0) { + return Optional.of( + CelMutableExpr.ofCall(expr.id(), CelMutableCall.create(functionName, arg1, arg0))); + } + return Optional.empty(); + } + + if (functionName.equals(Operator.LOGICAL_NOT.getFunction()) && args.size() == 1) { + CelMutableExpr target = args.get(0); + if (isCallWithArgCount(target, Operator.LOGICAL_NOT.getFunction(), 1)) { + return Optional.of(target.call().args().get(0)); + } + if (isCallWithArgCount(target, Operator.LOGICAL_AND.getFunction(), 2)) { + List subArgs = target.call().args(); + return Optional.of( + CelMutableExpr.ofCall( + expr.id(), + CelMutableCall.create( + Operator.LOGICAL_OR.getFunction(), + negate(subArgs.get(0)), + negate(subArgs.get(1))))); + } + if (isCallWithArgCount(target, Operator.LOGICAL_OR.getFunction(), 2)) { + List subArgs = target.call().args(); + return Optional.of( + CelMutableExpr.ofCall( + expr.id(), + CelMutableCall.create( + Operator.LOGICAL_AND.getFunction(), + negate(subArgs.get(0)), + negate(subArgs.get(1))))); + } + if (isCallWithArgCount(target, Operator.EQUALS.getFunction(), 2)) { + List subArgs = target.call().args(); + return Optional.of( + CelMutableExpr.ofCall( + expr.id(), + CelMutableCall.create( + Operator.NOT_EQUALS.getFunction(), subArgs.get(0), subArgs.get(1)))); + } + if (isCallWithArgCount(target, Operator.NOT_EQUALS.getFunction(), 2)) { + List subArgs = target.call().args(); + return Optional.of( + CelMutableExpr.ofCall( + expr.id(), + CelMutableCall.create( + Operator.EQUALS.getFunction(), subArgs.get(0), subArgs.get(1)))); + } + if (target.getKind() == Kind.COMPREHENSION) { + CelMutableComprehension comp = target.comprehension(); + if (isExistsMacro(mutableAst, target.id(), comp)) { + return negateComprehension(mutableAst, target.id(), comp, true); + } else if (isAllMacro(mutableAst, target.id(), comp)) { + return negateComprehension(mutableAst, target.id(), comp, false); + } + } + } + + return Optional.empty(); + } + + private static Optional negateComprehension( + CelMutableAst mutableAst, long compId, CelMutableComprehension comp, boolean isExists) { + CelMutableCall stepCall = comp.loopStep().call(); + CelMutableExpr predicate = getPredicateFromLoopStep(stepCall); + CelMutableExpr newLoopStep = + CelMutableExpr.ofCall( + comp.loopStep().id(), + CelMutableCall.create( + (isExists ? Operator.LOGICAL_AND : Operator.LOGICAL_OR).getFunction(), + CelMutableExpr.ofIdent(comp.accuVar()), + negate(predicate))); + CelMutableExpr newAccuInit = CelMutableExpr.ofConstant(CelConstant.ofValue(isExists)); + CelMutableExpr newLoopCondition = + CelMutableExpr.ofCall( + comp.loopCondition().id(), + CelMutableCall.create( + Operator.NOT_STRICTLY_FALSE.getFunction(), + isExists + ? CelMutableExpr.ofIdent(comp.accuVar()) + : negate(CelMutableExpr.ofIdent(comp.accuVar())))); + CelMutableComprehension newComp = + CelMutableComprehension.create( + comp.iterVar(), + comp.iterVar2(), + comp.iterRange(), + comp.accuVar(), + newAccuInit, + newLoopCondition, + newLoopStep, + comp.result()); + updateMacroCallForQuantifier( + mutableAst, compId, (isExists ? Operator.ALL : Operator.EXISTS).getFunction()); + return Optional.of(CelMutableExpr.ofComprehension(compId, newComp)); + } + + private static CelMutableExpr negate(CelMutableExpr expr) { + return CelMutableExpr.ofCall( + expr.id(), CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), expr)); + } + + private static void updateMacroCallForQuantifier( + CelMutableAst mutableAst, long compId, String newFunctionName) { + if (!mutableAst.source().getMacroCalls().containsKey(compId)) { + return; + } + CelMutableExpr macroCall = mutableAst.source().getMacroCalls().get(compId); + if (macroCall.getKind() != Kind.CALL) { + throw new IllegalStateException( + "Expected macro call to be of kind CALL, but got: " + macroCall.getKind()); + } + CelMutableCall call = macroCall.call(); + if (call.args().size() < 2) { + throw new IllegalStateException( + "Expected macro call to have at least 2 arguments, but got: " + call.args().size()); + } + CelMutableExpr predicateArg = Iterables.getLast(call.args()); + CelMutableExpr notPredicate; + if (isCallWithArgCount(predicateArg, Operator.LOGICAL_NOT.getFunction(), 1)) { + notPredicate = predicateArg.call().args().get(0); + } else { + notPredicate = + CelMutableExpr.ofCall( + 0, CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), predicateArg)); + } + List newArgs = new ArrayList<>(call.args()); + newArgs.set(newArgs.size() - 1, notPredicate); + CelMutableCall newCall = + call.target().isPresent() + ? CelMutableCall.create(call.target().get(), newFunctionName, newArgs) + : CelMutableCall.create(newFunctionName, newArgs); + mutableAst.source().addMacroCalls(compId, CelMutableExpr.ofCall(macroCall.id(), newCall)); + } + + private static List flattenNavigableOperands( + CelNavigableMutableExpr expr, String functionName) { + List result = new ArrayList<>(); + flattenNavigableOperandsRec(expr, functionName, result); + return result; + } + + private static void flattenNavigableOperandsRec( + CelNavigableMutableExpr expr, String functionName, List result) { + if (expr.getKind() == Kind.CALL + && expr.expr().call().function().equals(functionName) + && expr.expr().call().args().size() == 2) { + ImmutableList children = expr.children().collect(toImmutableList()); + if (children.size() == 2) { + flattenNavigableOperandsRec(children.get(0), functionName, result); + flattenNavigableOperandsRec(children.get(1), functionName, result); + return; + } + } + result.add(expr); + } + + private static CelMutableExpr getPredicateFromLoopStep(CelMutableCall stepCall) { + return stepCall.args().get(1); + } + + private static boolean isExistsMacro( + CelMutableAst mutableAst, long compId, CelMutableComprehension comp) { + return isStandardMacroCall(mutableAst, compId, Operator.EXISTS.getFunction()) + && isBooleanAccuInit(comp, false) + && isNotStrictlyFalseLoopCondition(comp, true) + && isLoopStepWithAccuVar(comp, Operator.LOGICAL_OR.getFunction()); + } + + private static boolean isAllMacro( + CelMutableAst mutableAst, long compId, CelMutableComprehension comp) { + return isStandardMacroCall(mutableAst, compId, Operator.ALL.getFunction()) + && isBooleanAccuInit(comp, true) + && isNotStrictlyFalseLoopCondition(comp, false) + && isLoopStepWithAccuVar(comp, Operator.LOGICAL_AND.getFunction()); + } + + private static boolean isStandardMacroCall( + CelMutableAst mutableAst, long compId, String expectedMacroFunction) { + if (!mutableAst.source().getMacroCalls().containsKey(compId)) { + return true; + } + CelMutableExpr macroCall = mutableAst.source().getMacroCalls().get(compId); + return macroCall.getKind() == Kind.CALL + && macroCall.call().function().equals(expectedMacroFunction); + } + + private static boolean isBooleanAccuInit(CelMutableComprehension comp, boolean expectedValue) { + return comp.accuInit().getKind() == Kind.CONSTANT + && comp.accuInit().constant().getKind() == CelConstant.Kind.BOOLEAN_VALUE + && comp.accuInit().constant().booleanValue() == expectedValue; + } + + private static boolean isNotStrictlyFalseLoopCondition( + CelMutableComprehension comp, boolean expectNot) { + if (comp.loopCondition().getKind() != Kind.CALL) { + throw new IllegalStateException( + "Expected comprehension loopCondition to be a CALL, but got: " + + comp.loopCondition().getKind()); + } + CelMutableCall call = comp.loopCondition().call(); + if (!call.function().equals(Operator.NOT_STRICTLY_FALSE.getFunction()) + && !call.function().equals(Operator.OLD_NOT_STRICTLY_FALSE.getFunction())) { + throw new IllegalStateException( + "Expected comprehension loopCondition to be @not_strictly_false, but got: " + + call.function()); + } + if (call.args().size() != 1) { + throw new IllegalStateException( + "Expected @not_strictly_false to have exactly 1 argument, but got: " + + call.args().size()); + } + CelMutableExpr arg = call.args().get(0); + if (expectNot) { + if (!isCallWithArgCount(arg, Operator.LOGICAL_NOT.getFunction(), 1)) { + return false; + } + arg = arg.call().args().get(0); + } + return isIdent(arg, comp.accuVar()); + } + + private static boolean isLoopStepWithAccuVar( + CelMutableComprehension comp, String expectedFunction) { + if (!isCallWithArgCount(comp.loopStep(), expectedFunction, 2)) { + return false; + } + List args = comp.loopStep().call().args(); + return isIdent(args.get(0), comp.accuVar()) || isIdent(args.get(1), comp.accuVar()); + } + + private static boolean isIdent(CelMutableExpr expr, String name) { + return expr.getKind() == Kind.IDENT && expr.ident().name().equals(name); + } + + private static boolean isCallWithArgCount( + CelMutableExpr expr, String functionName, int argCount) { + return expr.getKind() == Kind.CALL + && expr.call().function().equals(functionName) + && expr.call().args().size() == argCount; + } + + /** Options to configure how Canonicalization behaves. */ + @AutoValue + abstract static class CanonicalizationOptions { + abstract int maxIterationLimit(); + + /** Builder for configuring the {@link CanonicalizationOptions}. */ + @AutoValue.Builder + abstract static class Builder { + + /** + * Limit the number of iterations while performing canonicalization. An exception is thrown if + * the iteration count exceeds the set value. + */ + abstract Builder maxIterationLimit(int value); + + abstract CanonicalizationOptions build(); + + Builder() {} + } + + /** Returns a new options builder with recommended defaults pre-configured. */ + static Builder newBuilder() { + return new AutoValue_CanonicalizationOptimizer_CanonicalizationOptions.Builder() + .maxIterationLimit(500); + } + + CanonicalizationOptions() {} + } + + private CanonicalizationOptimizer(CanonicalizationOptions canonicalizationOptions) { + this.canonicalizationOptions = canonicalizationOptions; + } +} diff --git a/verifier/src/main/java/dev/cel/verifier/CelVerifierFactory.java b/verifier/src/main/java/dev/cel/verifier/CelVerifierFactory.java index da48ec484..d761428d6 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelVerifierFactory.java +++ b/verifier/src/main/java/dev/cel/verifier/CelVerifierFactory.java @@ -14,14 +14,43 @@ package dev.cel.verifier; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelFactory; +import dev.cel.checker.CelChecker; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.parser.CelParser; +import dev.cel.runtime.CelRuntime; /** Factory class for producing AST verifiers using Z3. */ public final class CelVerifierFactory { - /** Create a builder for configuring a {@link CelVerifier}. */ + /** + * Create a builder for configuring a {@link CelVerifier}. + * + * @deprecated Prefer passing a {@link Cel} environment using {@link #newVerifier(Cel)} to enable + * canonicalization and expression re-typechecking during verification. + */ + @Deprecated public static CelVerifierBuilder newVerifier() { return CelVerifierZ3Impl.newBuilder(); } + /** Create a builder for configuring a {@link CelVerifier} with a CEL environment. */ + public static CelVerifierBuilder newVerifier(Cel cel) { + return CelVerifierZ3Impl.newBuilder(cel); + } + + /** Create a builder for configuring a {@link CelVerifier} with a CEL environment. */ + public static CelVerifierBuilder newVerifier(CelCompiler celCompiler, CelRuntime celRuntime) { + return newVerifier(CelFactory.combine(celCompiler, celRuntime)); + } + + /** Create a builder for configuring a {@link CelVerifier} with a CEL environment. */ + public static CelVerifierBuilder newVerifier( + CelParser celParser, CelChecker celChecker, CelRuntime celRuntime) { + return newVerifier(CelCompilerFactory.combine(celParser, celChecker), celRuntime); + } + private CelVerifierFactory() {} } diff --git a/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java b/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java index 90d7238c2..04f3d3476 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java +++ b/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java @@ -27,9 +27,14 @@ import com.microsoft.z3.Params; import com.microsoft.z3.Solver; import com.microsoft.z3.Status; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypeProvider; +import dev.cel.optimizer.CelOptimizationException; +import dev.cel.optimizer.CelOptimizer; +import dev.cel.optimizer.CelOptimizerFactory; import dev.cel.verifier.axioms.CelZ3FunctionAxiom; import dev.cel.verifier.axioms.CelZ3StandardAxioms; import java.time.Duration; @@ -65,8 +70,15 @@ public Optional findType(String typeName) { private final CelZ3FunctionRegistry functionRegistry; private final CelTypeProvider typeProvider; + @SuppressWarnings("Immutable") // Cel environment is immutable, just not marked as such + private final Cel cel; + static Builder newBuilder() { - return new Builder(); + return new Builder(CelFactory.plannerCelBuilder().build()); + } + + static Builder newBuilder(Cel cel) { + return new Builder(Preconditions.checkNotNull(cel)); } static final class Builder implements CelVerifierBuilder { @@ -74,14 +86,16 @@ static final class Builder implements CelVerifierBuilder { private int comprehensionUnrollLimit; private final ImmutableSet.Builder unknownIdentifiers; private final ImmutableList.Builder functionAxioms; + private final Cel cel; private CelTypeProvider typeProvider; - private Builder() { + private Builder(Cel cel) { this.timeout = Duration.ofSeconds(10); this.comprehensionUnrollLimit = 5; this.unknownIdentifiers = ImmutableSet.builder(); this.functionAxioms = ImmutableList.builder(); this.typeProvider = EMPTY_TYPE_PROVIDER; + this.cel = cel; } @Override @@ -137,7 +151,12 @@ public CelVerifier build() { CelZ3FunctionRegistry registry = CelZ3FunctionRegistry.create(allFunctionAxioms); return new CelVerifierZ3Impl( - timeout, comprehensionUnrollLimit, unknownIdentifiers.build(), registry, typeProvider); + timeout, + comprehensionUnrollLimit, + unknownIdentifiers.build(), + registry, + typeProvider, + cel); } } @@ -160,6 +179,18 @@ public CelVerificationResult verifyEquivalence( CelAbstractSyntaxTree astA, CelAbstractSyntaxTree astB) throws CelVerificationException { Preconditions.checkArgument(astA.isChecked(), "astA must be type-checked."); Preconditions.checkArgument(astB.isChecked(), "astB must be type-checked."); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(cel) + .addAstOptimizers( + CanonicalizationOptimizer.newInstance( + CanonicalizationOptimizer.CanonicalizationOptions.newBuilder().build())) + .build(); + try { + astA = optimizer.optimize(astA); + astB = optimizer.optimize(astB); + } catch (CelOptimizationException e) { + // Fall back to original ASTs if canonicalization or re-typechecking fails + } try (Context ctx = new Context(ImmutableMap.of("model", "true"))) { CelAstToZ3Translator translator = new CelAstToZ3Translator( @@ -487,12 +518,14 @@ private static String getCounterexampleString( int comprehensionUnrollLimit, ImmutableSet unknownIdentifiers, CelZ3FunctionRegistry functionRegistry, - CelTypeProvider typeProvider) { + CelTypeProvider typeProvider, + Cel cel) { this.timeout = timeout; this.comprehensionUnrollLimit = comprehensionUnrollLimit; this.unknownIdentifiers = unknownIdentifiers; this.functionRegistry = functionRegistry; this.typeProvider = typeProvider; + this.cel = cel; } private enum SolverOutcome { diff --git a/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierToolCore.java b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierToolCore.java index 89e842fb1..51b7164e4 100644 --- a/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierToolCore.java +++ b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierToolCore.java @@ -50,7 +50,7 @@ static CelVerificationResult checkSatisfiable( throws Exception { CelCompiler compiler = buildCompiler(variables); CelAbstractSyntaxTree ast = compiler.compile(expression).getAst(); - CelVerifier verifier = buildVerifier(options); + CelVerifier verifier = buildVerifier(variables, options); return verifier.isSatisfiable(ast); } @@ -60,7 +60,7 @@ static CelVerificationResult checkValid( throws Exception { CelCompiler compiler = buildCompiler(variables); CelAbstractSyntaxTree ast = compiler.compile(expression).getAst(); - CelVerifier verifier = buildVerifier(options); + CelVerifier verifier = buildVerifier(variables, options); return verifier.isAlwaysTrue(ast); } @@ -74,7 +74,7 @@ static CelVerificationResult verifyEquivalence( CelCompiler compiler = buildCompiler(variables); CelAbstractSyntaxTree astA = compiler.compile(expressionA).getAst(); CelAbstractSyntaxTree astB = compiler.compile(expressionB).getAst(); - CelVerifier verifier = buildVerifier(options); + CelVerifier verifier = buildVerifier(variables, options); return verifier.verifyEquivalence(astA, astB); } @@ -125,20 +125,7 @@ static CelCompiler buildCompiler(Map variables) { return builder.build(); } - static CelVerifier buildVerifier(VerificationOptions options) { - CelVerifierBuilder builder = - CelVerifierFactory.newVerifier() - .setTimeout(options.getTimeout()) - .setComprehensionUnrollLimit(options.getComprehensionUnrollLimit()); - - for (String unknown : options.getUnknownIdentifiers()) { - builder.addUnknownIdentifier(unknown); - } - return builder.build(); - } - - private static CelPolicyVerifier buildPolicyVerifier( - Map variables, VerificationOptions options) { + static Cel buildCel(Map variables) { CelBuilder celBuilder = CelFactory.plannerCelBuilder() .setStandardMacros(CelStandardMacro.STANDARD_MACROS) @@ -151,10 +138,27 @@ private static CelPolicyVerifier buildPolicyVerifier( for (Map.Entry entry : variables.entrySet()) { celBuilder.addVar(entry.getKey(), entry.getValue()); } - Cel celBundle = celBuilder.build(); + return celBuilder.build(); + } + + static CelVerifier buildVerifier(Map variables, VerificationOptions options) { + CelVerifierBuilder builder = + CelVerifierFactory.newVerifier(buildCel(variables)) + .setTimeout(options.getTimeout()) + .setComprehensionUnrollLimit(options.getComprehensionUnrollLimit()); + + for (String unknown : options.getUnknownIdentifiers()) { + builder.addUnknownIdentifier(unknown); + } + return builder.build(); + } + + private static CelPolicyVerifier buildPolicyVerifier( + Map variables, VerificationOptions options) { + Cel celBundle = buildCel(variables); CelPolicyCompiler policyCompiler = CelPolicyCompilerFactory.newPolicyCompiler(celBundle).build(); - CelVerifier astVerifier = buildVerifier(options); + CelVerifier astVerifier = buildVerifier(variables, options); return CelPolicyVerifierFactory.newVerifier(policyCompiler, astVerifier).build(); } diff --git a/verifier/src/test/java/dev/cel/verifier/BUILD.bazel b/verifier/src/test/java/dev/cel/verifier/BUILD.bazel index f1669c486..55b9c24be 100644 --- a/verifier/src/test/java/dev/cel/verifier/BUILD.bazel +++ b/verifier/src/test/java/dev/cel/verifier/BUILD.bazel @@ -20,9 +20,11 @@ java_library( "//common:cel_ast", "//common:compiler_common", "//common:container", + "//common:mutable_ast", "//common:operator", "//common:options", "//common/ast", + "//common/ast:mutable_expr", "//common/types", "//common/types:message_type_provider", "//compiler:compiler_builder", @@ -47,6 +49,7 @@ java_library( "//:java_truth", "@maven//:tools_aqua_z3_turnkey", "//verifier", + "//verifier:canonicalization_optimizer", "//verifier:numeric_bounds", "//verifier:policy_verifier", "//verifier:policy_verifier_factory", diff --git a/verifier/src/test/java/dev/cel/verifier/CanonicalizationOptimizerTest.java b/verifier/src/test/java/dev/cel/verifier/CanonicalizationOptimizerTest.java new file mode 100644 index 000000000..ead53d46f --- /dev/null +++ b/verifier/src/test/java/dev/cel/verifier/CanonicalizationOptimizerTest.java @@ -0,0 +1,566 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.verifier; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelFactory; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelContainer; +import dev.cel.common.CelMutableAst; +import dev.cel.common.CelOptions; +import dev.cel.common.ast.CelMutableExpr; +import dev.cel.common.ast.CelMutableExpr.CelMutableCall; +import dev.cel.common.types.ListType; +import dev.cel.common.types.MapType; +import dev.cel.common.types.OptionalType; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.extensions.CelExtensions; +import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.optimizer.CelOptimizer; +import dev.cel.optimizer.CelOptimizerFactory; +import dev.cel.parser.CelStandardMacro; +import dev.cel.parser.CelUnparser; +import dev.cel.parser.CelUnparserFactory; +import dev.cel.verifier.CanonicalizationOptimizer.CanonicalizationOptions; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class CanonicalizationOptimizerTest { + + private static final Cel CEL = + CelFactory.plannerCelBuilder() + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions( + CelOptions.current() + .populateMacroCalls(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .addMessageTypes(TestAllTypes.getDescriptor()) + .addCompilerLibraries( + CelExtensions.comprehensions(), CelExtensions.bindings(), CelOptionalLibrary.INSTANCE) + .addRuntimeLibraries(CelExtensions.comprehensions(), CelOptionalLibrary.INSTANCE) + // Abstract DYN variables for alphabetical ordering and precedence tests + .addVar("dyn_a", SimpleType.DYN) + .addVar("dyn_b", SimpleType.DYN) + .addVar("dyn_c", SimpleType.DYN) + .addVar("dyn_d", SimpleType.DYN) + // Explicit Primitive typed variables + .addVar("bool_var", SimpleType.BOOL) + .addVar("bool_var2", SimpleType.BOOL) + .addVar("int_var", SimpleType.INT) + .addVar("int_var2", SimpleType.INT) + .addVar("uint_var", SimpleType.UINT) + .addVar("uint_var2", SimpleType.UINT) + .addVar("double_var", SimpleType.DOUBLE) + .addVar("double_var2", SimpleType.DOUBLE) + .addVar("string_var", SimpleType.STRING) + .addVar("string_var2", SimpleType.STRING) + .addVar("bytes_var", SimpleType.BYTES) + .addVar("bytes_var2", SimpleType.BYTES) + .addVar("duration_var", SimpleType.DURATION) + .addVar("timestamp_var", SimpleType.TIMESTAMP) + .addVar("null_var", SimpleType.NULL_TYPE) + // Collection variables + .addVar("int_list", ListType.create(SimpleType.INT)) + .addVar("string_list", ListType.create(SimpleType.STRING)) + .addVar("bool_list", ListType.create(SimpleType.BOOL)) + .addVar("nested_list", ListType.create(ListType.create(SimpleType.INT))) + .addVar("opt_list", ListType.create(OptionalType.create(SimpleType.INT))) + .addVar("string_int_map", MapType.create(SimpleType.STRING, SimpleType.INT)) + .addVar("int_string_map", MapType.create(SimpleType.INT, SimpleType.STRING)) + .addVar( + "nested_map", + MapType.create(SimpleType.STRING, MapType.create(SimpleType.STRING, SimpleType.INT))) + .addVar("list_map", ListType.create(MapType.create(SimpleType.STRING, SimpleType.INT))) + .addVar("int_list_map", MapType.create(SimpleType.INT, ListType.create(SimpleType.INT))) + // Struct / proto message variables + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .addVar("msg2", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .build(); + + private static final CelOptimizer OPTIMIZER = + CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + .addAstOptimizers( + CanonicalizationOptimizer.newInstance(CanonicalizationOptions.newBuilder().build())) + .build(); + + private static final CelUnparser UNPARSER = CelUnparserFactory.newUnparser(); + + private enum CanonicalizationTestCase { + // Commutative Logical Operators (&&, ||) across Simple Types + COMMUTATIVE_AND_BOOL( + "bool_var == true && bool_var2 == false", "bool_var == true && bool_var2 == false"), + COMMUTATIVE_AND_INT("int_var == 2 && int_var2 == 1", "int_var == 2 && int_var2 == 1"), + COMMUTATIVE_AND_UINT( + "uint_var == 20u && uint_var2 == 10u", "uint_var == 20u && uint_var2 == 10u"), + COMMUTATIVE_AND_DOUBLE( + "double_var == 3.14 && double_var2 == 1.41", "double_var == 3.14 && double_var2 == 1.41"), + COMMUTATIVE_AND_STRING( + "string_var == 'foo' && string_var2 == 'bar'", + "string_var == \"foo\" && string_var2 == \"bar\""), + COMMUTATIVE_AND_BYTES( + "bytes_var == b'foo' && bytes_var2 == b'bar'", + "bytes_var == b\"\\146\\157\\157\" && bytes_var2 == b\"\\142\\141\\162\""), + COMMUTATIVE_AND_NULL("null_var == null && dyn_a == null", "dyn_a == null && null_var == null"), + COMMUTATIVE_AND_MULTI_OPERAND( + "string_var == 'c' && string_var == 'a' && string_var == 'b'", + "string_var == \"a\" && string_var == \"b\" && string_var == \"c\""), + COMMUTATIVE_OR_MULTI_OPERAND( + "int_var == 30 || int_var == 10 || int_var == 20", + "int_var == 10 || int_var == 20 || int_var == 30"), + COMMUTATIVE_AND_DEDUPLICATION("int_var == 1 && int_var == 1", "int_var == 1"), + COMMUTATIVE_OR_DEDUPLICATION("string_var == 'a' || string_var == 'a'", "string_var == \"a\""), + COMMUTATIVE_AND_MIXED_TYPES( + "string_var == 'foo' && int_var == 1", "int_var == 1 && string_var == \"foo\""), + COMMUTATIVE_OR_MIXED_TYPES( + "bool_var == true || int_var == 1", "bool_var == true || int_var == 1"), + LIST_DIFFERENT_SIZES_EQUALITY("[1, 2] == [1]", "[1] == [1, 2]"), + ONE_ARG_CALL_WITH_LOGICAL_OPERANDS( + "type(bool_var == true && bool_var2 == false)", + "type(bool_var == true && bool_var2 == false)"), + COMMUTATIVE_AND_DURATION_TIMESTAMP( + "timestamp_var == timestamp('2026-01-01T00:00:00Z') && duration_var == duration('10s')", + "duration_var == duration(\"10s\") && timestamp_var ==" + + " timestamp(\"2026-01-01T00:00:00Z\")"), + COMMUTATIVE_AND_NESTED_LOGIC( + "(dyn_b || dyn_a) && (dyn_d || dyn_c)", "(dyn_a || dyn_b) && (dyn_c || dyn_d)"), + + // Symmetric Equality (==) and Inequality (!=) across Types + SYMMETRIC_EQUALS_BOOL("true == bool_var", "bool_var == true"), + SYMMETRIC_NOT_EQUALS_BOOL("false != bool_var", "bool_var != false"), + SYMMETRIC_EQUALS_INT("42 == int_var", "int_var == 42"), + SYMMETRIC_NOT_EQUALS_INT("0 != int_var", "int_var != 0"), + SYMMETRIC_EQUALS_UINT("100u == uint_var", "uint_var == 100u"), + SYMMETRIC_NOT_EQUALS_UINT("0u != uint_var", "uint_var != 0u"), + SYMMETRIC_EQUALS_DOUBLE("3.14159 == double_var", "double_var == 3.14159"), + SYMMETRIC_NOT_EQUALS_DOUBLE("0.0 != double_var", "double_var != 0.0"), + SYMMETRIC_EQUALS_STRING("'hello' == string_var", "string_var == \"hello\""), + SYMMETRIC_NOT_EQUALS_STRING("'' != string_var", "string_var != \"\""), + SYMMETRIC_EQUALS_BYTES("b'abc' == bytes_var", "bytes_var == b\"\\141\\142\\143\""), + SYMMETRIC_NOT_EQUALS_BYTES("b'' != bytes_var", "bytes_var != b\"\""), + SYMMETRIC_EQUALS_IDENT_ORDERING("dyn_c == dyn_a", "dyn_a == dyn_c"), + SYMMETRIC_EQUALS_CALL_VS_IDENT("size(int_list) == int_var", "int_var == size(int_list)"), + SYMMETRIC_EQUALS_SELECT_VS_IDENT("msg.single_int64 == dyn_a", "dyn_a == msg.single_int64"), + SYMMETRIC_EQUALS_GLOBAL_VS_MEMBER_CALL( + "int_list.size() == size(int_list)", "size(int_list) == int_list.size()"), + COMMUTATIVE_AND_GLOBAL_VS_MEMBER_CALL( + "int_list.size() == 1 && size(int_list) == 1", + "size(int_list) == 1 && int_list.size() == 1"), + + // De Morgan Transformations on Logical NOT (!) + DE_MORGAN_DOUBLE_NEGATION("!!bool_var", "bool_var"), + DE_MORGAN_QUADRUPLE_NEGATION("!!!!(int_var == 1)", "int_var == 1"), + DE_MORGAN_AND_TYPED( + "!(int_var == 1 && string_var == 'foo')", "int_var != 1 || string_var != \"foo\""), + DE_MORGAN_OR_TYPED( + "!(int_var == 1 || string_var == 'foo')", "int_var != 1 && string_var != \"foo\""), + DE_MORGAN_EQUALS_TYPED("!(int_var == 1)", "int_var != 1"), + DE_MORGAN_NOT_EQUALS_TYPED("!(int_var != 1)", "int_var == 1"), + DE_MORGAN_NESTED_AND_OR( + "!((dyn_a && dyn_b) || (dyn_c && dyn_d))", "(!dyn_a || !dyn_b) && (!dyn_c || !dyn_d)"), + DE_MORGAN_NESTED_OR_AND( + "!((dyn_a || dyn_b) && (dyn_c || dyn_d))", "!dyn_a && !dyn_b || !dyn_c && !dyn_d"), + DE_MORGAN_MIXED_TYPES( + "!(bool_var == true && double_var == 1.0)", "bool_var != true || double_var != 1.0"), + DE_MORGAN_ALL_NEGATED_PREDICATE("!int_list.all(e, !(e == 1))", "e == 1"), + DE_MORGAN_EXISTS_NEGATED_PREDICATE("!int_list.exists(e, !(e == 1))", "e == 1"), + DE_MORGAN_ALL_NEGATED_VAR_PREDICATE( + "!int_list.all(e, !bool_var)", "int_list.exists(e, bool_var)"), + DE_MORGAN_EXISTS_NEGATED_VAR_PREDICATE( + "!int_list.exists(e, !bool_var)", "int_list.all(e, bool_var)"), + DE_MORGAN_RELATIONAL_UNCHANGED("!(int_var > 5)", "!(int_var > 5)"), + DE_MORGAN_EXISTS_TYPED("!int_list.exists(e, e == 1)", "e != 1"), + DE_MORGAN_ALL_TYPED("!int_list.all(e, e == 1)", "e != 1"), + DE_MORGAN_EXISTS_COMPLEX_PREDICATE( + "!int_list.exists(e, !(e == 1 && e == 2))", "e == 1 && e == 2"), + DE_MORGAN_ALL_COMPLEX_PREDICATE("!int_list.all(e, !(e == 1 || e == 2))", "e == 1 || e == 2"), + DE_MORGAN_BOOL_VARIABLES("!(bool_var && bool_var2)", "!bool_var || !bool_var2"), + + // Extension Coverage - Optionals & Optional Indexing/Fields + OPTIONAL_OF_EQUALITY_SYMMETRY( + "optional.of(dyn_b) == optional.of(dyn_a)", "optional.of(dyn_a) == optional.of(dyn_b)"), + OPTIONAL_NONE_EQUALITY_SYMMETRY( + "optional.of(dyn_a) == optional.none()", "optional.none() == optional.of(dyn_a)"), + OPTIONAL_OF_NON_ZERO_VALUE_SYMMETRY( + "optional.ofNonZeroValue(dyn_b) == optional.ofNonZeroValue(dyn_a)", + "optional.ofNonZeroValue(dyn_a) == optional.ofNonZeroValue(dyn_b)"), + OPTIONAL_FIELD_SELECT_EQUALITY( + "msg.?single_int64 == optional.of(1)", "msg.?single_int64 == optional.of(1)"), + OPTIONAL_FIELD_SELECT_INEQUALITY( + "msg.?single_string != optional.none()", "msg.?single_string != optional.none()"), + OPTIONAL_FIELD_SELECT_OR_VALUE_EQUALITY( + "msg.?single_int64.orValue(0) == int_var", "int_var == msg.?single_int64.orValue(0)"), + OPTIONAL_LIST_ELEMENT_EQUALITY( + "[?optional.of(1)] == [?optional.of(int_var)]", + "[?optional.of(int_var)] == [?optional.of(1)]"), + OPTIONAL_MAP_ENTRY_EQUALITY( + "{?'key': optional.of(1)} == {?'key': optional.of(int_var)}", + "{?\"key\": optional.of(int_var)} == {?\"key\": optional.of(1)}"), + DE_MORGAN_OPTIONAL_EQUALITY( + "!(optional.of(dyn_a) == optional.of(dyn_b))", "optional.of(dyn_a) != optional.of(dyn_b)"), + DE_MORGAN_OPTIONAL_INEQUALITY( + "!(optional.of(dyn_a) != optional.none())", "optional.none() == optional.of(dyn_a)"), + COMMUTATIVE_AND_OPTIONAL_HAS_VALUE( + "optional.of(dyn_b).hasValue() && optional.of(dyn_a).hasValue()", + "optional.of(dyn_a).hasValue() && optional.of(dyn_b).hasValue()"), + COMMUTATIVE_OR_OPTIONAL_HAS_VALUE( + "optional.of(dyn_b).hasValue() || optional.of(dyn_a).hasValue()", + "optional.of(dyn_a).hasValue() || optional.of(dyn_b).hasValue()"), + COMMUTATIVE_AND_OPTIONAL_FIELD_SELECT( + "msg.?single_string.hasValue() && msg.?single_int64.hasValue()", + "msg.?single_int64.hasValue() && msg.?single_string.hasValue()"), + COMMUTATIVE_OR_OPTIONAL_FIELD_SELECT( + "msg.?single_string.hasValue() || msg.?single_int64.hasValue()", + "msg.?single_int64.hasValue() || msg.?single_string.hasValue()"), + DE_MORGAN_OPTIONAL_HAS_VALUE_AND( + "!(optional.of(dyn_a).hasValue() && optional.of(dyn_b).hasValue())", + "!optional.of(dyn_a).hasValue() || !optional.of(dyn_b).hasValue()"), + DE_MORGAN_OPTIONAL_HAS_VALUE_OR( + "!(optional.of(dyn_a).hasValue() || optional.of(dyn_b).hasValue())", + "!optional.of(dyn_a).hasValue() && !optional.of(dyn_b).hasValue()"), + OPTIONAL_IN_EXISTS_COMPREHENSION( + "!opt_list.exists(x, !(x.hasValue() && x.value() == 1))", "x.value() == 1 && x.hasValue()"), + OPTIONAL_IN_ALL_COMPREHENSION( + "!opt_list.all(x, !(x.hasValue() || x.value() == 1))", "x.value() == 1 || x.hasValue()"), + OPTIONAL_FIELD_CHAINING_EQUALITY( + "msg.?single_nested_message.?bb == optional.of(42)", + "msg.?single_nested_message.?bb == optional.of(42)"), + OPTIONAL_MAP_INDEXING_EQUALITY( + "string_int_map.?foo == optional.of(1)", "string_int_map.?foo == optional.of(1)"), + + // Extension Coverage - Two-Variable Comprehensions + DE_MORGAN_2VAR_EXISTS_MAP( + "!string_int_map.exists(k, v, k == 'foo' && v == 1)", "k != \"foo\" || v != 1"), + DE_MORGAN_2VAR_ALL_MAP( + "!string_int_map.all(k, v, !(k == 'foo' || v == 1))", "k == \"foo\" || v == 1"), + DE_MORGAN_2VAR_EXISTS_NEGATED_PREDICATE( + "!string_int_map.exists(k, v, !(v > 0 && k == 'foo'))", "k == \"foo\" && v > 0"), + DE_MORGAN_2VAR_ALL_NEGATED_PREDICATE( + "!string_int_map.all(k, v, !(v > 0 || k == 'foo'))", "k == \"foo\" || v > 0"), + TWO_VAR_EXISTS_COMMUTATIVE_AND( + "string_int_map.exists(k, v, v == 1 && k == 'foo')", + "string_int_map.exists(k, v, k == \"foo\" && v == 1)"), + TWO_VAR_ALL_COMMUTATIVE_OR( + "string_int_map.all(k, v, v == 1 || k == 'foo')", + "string_int_map.all(k, v, k == \"foo\" || v == 1)"), + TWO_VAR_EXISTS_SYMMETRIC_EQUALITY( + "string_int_map.exists(k, v, v == 1)", "string_int_map.exists(k, v, v == 1)"), + TWO_VAR_ALL_SYMMETRIC_INEQUALITY( + "string_int_map.all(k, v, v != 0)", "string_int_map.all(k, v, v != 0)"), + TWO_VAR_EXISTS_INT_STRING_MAP( + "int_string_map.exists(k, v, v == 'bar' && k == 1)", + "int_string_map.exists(k, v, k == 1 && v == \"bar\")"), + TWO_VAR_ALL_INT_STRING_MAP( + "!int_string_map.all(k, v, k == 1 || v == 'bar')", "k != 1 && v != \"bar\""), + TWO_VAR_EXISTS_LIST_INDEX_VALUE( + "!int_list.exists(i, v, i == 0 && v == 100)", "i != 0 || v != 100"), + TWO_VAR_ALL_LIST_INDEX_VALUE( + "!int_list.all(i, v, !(i == 0 || v == 100))", "i == 0 || v == 100"), + TWO_VAR_EXISTS_LIST_COMMUTATIVE_AND( + "int_list.exists(i, v, v == 100 && i == 0)", "int_list.exists(i, v, i == 0 && v == 100)"), + TWO_VAR_ALL_LIST_COMMUTATIVE_OR( + "int_list.all(i, v, v == 100 || i == 0)", "int_list.all(i, v, i == 0 || v == 100)"), + TWO_VAR_NESTED_COMPREHENSIONS( + "string_int_map.exists(k, v, k == 'foo' && int_list.all(i, e, e == v && i == 0))", + "string_int_map.exists(k, v, k == \"foo\" && int_list.all(i, e, e == v && i == 0))"), + DE_MORGAN_2VAR_NESTED_COMPREHENSIONS( + "string_int_map.exists(k, v, k == 'foo' && !int_list.exists(i, e, e == v))", + "string_int_map.exists(k, v, k == \"foo\" && e != v)"), + TWO_VAR_COMPREHENSION_WITH_OPTIONALS( + "!string_int_map.exists(k, v, optional.of(v).hasValue() && k == 'foo')", + "!optional.of(v).hasValue() || k != \"foo\""), + TWO_VAR_COMPREHENSION_STRUCT_FIELDS( + "!string_int_map.exists(k, v, !(k == msg.single_string && v == msg.single_int64))", + "k == msg.single_string && v == msg.single_int64"), + TWO_VAR_COMPREHENSION_DEDUPLICATION( + "string_int_map.exists(k, v, k == 'foo' && k == 'foo')", + "string_int_map.exists(k, v, k == \"foo\")"), + TWO_VAR_COMPREHENSION_DE_MORGAN_INEQUALITY( + "!string_int_map.exists(k, v, !(k != 'foo' && v != 1))", "k != \"foo\" && v != 1"), + + // Extension Coverage - cel.bind Macro + CEL_BIND_COMMUTATIVE_AND( + "cel.bind(x, int_var + 10, 1 == x && 2 == int_var2)", + "cel.bind(x, int_var + 10, int_var2 == 2 && x == 1)"), + CEL_BIND_COMMUTATIVE_OR( + "cel.bind(x, int_var + 10, 1 == x || 2 == int_var2)", + "cel.bind(x, int_var + 10, int_var2 == 2 || x == 1)"), + CEL_BIND_SYMMETRIC_EQUALITY( + "cel.bind(x, int_var + 10, 20 == x)", "cel.bind(x, int_var + 10, x == 20)"), + CEL_BIND_NESTED( + "cel.bind(x, int_var + 10, cel.bind(y, int_var2 + 20, 2 == y && 1 == x))", + "cel.bind(x, int_var + 10, cel.bind(y, int_var2 + 20, x == 1 && y == 2))"), + CEL_BIND_DE_MORGAN( + "cel.bind(x, int_var == 1, !(2 == int_var2 && x == true))", + "cel.bind(x, int_var == 1, int_var2 != 2 || x != true)"), + + // Nested Lists, Maps, and Structs + NESTED_LIST_EQUALITY_SYMMETRY( + "[[2, 1], [4, 3]] == [[1, 2], [3, 4]]", "[[1, 2], [3, 4]] == [[2, 1], [4, 3]]"), + NESTED_LIST_INEQUALITY_SYMMETRY("[[2, 1]] != [[1, 2]]", "[[1, 2]] != [[2, 1]]"), + LIST_ELEMENT_ORDERING_EQUALITY("int_list == [3, 2, 1]", "int_list == [3, 2, 1]"), + MAP_EQUALITY_ORDERING( + "string_int_map == {'b': 2, 'a': 1}", "string_int_map == {\"b\": 2, \"a\": 1}"), + MAP_DIFFERENT_SIZES_EQUALITY( + "{'b': 2, 'a': 1} == {'a': 1}", "{\"a\": 1} == {\"b\": 2, \"a\": 1}"), + COMMUTATIVE_AND_MAP_DIFFERENT_SIZES( + "string_int_map == {'a': 1, 'b': 2} && string_int_map == {'a': 1}", + "string_int_map == {\"a\": 1} && string_int_map == {\"a\": 1, \"b\": 2}"), + NESTED_MAP_EQUALITY( + "nested_map == {'b': {'d': 4, 'c': 3}, 'a': {'y': 2, 'x': 1}}", + "nested_map == {\"b\": {\"d\": 4, \"c\": 3}, \"a\": {\"y\": 2, \"x\": 1}}"), + LIST_OF_MAPS_EQUALITY( + "list_map == [{'y': 2, 'x': 1}, {'d': 4, 'c': 3}]", + "list_map == [{\"y\": 2, \"x\": 1}, {\"d\": 4, \"c\": 3}]"), + MAP_OF_LISTS_EQUALITY( + "int_list_map == {1: [2, 1], 2: [4, 3]}", "int_list_map == {1: [2, 1], 2: [4, 3]}"), + STRUCT_EQUALITY_ORDERING( + "msg == TestAllTypes{single_int64: 10, single_string: 'foo'}", + "msg == cel.expr.conformance.proto3.TestAllTypes{single_int64: 10, single_string:" + + " \"foo\"}"), + NESTED_STRUCT_EQUALITY( + "msg == TestAllTypes{single_nested_message: TestAllTypes.NestedMessage{bb: 42}}", + "msg == cel.expr.conformance.proto3.TestAllTypes{single_nested_message:" + + " cel.expr.conformance.proto3.TestAllTypes.NestedMessage{bb: 42}}"), + STRUCT_INEQUALITY( + "msg != TestAllTypes{single_int64: 0}", + "msg != cel.expr.conformance.proto3.TestAllTypes{single_int64: 0}"), + STRUCT_DIFFERENT_ENTRY_COUNTS_ORDERING( + "TestAllTypes{single_int64: 10, single_string: 'foo'} == TestAllTypes{single_int64: 10}", + "cel.expr.conformance.proto3.TestAllTypes{single_int64: 10} ==" + + " cel.expr.conformance.proto3.TestAllTypes{single_int64: 10, single_string:" + + " \"foo\"}"), + STRUCT_DIFFERENT_FIELD_VALUES_ORDERING( + "TestAllTypes{single_int64: 20} == TestAllTypes{single_int64: 10}", + "cel.expr.conformance.proto3.TestAllTypes{single_int64: 10} ==" + + " cel.expr.conformance.proto3.TestAllTypes{single_int64: 20}"), + DE_MORGAN_STRUCT_EQUALITY( + "!(msg == TestAllTypes{single_int64: 10})", + "msg != cel.expr.conformance.proto3.TestAllTypes{single_int64: 10}"), + DE_MORGAN_STRUCT_INEQUALITY( + "!(msg != TestAllTypes{single_int64: 10})", + "msg == cel.expr.conformance.proto3.TestAllTypes{single_int64: 10}"), + COMMUTATIVE_AND_STRUCT_FIELDS( + "msg.single_string == 'foo' && msg.single_int64 == 10", + "msg.single_int64 == 10 && msg.single_string == \"foo\""), + COMMUTATIVE_OR_STRUCT_FIELDS( + "msg.single_int64 == 20 || msg.single_int64 == 10", + "msg.single_int64 == 10 || msg.single_int64 == 20"), + DE_MORGAN_STRUCT_FIELDS_AND( + "!(msg.single_int64 == 10 && msg.single_string == 'foo')", + "msg.single_int64 != 10 || msg.single_string != \"foo\""), + DE_MORGAN_STRUCT_FIELDS_OR( + "!(msg.single_int64 == 10 || msg.single_int64 == 20)", + "msg.single_int64 != 10 && msg.single_int64 != 20"), + STRUCT_SELECT_ORDERING("msg.single_int64 == int_var", "int_var == msg.single_int64"), + NESTED_STRUCT_SELECT_ORDERING( + "msg.single_nested_message.bb == int_var", "int_var == msg.single_nested_message.bb"), + MAP_LOOKUP_IN_LOGICAL_EXPR( + "string_int_map['foo'] == 1 && string_int_map['bar'] == 2", + "string_int_map[\"bar\"] == 2 && string_int_map[\"foo\"] == 1"), + LIST_INDEX_IN_LOGICAL_EXPR( + "int_list[1] == 20 && int_list[0] == 10", "int_list[0] == 10 && int_list[1] == 20"), + COMPREHENSIONS_IN_LIST_LITERALS( + "[int_list.exists(e, e == 2), int_list.exists(e, e == 1)] == [true, false]", + "[int_list.exists(e, e == 2), int_list.exists(e, e == 1)] == [true, false]"), + COMPREHENSIONS_IN_MAP_LITERALS( + "{'b': int_list.all(e, e > 0), 'a': int_list.exists(e, e == 1)} == {'a': true, 'b': false}", + "{\"a\": true, \"b\": false} == {\"b\": int_list.all(e, e > 0), \"a\": int_list.exists(e, e" + + " == 1)}"), + COMPREHENSIONS_IN_STRUCT_FIELDS( + "TestAllTypes{single_int64: int_list[0]} == msg", + "msg == cel.expr.conformance.proto3.TestAllTypes{single_int64: int_list[0]}"), + NESTED_COMPREHENSIONS_IN_STRUCT_FIELDS( + "!int_list.exists(x, TestAllTypes{single_int64: x} == msg)", + "msg != cel.expr.conformance.proto3.TestAllTypes{single_int64: x}"), + DE_MORGAN_COLLECTION_LITERAL_EQUALITY("!([2, 1] == [1, 2])", "[1, 2] != [2, 1]"), + + // Cross-Type & Heterogeneous Comparisons + HETEROGENEOUS_INT_UINT_AND("int_var == 1 && uint_var == 1u", "int_var == 1 && uint_var == 1u"), + HETEROGENEOUS_INT_DOUBLE_OR( + "int_var == 1 || double_var == 1.0", "double_var == 1.0 || int_var == 1"), + HETEROGENEOUS_UINT_DOUBLE_EQUALITY( + "uint_var == 10u && double_var == 10.0", "double_var == 10.0 && uint_var == 10u"), + CROSS_TYPE_DURATION_TIMESTAMP_AND( + "timestamp_var == timestamp('2026-01-01T00:00:00Z') && duration_var == duration('10s')", + "duration_var == duration(\"10s\") && timestamp_var ==" + + " timestamp(\"2026-01-01T00:00:00Z\")"), + CROSS_TYPE_STRING_BYTES_OR( + "string_var == 'foo' || bytes_var == b'foo'", + "bytes_var == b\"\\146\\157\\157\" || string_var == \"foo\""), + NULL_VS_PRIMITIVE_EQUALITY("dyn_a == null", "dyn_a == null"), + NULL_VS_MESSAGE_EQUALITY("msg == null", "msg == null"), + NULL_VS_OPTIONAL_EQUALITY("optional.of(int_var) == null", "optional.of(int_var) == null"), + CROSS_TYPE_COMMUTATIVE_CHAIN( + "string_var == 'a' && double_var == 1.0 && int_var == 1 && bool_var == true", + "bool_var == true && double_var == 1.0 && int_var == 1 && string_var == \"a\""), + DE_MORGAN_CROSS_TYPE_CHAIN( + "!(string_var == 'a' && double_var == 1.0 && int_var == 1)", + "double_var != 1.0 || int_var != 1 || string_var != \"a\""), + HETEROGENEOUS_NUMERIC_DE_MORGAN( + "!(int_var != 1 || uint_var != 1u || double_var != 1.0)", + "double_var == 1.0 && int_var == 1 && uint_var == 1u"), + CROSS_TYPE_IN_2VAR_COMPREHENSION( + "!string_int_map.exists(k, v, !(int_var == 1 && double_var == 1.0))", + "double_var == 1.0 && int_var == 1"), + CROSS_TYPE_IN_LIST_COMPREHENSION( + "!int_list.exists(e, !(uint_var == 1u || double_var == 1.0))", + "double_var == 1.0 || uint_var == 1u"), + MIXED_SELECT_AND_CALLS_ACROSS_TYPES( + "msg.single_int64 == size(int_list) && msg.single_string == string(int_var)", + "msg.single_int64 == size(int_list) && msg.single_string == string(int_var)"), + DE_MORGAN_MIXED_SELECT_AND_CALLS( + "!(msg.single_int64 == size(int_list) && msg.single_string == 'foo')", + "msg.single_int64 != size(int_list) || msg.single_string != \"foo\""), + + // Edge Cases, Invariants, Non-Canonicalizable Expressions, and Precedence + RELATIONAL_OPERATORS_UNCHANGED("int_var < 10 && int_var > 5", "int_var < 10 && int_var > 5"), + DE_MORGAN_RELATIONAL_OPERATORS("!(int_var < 10)", "!(int_var < 10)"), + TERNARY_OPERATOR_UNCHANGED_COND( + "bool_var ? int_var == 1 : int_var == 2", "bool_var ? (int_var == 1) : (int_var == 2)"), + DE_MORGAN_TERNARY_OPERATOR( + "!(bool_var ? int_var == 1 : int_var == 2)", + "!(bool_var ? (int_var == 1) : (int_var == 2))"), + IN_OPERATOR_WITH_COMMUTATIVE_AND( + "int_var in int_list && bool_var == true", "int_var in int_list && bool_var == true"), + DE_MORGAN_IN_OPERATOR("!(int_var in int_list)", "!(int_var in int_list)"), + COMPREHENSION_ACCU_VAR_NOT_REORDERED( + "int_list.exists(e, e == 1 && e == 2)", "int_list.exists(e, e == 1 && e == 2)"), + COMPLEX_NESTED_DE_MORGAN_PRECEDENCE( + "!(dyn_a && dyn_b || dyn_c && dyn_d)", "(!dyn_a || !dyn_b) && (!dyn_c || !dyn_d)"), + COMPLEX_NESTED_DE_MORGAN_OR_AND( + "!((dyn_a || dyn_b) && (dyn_c || dyn_d))", "!dyn_a && !dyn_b || !dyn_c && !dyn_d"), + TRIPLE_AND_DEDUPLICATION("int_var == 1 && int_var == 1 && int_var == 1", "int_var == 1"), + TRIPLE_OR_DEDUPLICATION( + "string_var == 'x' || string_var == 'x' || string_var == 'x'", "string_var == \"x\""), + EMPTY_STRING_ZERO_CONSTANT_COMPARISONS( + "string_var == '' && int_var == 0 && bool_var == false", + "bool_var == false && int_var == 0 && string_var == \"\""), + IDENT_COMPARISON_SYMMETRY( + "dyn_b == dyn_a && dyn_d == dyn_c", "dyn_a == dyn_b && dyn_c == dyn_d"), + IDENT_INEQUALITY_SYMMETRY( + "dyn_b != dyn_a || dyn_d != dyn_c", "dyn_a != dyn_b || dyn_c != dyn_d"), + IDENT_SAME_NAME_DIFFERENT_OPERATORS( + "dyn_a != dyn_b && dyn_a == dyn_b", "dyn_a != dyn_b && dyn_a == dyn_b"); + + private final String input; + private final String expected; + + CanonicalizationTestCase(String input, String expected) { + this.input = input; + this.expected = expected; + } + } + + @Test + public void optimize_success(@TestParameter CanonicalizationTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = CEL.compile(testCase.input).getAst(); + CelAbstractSyntaxTree optimizedAst = OPTIMIZER.optimize(ast); + + String unparsed = UNPARSER.unparse(optimizedAst); + assertThat(unparsed).isEqualTo(testCase.expected); + } + + @Test + public void optimize_maxIterationLimitReached_throwsException() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("dyn_b == dyn_a && dyn_d == dyn_c").getAst(); + CanonicalizationOptimizer optimizer = + CanonicalizationOptimizer.newInstance( + CanonicalizationOptions.newBuilder().maxIterationLimit(1).build()); + + IllegalStateException e = + assertThrows(IllegalStateException.class, () -> optimizer.optimize(ast, CEL)); + assertThat(e).hasMessageThat().contains("Max iteration count reached."); + } + + @Test + public void optimize_deMorganAll_evaluatesCorrectly() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("!int_list.all(e, e == 1)").getAst(); + CelAbstractSyntaxTree optimizedAst = OPTIMIZER.optimize(ast); + + boolean result = + (boolean) + CEL.createProgram(optimizedAst) + .eval(ImmutableMap.of("int_list", ImmutableList.of(1, 2))); + assertThat(result).isTrue(); + } + + @Test + public void optimize_deMorganExists_evaluatesCorrectly() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("!int_list.exists(e, e == 1)").getAst(); + CelAbstractSyntaxTree optimizedAst = OPTIMIZER.optimize(ast); + + boolean result = + (boolean) + CEL.createProgram(optimizedAst) + .eval(ImmutableMap.of("int_list", ImmutableList.of(1, 2))); + assertThat(result).isFalse(); + } + + @Test + public void optimize_deMorganAll_negatedPredicate_evaluatesCorrectly() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("!int_list.all(e, !(e == 1))").getAst(); + CelAbstractSyntaxTree optimizedAst = OPTIMIZER.optimize(ast); + + boolean result = + (boolean) + CEL.createProgram(optimizedAst) + .eval(ImmutableMap.of("int_list", ImmutableList.of(1, 2))); + assertThat(result).isTrue(); + } + + @Test + public void optimize_deMorganExists_negatedPredicate_evaluatesCorrectly() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("!int_list.exists(e, !(e == 1))").getAst(); + CelAbstractSyntaxTree optimizedAst = OPTIMIZER.optimize(ast); + + boolean result = + (boolean) + CEL.createProgram(optimizedAst) + .eval(ImmutableMap.of("int_list", ImmutableList.of(1, 2))); + assertThat(result).isFalse(); + } + + @Test + public void optimize_customMacroWithExistsStructure_notCanonicalized() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("!int_list.exists(e, e == 1)").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + long macroKey = mutableAst.source().getMacroCalls().keySet().iterator().next(); + CelMutableExpr existingMacro = mutableAst.source().getMacroCalls().get(macroKey); + CelMutableCall customCall = + CelMutableCall.create( + existingMacro.call().target().get(), "my_custom_exists", existingMacro.call().args()); + mutableAst + .source() + .addMacroCalls(macroKey, CelMutableExpr.ofCall(existingMacro.id(), customCall)); + + CelAbstractSyntaxTree optimizedAst = + CanonicalizationOptimizer.newInstance(CanonicalizationOptions.newBuilder().build()) + .optimize(mutableAst.toParsedAst(), CEL) + .optimizedAst(); + assertThat(UNPARSER.unparse(optimizedAst)).isEqualTo("!int_list.my_custom_exists(e, e == 1)"); + } +} diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index cea14e910..d7724ac91 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -131,7 +131,7 @@ public final class CelVerifierZ3ImplTest { .build(); private static final CelVerifier VERIFIER = - CelVerifierFactory.newVerifier().setTypeProvider(TYPE_PROVIDER).build(); + CelVerifierFactory.newVerifier(CEL).setTypeProvider(TYPE_PROVIDER).build(); @Before public void setUp() { @@ -380,7 +380,7 @@ public void isSatisfiable_maskedByBmcNested_inconclusive() throws Exception { CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); CelVerifier customVerifier = - CelVerifierFactory.newVerifier() + CelVerifierFactory.newVerifier(CEL) .setComprehensionUnrollLimit(3) .setTypeProvider(TYPE_PROVIDER) .build(); @@ -395,7 +395,8 @@ public void isSatisfiable_comprehensionZeroUnrollLimit_inconclusive() throws Exc String expr = "int_list == [1] ? int_list.exists(x, x == 1) : false"; CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); - CelVerifier verifier = CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(0).build(); + CelVerifier verifier = + CelVerifierFactory.newVerifier(CEL).setComprehensionUnrollLimit(0).build(); CelVerificationResult result = verifier.isSatisfiable(ast); assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); @@ -956,7 +957,7 @@ public void isAlwaysTrue_withUnknownIdentifier_evaluatesToUnknown( CelAbstractSyntaxTree ast = CEL.compile(expression).getAst(); CelVerifier verifierWithUnknown = - CelVerifierFactory.newVerifier().addUnknownIdentifier("x").build(); + CelVerifierFactory.newVerifier(CEL).addUnknownIdentifier("x").build(); // 'x == x' is not a tautology if it can be unknown // i.e: CelUnknown == CelUnknown is unknown. @@ -978,7 +979,10 @@ public void isAlwaysTrue_dynamicComprehensionNonBoolYieldsError() throws Excepti + " dyn_list.all(x, x.not_a_bool) == false) : true"; CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); CelVerificationResult result = - CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(1).build().isAlwaysTrue(ast); + CelVerifierFactory.newVerifier(CEL) + .setComprehensionUnrollLimit(1) + .build() + .isAlwaysTrue(ast); assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); assertThat(result.message()) @@ -992,7 +996,8 @@ public void isAlwaysTrue_comprehensionExceedsMaxIterations_returnsUnknown() thro String expr = "int_list == [1, 2, 3] ? int_list.all(x, x > 0) : true"; CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); - CelVerifier verifier = CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(2).build(); + CelVerifier verifier = + CelVerifierFactory.newVerifier(CEL).setComprehensionUnrollLimit(2).build(); CelVerificationResult verifiedValue = verifier.isAlwaysTrue(ast); // Truncated loops return Unknown, which negates to Unknown. @@ -1009,7 +1014,8 @@ public void isAlwaysTrue_comprehensionZeroUnrollLimit_emptyList() throws Excepti String expr = "int_list == [] ? int_list.all(x, x > 0) : true"; CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); - CelVerifier verifier = CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(0).build(); + CelVerifier verifier = + CelVerifierFactory.newVerifier(CEL).setComprehensionUnrollLimit(0).build(); CelVerificationResult verifiedValue = verifier.isAlwaysTrue(ast); assertThat(verifiedValue.status()).isEqualTo(VerificationStatus.VERIFIED); @@ -1067,7 +1073,9 @@ public void verifyEquivalence_unknownPrecedenceOverError() throws Exception { CelAbstractSyntaxTree astB = celWithCustomFunc.compile("1 / 0").getAst(); CelVerifier verifier = - CelVerifierFactory.newVerifier().addUnknownIdentifier("unknown_var").build(); + CelVerifierFactory.newVerifier(celWithCustomFunc) + .addUnknownIdentifier("unknown_var") + .build(); CelVerificationResult result = verifier.verifyEquivalence(astA, astB); @@ -1981,7 +1989,18 @@ private enum EquivalenceTestCase { HETEROGENEOUS_UINT_NEGATIVE_DOUBLE_EQUIVALENCE("dyn(u) == -1.0", "false"), HETEROGENEOUS_UINT_ZERO_DOUBLE_EQUIVALENCE("dyn(u) == 0.0", "u == 0u"), DYNAMIC_LIST_ELEMENT_NEVER_ERROR_EQUIVALENCE( - "size(dyn_list) > 0 ? (dyn_list[0] == 1 || dyn_list[0] != 1) : true", "true"); + "size(dyn_list) > 0 ? (dyn_list[0] == 1 || dyn_list[0] != 1) : true", "true"), + CANONICALIZE_MAP_TWO_VAR_PREDICATE_ORDER( + "string_int_map.exists(k, v, k == 'foo' && v == 1)", + "string_int_map.exists(k, v, v == 1 && k == 'foo')"), + CANONICALIZE_MAP_TWO_VAR_ALPHA_RENAME( + "string_int_map.exists(k, v, k == 'foo' && v == 1)", + "string_int_map.exists(key, val, key == 'foo' && val == 1)"), + CANONICALIZE_MAP_TWO_VAR_DE_MORGAN( + "!string_int_map.exists(k, v, !(v > 0))", "string_int_map.all(k, v, v > 0)"), + CANONICALIZE_LIST_PREDICATE_ORDER( + "int_list.all(e, e > 0 && e < 100)", "int_list.all(e, e < 100 && e > 0)"), + CANONICALIZE_LIST_ALPHA_RENAME("int_list.all(e, e > 0)", "int_list.all(elem, elem > 0)"); private final String exprA; private final String exprB; @@ -2367,7 +2386,7 @@ public void verifyEquivalence_functionError_equivalent() throws Exception { @Test @SuppressWarnings("GoodTime-ApiWithNumericTimeUnit") // Test only public void setTimeout_invalidDuration_throws(@TestParameter({"0", "-1"}) long timeoutSeconds) { - CelVerifierBuilder builder = CelVerifierFactory.newVerifier(); + CelVerifierBuilder builder = CelVerifierFactory.newVerifier(CEL); IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, @@ -2386,9 +2405,6 @@ public void isSatisfiable_divisionByZero_failsInCelWithErrors() throws Exception @Test public void isSatisfiable_timeoutReached_throwsCelVerificationException() throws Exception { - CelVerifier timeoutVerifier = - CelVerifierFactory.newVerifier().setTimeout(Duration.ofMillis(1)).build(); - Cel customCel = CelFactory.plannerCelBuilder() .addVar("d1", SimpleType.DOUBLE) @@ -2396,6 +2412,8 @@ public void isSatisfiable_timeoutReached_throwsCelVerificationException() throws .addVar("d3", SimpleType.DOUBLE) .addVar("d4", SimpleType.DOUBLE) .build(); + CelVerifier timeoutVerifier = + CelVerifierFactory.newVerifier(customCel).setTimeout(Duration.ofMillis(1)).build(); // An overly complex double multiplication to guarantee Z3 FPA theory solver timeouts. CelAbstractSyntaxTree ast = @@ -2819,7 +2837,7 @@ public void isAlwaysTrue_largeListCounterexample_truncatesOutput() throws Except CelAbstractSyntaxTree ast = cel.compile("!(large_list == " + listLiteral + ")").getAst(); CelVerifier verifier = - CelVerifierFactory.newVerifier().setTimeout(Duration.ofSeconds(10)).build(); + CelVerifierFactory.newVerifier(cel).setTimeout(Duration.ofSeconds(10)).build(); CelVerificationResult result = verifier.isAlwaysTrue(ast); @@ -2844,7 +2862,7 @@ public void isAlwaysTrue_largeMapCounterexample_truncatesOutput() throws Excepti CelAbstractSyntaxTree ast = cel.compile("!(large_map == " + mapLiteral + ")").getAst(); CelVerifier verifier = - CelVerifierFactory.newVerifier().setTimeout(Duration.ofSeconds(10)).build(); + CelVerifierFactory.newVerifier(cel).setTimeout(Duration.ofSeconds(10)).build(); CelVerificationResult result = verifier.isAlwaysTrue(ast); @@ -2895,7 +2913,7 @@ public void isAlwaysTrue_customComprehensionWithTrueAccuInit() throws Exception .build(); CelAbstractSyntaxTree ast = cel.compile("dyn_list == [1, 2] ? dyn_list.custom_fold(x) == true : true").getAst(); - CelVerifier verifier = CelVerifierFactory.newVerifier().build(); + CelVerifier verifier = CelVerifierFactory.newVerifier(cel).build(); CelVerificationResult result = verifier.isAlwaysTrue(ast); @@ -2910,7 +2928,8 @@ public void isSatisfiable_maskedByBmcButAlwaysFalse_returnsFailed() throws Excep String expr = "int_list == [1, 2, 3, 4] ? int_list.exists(x, x == 42) && false : false"; CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); - CelVerifier verifier = CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(3).build(); + CelVerifier verifier = + CelVerifierFactory.newVerifier(CEL).setComprehensionUnrollLimit(3).build(); CelVerificationResult result = verifier.isSatisfiable(ast); assertThat(result.status()).isEqualTo(VerificationStatus.VIOLATED); @@ -2925,7 +2944,8 @@ public void verifyEquivalence_maskedByBmcButAlwaysEqual_returnsVerified() throws CelAbstractSyntaxTree astA = CEL.compile(exprA).getAst(); CelAbstractSyntaxTree astB = CEL.compile(exprB).getAst(); - CelVerifier verifier = CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(3).build(); + CelVerifier verifier = + CelVerifierFactory.newVerifier(CEL).setComprehensionUnrollLimit(3).build(); CelVerificationResult result = verifier.verifyEquivalence(astA, astB); assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); @@ -2952,7 +2972,8 @@ public void verifyEquivalence_zeroUnrollLimit_returnsInconclusive( CelAbstractSyntaxTree astA = CEL.compile(testCase.exprA).getAst(); CelAbstractSyntaxTree astB = CEL.compile(testCase.exprB).getAst(); - CelVerifier verifier = CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(0).build(); + CelVerifier verifier = + CelVerifierFactory.newVerifier(CEL).setComprehensionUnrollLimit(0).build(); CelVerificationResult result = verifier.verifyEquivalence(astA, astB); assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); @@ -3001,7 +3022,8 @@ public void verifyEquivalence_comprehensionScopeShadowing_returnsInconclusive() CelAbstractSyntaxTree astA = customCel.compile("dyn_list.my_macro_1(true)").getAst(); CelAbstractSyntaxTree astB = customCel.compile("dyn_list.my_macro_2(true)").getAst(); - CelVerifier verifier = CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(0).build(); + CelVerifier verifier = + CelVerifierFactory.newVerifier(customCel).setComprehensionUnrollLimit(0).build(); CelVerificationResult result = verifier.verifyEquivalence(astA, astB); assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); @@ -3045,7 +3067,8 @@ public void verifyEquivalence_comprehensionResultScopeIsolation_returnsInconclus CelAbstractSyntaxTree astB = customCel.compile("cel.bind(x, 20, dyn_list.my_macro(1))").getAst(); - CelVerifier verifier = CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(0).build(); + CelVerifier verifier = + CelVerifierFactory.newVerifier(customCel).setComprehensionUnrollLimit(0).build(); CelVerificationResult result = verifier.verifyEquivalence(astA, astB); assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); @@ -3053,9 +3076,6 @@ public void verifyEquivalence_comprehensionResultScopeIsolation_returnsInconclus @Test public void verifyEquivalence_timeoutReached_throwsCelVerificationException() throws Exception { - CelVerifier timeoutVerifier = - CelVerifierFactory.newVerifier().setTimeout(Duration.ofMillis(1)).build(); - Cel customCel = CelFactory.plannerCelBuilder() .addVar("d1", SimpleType.DOUBLE) @@ -3064,6 +3084,9 @@ public void verifyEquivalence_timeoutReached_throwsCelVerificationException() th .addVar("d4", SimpleType.DOUBLE) .build(); + CelVerifier timeoutVerifier = + CelVerifierFactory.newVerifier(customCel).setTimeout(Duration.ofMillis(1)).build(); + CelAbstractSyntaxTree astA = customCel .compile( @@ -3085,7 +3108,7 @@ public void verifyImplication_loopExceedsLimit_returnsTruncatedInconclusive() th CelAbstractSyntaxTree assertAst = CEL.compile("int_list.all(x, x > 0)").getAst(); CelVerifier verifier = - CelVerifierFactory.newVerifier().setComprehensionUnrollLimit(2).build(); + CelVerifierFactory.newVerifier(CEL).setComprehensionUnrollLimit(2).build(); CelVerificationResult result = ((CelVerifierZ3Impl) verifier) .verifyImplication(assumeAst, assertAst, ImmutableMap.of(), "Implication"); @@ -3102,7 +3125,7 @@ public void verifyImplication_symbolicNan_crossNumericComparisonReturnsFalse() t // Assertion: x < d is false when d is NaN CelAbstractSyntaxTree assertAst = CEL.compile("!(x < d)").getAst(); - CelVerifier verifier = CelVerifierFactory.newVerifier().build(); + CelVerifier verifier = CelVerifierFactory.newVerifier(CEL).build(); CelVerificationResult result = ((CelVerifierZ3Impl) verifier) .verifyImplication(assumeAst, assertAst, ImmutableMap.of(), "Implication"); diff --git a/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierReplTest.java b/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierReplTest.java index 5b289c39a..88cd62b88 100644 --- a/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierReplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierReplTest.java @@ -174,6 +174,20 @@ public void repl_equivDoubleNegation() throws Exception { assertThat(output[0]).contains("[VERIFIED]"); } + @Test + public void repl_equivCanonicalization() throws Exception { + String[] output = + runReplWithCommands( + ":var map_string_int map", + ":var int_list list", + "equiv map_string_int.exists(k, v, k == 'foo' && v == 1) <=> map_string_int.exists(k," + + " v, v == 1 && k == 'foo')", + "equiv int_list.all(e, e > 0) <=> int_list.all(elem, elem > 0)", + ":quit"); + assertThat(output[0]).contains("[VERIFIED]"); + assertThat(output[1]).isEmpty(); + } + @Test public void repl_unknownCommandsAndErrors() throws Exception { String[] output = diff --git a/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierToolTest.java b/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierToolTest.java index 383604aa0..e3b2a21a6 100644 --- a/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierToolTest.java +++ b/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierToolTest.java @@ -267,6 +267,23 @@ public void verifyEquivalence_equivalent() throws Exception { assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); } + @Test + public void verifyEquivalence_canonicalizedMapComprehension() throws Exception { + VerificationOptions options = + VerificationOptions.builder().setTimeout(Duration.ofSeconds(5)).build(); + ImmutableMap vars = + ImmutableMap.of("map_string_int", MapType.create(SimpleType.STRING, SimpleType.INT)); + + CelVerificationResult result = + CelVerifierToolCore.verifyEquivalence( + "map_string_int.exists(k, v, k == 'foo' && v == 1)", + "map_string_int.exists(k, v, v == 1 && k == 'foo')", + vars, + options); + + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } + @Test public void verifyPolicyInvariants_success() throws Exception { String yamlPolicy = From d5f903bb82f7041ab81abc1c929654116e4cfa78 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Thu, 6 Aug 2026 20:52:38 -0700 Subject: [PATCH 164/204] Include verifier and verifier-cli as publishable maven artifacts PiperOrigin-RevId: 960666765 --- .github/workflows/unwanted_deps.sh | 4 ++++ publish/BUILD.bazel | 21 ++++++++++++++++++- publish/publish.sh | 2 +- .../java/dev/cel/verifier/tools/BUILD.bazel | 1 + verifier/tools/README.md | 19 ++++++++++++----- 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/.github/workflows/unwanted_deps.sh b/.github/workflows/unwanted_deps.sh index c483f9730..a74ee003d 100755 --- a/.github/workflows/unwanted_deps.sh +++ b/.github/workflows/unwanted_deps.sh @@ -46,4 +46,8 @@ checkUnwantedDeps '//publish:cel' '@maven_android//:com_google_protobuf_protobuf # cel_runtime_android shouldn't depend on the full protobuf runtime or antlr checkUnwantedDeps '//publish:cel_runtime_android' '@maven//:com_google_protobuf_protobuf_java' checkUnwantedDeps '//publish:cel_runtime_android' '@maven//:org_antlr_antlr4_runtime' + +# cel shouldn't depend on the verifier +checkUnwantedDeps '//publish:cel' '//verifier/' + exit 0 diff --git a/publish/BUILD.bazel b/publish/BUILD.bazel index 185c7fb7d..69766290e 100644 --- a/publish/BUILD.bazel +++ b/publish/BUILD.bazel @@ -1,5 +1,5 @@ load("@bazel_common//tools/maven:pom_file.bzl", "pom_file") -load("@rules_jvm_external//:defs.bzl", "java_export") +load("@rules_jvm_external//:defs.bzl", "java_export", "maven_export") load("//publish:cel_version.bzl", "CEL_VERSION") # Note: These targets must reference the build targets in `src` directly in @@ -349,3 +349,22 @@ java_export( pom_template = ":cel_verifier_pom", exports = VERIFIER_TARGETS + [":cel"], ) + +pom_file( + name = "cel_verifier_cli_pom", + substitutions = { + "CEL_VERSION": CEL_VERSION, + "CEL_ARTIFACT_ID": "verifier-cli", + "PACKAGE_NAME": "CEL Java Verifier CLI", + "PACKAGE_DESC": "Formal verification CLI and REPL tool for Common Expression Language for Java.", + }, + targets = [], + template_file = "pom_template.xml", +) + +maven_export( + name = "cel_verifier_cli", + maven_coordinates = "dev.cel:verifier-cli:%s" % CEL_VERSION, + pom_template = ":cel_verifier_cli_pom", + target = "//verifier/src/main/java/dev/cel/verifier/tools:cel_verifier_tool_deploy.jar", +) diff --git a/publish/publish.sh b/publish/publish.sh index 28d0f0f53..d83016de3 100755 --- a/publish/publish.sh +++ b/publish/publish.sh @@ -26,7 +26,7 @@ # Note, to run script: Bazel and jq are required -ALL_TARGETS=("//publish:cel_common.publish" "//publish:cel.publish" "//publish:cel_compiler.publish" "//publish:cel_runtime.publish" "//publish:cel_v1alpha1.publish" "//publish:cel_protobuf.publish" "//publish:cel_runtime_android.publish") +ALL_TARGETS=("//publish:cel_common.publish" "//publish:cel.publish" "//publish:cel_compiler.publish" "//publish:cel_runtime.publish" "//publish:cel_v1alpha1.publish" "//publish:cel_protobuf.publish" "//publish:cel_runtime_android.publish" "//publish:cel_verifier.publish" "//publish:cel_verifier_cli.publish") JDK8_FLAGS="--java_language_version=8 --java_runtime_version=8" function publish_maven_remote() { diff --git a/verifier/src/main/java/dev/cel/verifier/tools/BUILD.bazel b/verifier/src/main/java/dev/cel/verifier/tools/BUILD.bazel index 28ce776cb..1d2c7569e 100644 --- a/verifier/src/main/java/dev/cel/verifier/tools/BUILD.bazel +++ b/verifier/src/main/java/dev/cel/verifier/tools/BUILD.bazel @@ -6,6 +6,7 @@ package( "//:license", ], default_visibility = [ + "//publish:__pkg__", "//verifier:__subpackages__", ], ) diff --git a/verifier/tools/README.md b/verifier/tools/README.md index 398cbad74..b34422b27 100644 --- a/verifier/tools/README.md +++ b/verifier/tools/README.md @@ -6,6 +6,20 @@ and policy invariants without writing Java code. ## Running the CLI Tool +### Standalone Executable (Prebuilt JAR) + +You can download the standalone executable fat-JAR (`dev.cel:verifier-cli`) +directly from Maven Central and invoke it with `java -jar`: + + +```bash +# Download the latest CLI JAR +curl -LO https://repo1.maven.org/maven2/dev/cel/verifier-cli/0.13.1/verifier-cli-0.13.1.jar + +# Run the verifier CLI / REPL +java -jar verifier-cli-0.13.1.jar --help +``` + ### Running via Bazel ```bash @@ -27,11 +41,6 @@ bazel run //verifier/tools:cel_verifier_tool -- \ bazel run //verifier/tools:cel_verifier_tool -- repl ``` -### Running via Maven Central - -> **Note:** Executable binaries and Maven packages (`dev.cel:cel-verifier`) -> will be published to Maven Central in an upcoming release. - ## CLI Commands * `check-sat --expr "..."`: Verifies satisfiability of an expression and From d459cc91720fa6d0e4e28c7e3f14189c23c521b7 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Fri, 7 Aug 2026 10:26:31 -0700 Subject: [PATCH 165/204] Introduce common helpers for handling AST navigation PiperOrigin-RevId: 960999429 --- common/navigation/BUILD.bazel | 10 + .../dev/cel/common/navigation/BUILD.bazel | 30 ++ .../common/navigation/BaseNavigableExpr.java | 9 +- .../navigation/CelNavigableExprUtil.java | 156 ++++++++ .../dev/cel/common/navigation/BUILD.bazel | 2 + .../navigation/CelNavigableExprUtilTest.java | 348 ++++++++++++++++++ .../dev/cel/optimizer/optimizers/BUILD.bazel | 2 + .../optimizers/ConstantFoldingOptimizer.java | 36 +- .../optimizers/InliningOptimizer.java | 21 +- 9 files changed, 563 insertions(+), 51 deletions(-) create mode 100644 common/src/main/java/dev/cel/common/navigation/CelNavigableExprUtil.java create mode 100644 common/src/test/java/dev/cel/common/navigation/CelNavigableExprUtilTest.java diff --git a/common/navigation/BUILD.bazel b/common/navigation/BUILD.bazel index 0c03596f9..8da2514b8 100644 --- a/common/navigation/BUILD.bazel +++ b/common/navigation/BUILD.bazel @@ -25,3 +25,13 @@ java_library( name = "mutable_navigation", exports = ["//common/src/main/java/dev/cel/common/navigation:mutable_navigation"], ) + +java_library( + name = "expr_util", + exports = ["//common/src/main/java/dev/cel/common/navigation:expr_util"], +) + +cel_android_library( + name = "expr_util_android", + exports = ["//common/src/main/java/dev/cel/common/navigation:expr_util_android"], +) diff --git a/common/src/main/java/dev/cel/common/navigation/BUILD.bazel b/common/src/main/java/dev/cel/common/navigation/BUILD.bazel index 3c2eaad62..4ae2908bc 100644 --- a/common/src/main/java/dev/cel/common/navigation/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/navigation/BUILD.bazel @@ -48,6 +48,36 @@ cel_android_library( ], ) +java_library( + name = "expr_util", + srcs = [ + "CelNavigableExprUtil.java", + ], + tags = [ + ], + deps = [ + ":common", + "//common/ast", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "expr_util_android", + srcs = [ + "CelNavigableExprUtil.java", + ], + tags = [ + ], + deps = [ + ":common_android", + "//common/ast:ast_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "navigation", srcs = [ diff --git a/common/src/main/java/dev/cel/common/navigation/BaseNavigableExpr.java b/common/src/main/java/dev/cel/common/navigation/BaseNavigableExpr.java index 1699b4a96..dabcac3a2 100644 --- a/common/src/main/java/dev/cel/common/navigation/BaseNavigableExpr.java +++ b/common/src/main/java/dev/cel/common/navigation/BaseNavigableExpr.java @@ -16,6 +16,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CheckReturnValue; +import com.google.errorprone.annotations.DoNotMock; import dev.cel.common.ast.CelExpr; import dev.cel.common.ast.CelExpr.ExprKind; import dev.cel.common.ast.Expression; @@ -25,9 +26,15 @@ /** * BaseNavigableExpr represents the base navigable expression value with methods to inspect the * parent and child expressions. + * + *

This class is intentionally non-extensible outside of the {@code dev.cel.common.navigation} + * package. */ +@DoNotMock("Use CelNavigableExpr or CelNavigableMutableExpr") @SuppressWarnings("unchecked") // Generic types are properly bound to Expression -abstract class BaseNavigableExpr { +public abstract class BaseNavigableExpr { + + BaseNavigableExpr() {} public abstract E expr(); diff --git a/common/src/main/java/dev/cel/common/navigation/CelNavigableExprUtil.java b/common/src/main/java/dev/cel/common/navigation/CelNavigableExprUtil.java new file mode 100644 index 000000000..9229214eb --- /dev/null +++ b/common/src/main/java/dev/cel/common/navigation/CelNavigableExprUtil.java @@ -0,0 +1,156 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.navigation; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.errorprone.annotations.CheckReturnValue; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import dev.cel.common.ast.Expression; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; + +/** Utility class for common AST navigation and scoping inspections on {@link BaseNavigableExpr}. */ +@CheckReturnValue +public final class CelNavigableExprUtil { + + /** + * Returns true if {@code variableName} is in scope and shadowed by an enclosing comprehension + * above {@code expr}. + * + *

A variable is shadowed at {@code expr} if an ancestor comprehension declares it as an + * iteration variable ({@code iterVar}, {@code iterVar2}) or accumulator variable ({@code + * accuVar}) and {@code expr} resides within a branch where that variable is active: + * + *

    + *
  • In {@code loopCondition} and {@code loopStep}: {@code iterVar}, {@code iterVar2}, and + * {@code accuVar} are in scope. + *
  • In {@code result}: only {@code accuVar} is in scope ({@code iterVar} and {@code iterVar2} + * have fallen out of scope). + *
  • In {@code iterRange} and {@code accuInit}: none of the comprehension variables are in + * scope. + *
+ * + *

For example, in the expression: + * + *

{@code
+   * [1, 2].all(x, x > 0)
+   * }
+ * + *
    + *
  • At {@code x} in {@code x > 0}: {@code isVariableShadowed(x, "x")} is {@code true}. + *
  • At the list {@code [1, 2]}: {@code isVariableShadowed(list, "x")} is {@code false}. + *
+ */ + public static boolean isVariableShadowed(BaseNavigableExpr expr, String variableName) { + return areVariablesShadowed(expr, Collections.singleton(variableName)); + } + + /** + * Returns true if any of {@code variableNames} is in scope and shadowed by an enclosing + * comprehension above {@code expr}. + * + *

For example, in the nested comprehension expression: + * + *

{@code
+   * [1, 2].all(x, [3, 4].all(y, x > 0 && y > 0))
+   * }
+ * + * At {@code y > 0}, {@code areVariablesShadowed(node, ImmutableSet.of("x", "z"))} is {@code true} + * because {@code x} is in scope from the outer comprehension. + */ + @SuppressWarnings("ReferenceEquality") // Required to disambiguate child branches + public static boolean areVariablesShadowed( + BaseNavigableExpr expr, Collection variableNames) { + checkNotNull(expr); + checkNotNull(variableNames); + if (variableNames.isEmpty()) { + return false; + } + BaseNavigableExpr curr = expr; + Optional> maybeParent = curr.parent(); + while (maybeParent.isPresent()) { + BaseNavigableExpr parent = maybeParent.get(); + if (parent.getKind() == Kind.COMPREHENSION) { + Expression.Comprehension comp = parent.expr().comprehension(); + Expression currExpr = curr.expr(); + + if (currExpr != comp.iterRange() && currExpr != comp.accuInit()) { + if (currExpr == comp.result()) { + if (variableNames.contains(comp.accuVar())) { + return true; + } + } else { + if (variableNames.contains(comp.iterVar()) + || variableNames.contains(comp.iterVar2()) + || variableNames.contains(comp.accuVar())) { + return true; + } + } + } + } + curr = parent; + maybeParent = parent.parent(); + } + return false; + } + + /** + * Returns true if {@code expr} is an {@code IDENT} node that references a variable declared by an + * enclosing comprehension. + * + *

For example, in the expression: + * + *

{@code
+   * [a].all(x, x > a)
+   * }
+ * + *
    + *
  • At identifier {@code x}: {@code isComprehensionVariable(x)} is {@code true}. + *
  • At identifier {@code a}: {@code isComprehensionVariable(a)} is {@code false}. + *
+ */ + public static boolean isComprehensionVariable(BaseNavigableExpr expr) { + checkNotNull(expr); + return expr.getKind() == Kind.IDENT + && areVariablesShadowed(expr, Collections.singleton(expr.expr().ident().name())); + } + + /** + * Returns true if {@code expr} or any identifier within {@code expr} references a variable + * declared by an enclosing comprehension. + * + *

For example, in the expression: + * + *

{@code
+   * [a].all(x, x > a)
+   * }
+ * + *
    + *
  • At the subtree {@code x > a}: {@code hasComprehensionVariable(subtree)} is {@code true} + * because {@code x} is a comprehension variable. + *
  • At the subtree {@code [a]}: {@code hasComprehensionVariable(iterRange)} is {@code false}. + *
+ */ + public static boolean hasComprehensionVariable(BaseNavigableExpr expr) { + checkNotNull(expr); + return expr.allNodes() + .filter(node -> node.getKind() == Kind.IDENT) + .anyMatch(CelNavigableExprUtil::isComprehensionVariable); + } + + private CelNavigableExprUtil() {} +} diff --git a/common/src/test/java/dev/cel/common/navigation/BUILD.bazel b/common/src/test/java/dev/cel/common/navigation/BUILD.bazel index f8b2b988b..0a29dfe8a 100644 --- a/common/src/test/java/dev/cel/common/navigation/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/navigation/BUILD.bazel @@ -21,10 +21,12 @@ java_library( "//common/ast:mutable_expr", "//common/navigation", "//common/navigation:common", + "//common/navigation:expr_util", "//common/navigation:mutable_navigation", "//common/types", "//compiler", "//compiler:compiler_builder", + "//extensions", "//parser:macro", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@maven//:com_google_guava_guava", diff --git a/common/src/test/java/dev/cel/common/navigation/CelNavigableExprUtilTest.java b/common/src/test/java/dev/cel/common/navigation/CelNavigableExprUtilTest.java new file mode 100644 index 000000000..56e06d187 --- /dev/null +++ b/common/src/test/java/dev/cel/common/navigation/CelNavigableExprUtilTest.java @@ -0,0 +1,348 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.navigation; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelMutableAst; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import dev.cel.common.ast.CelMutableExpr; +import dev.cel.common.ast.CelMutableExpr.CelMutableCall; +import dev.cel.common.ast.CelMutableExpr.CelMutableComprehension; +import dev.cel.common.ast.CelMutableExpr.CelMutableList; +import dev.cel.common.types.SimpleType; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.extensions.CelExtensions; +import dev.cel.parser.CelStandardMacro; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class CelNavigableExprUtilTest { + + private static final CelCompiler COMPILER = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addLibraries(CelExtensions.comprehensions()) + .addVar("a", SimpleType.INT) + .addVar("b", SimpleType.INT) + .build(); + + @Test + public void isVariableShadowed_singleVarComprehension_loopStep() throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile("[1, 2].all(x, x > 0)").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + CelNavigableExpr identX = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.expr().identOrDefault().name().equals("x")) + .findFirst() + .get(); + + assertThat(CelNavigableExprUtil.isVariableShadowed(identX, "x")).isTrue(); + assertThat(CelNavigableExprUtil.isVariableShadowed(identX, "y")).isFalse(); + } + + @Test + public void isVariableShadowed_twoVarComprehension_loopStep() throws Exception { + CelAbstractSyntaxTree ast = + COMPILER.compile("{'k1': 1, 'k2': 2}.all(k, v, k != '' && v > 0)").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + CelNavigableExpr identK = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.expr().identOrDefault().name().equals("k")) + .findFirst() + .get(); + CelNavigableExpr identV = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.expr().identOrDefault().name().equals("v")) + .findFirst() + .get(); + CelNavigableExpr iterRangeMap = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.getKind() == Kind.MAP) + .findFirst() + .get(); + + assertThat(CelNavigableExprUtil.isVariableShadowed(identK, "k")).isTrue(); + assertThat(CelNavigableExprUtil.isVariableShadowed(identK, "v")).isTrue(); + assertThat(CelNavigableExprUtil.isVariableShadowed(identV, "k")).isTrue(); + assertThat(CelNavigableExprUtil.isVariableShadowed(identV, "v")).isTrue(); + assertThat(CelNavigableExprUtil.isVariableShadowed(identK, "other")).isFalse(); + + assertThat(CelNavigableExprUtil.isVariableShadowed(iterRangeMap, "k")).isFalse(); + assertThat(CelNavigableExprUtil.isVariableShadowed(iterRangeMap, "v")).isFalse(); + } + + @Test + public void isVariableShadowed_iterRange_notShadowed() throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile("[a].all(x, x > 0)").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + CelNavigableExpr identA = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.expr().identOrDefault().name().equals("a")) + .findFirst() + .get(); + + assertThat(CelNavigableExprUtil.isVariableShadowed(identA, "x")).isFalse(); + assertThat(CelNavigableExprUtil.isVariableShadowed(identA, "a")).isFalse(); + } + + @Test + public void isVariableShadowed_nestedComprehension_scopedCorrectly() throws Exception { + CelAbstractSyntaxTree ast = + COMPILER.compile("[1, 2].all(x, [3, 4].all(y, x > 0 && y > 0))").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + CelNavigableExpr innerIdentX = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.expr().identOrDefault().name().equals("x")) + .findFirst() + .get(); + CelNavigableExpr innerIdentY = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.expr().identOrDefault().name().equals("y")) + .findFirst() + .get(); + + assertThat(CelNavigableExprUtil.isVariableShadowed(innerIdentX, "x")).isTrue(); + assertThat(CelNavigableExprUtil.isVariableShadowed(innerIdentX, "y")).isTrue(); + assertThat(CelNavigableExprUtil.isVariableShadowed(innerIdentY, "x")).isTrue(); + assertThat(CelNavigableExprUtil.isVariableShadowed(innerIdentY, "y")).isTrue(); + } + + @Test + public void isVariableShadowed_nestedComprehension_innerIterRangeShadowsOuterOnly() + throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile("[1, 2].all(x, [x].all(y, y > 0))").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + CelNavigableExpr innerIterRangeIdentX = + navigableAst + .getRoot() + .allNodes() + .filter( + node -> + node.expr().identOrDefault().name().equals("x") + && node.parent().isPresent() + && node.parent().get().getKind() == Kind.LIST) + .findFirst() + .get(); + + // In the inner comprehension's iterRange, outer 'x' IS in scope, but inner 'y' is NOT in scope + assertThat(CelNavigableExprUtil.isVariableShadowed(innerIterRangeIdentX, "x")).isTrue(); + assertThat(CelNavigableExprUtil.isVariableShadowed(innerIterRangeIdentX, "y")).isFalse(); + } + + @Test + public void isVariableShadowed_comprehensionResultBranch() throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile("[1, 2].all(x, x > 0)").getAst(); + CelNavigableMutableAst navigableAst = + CelNavigableMutableAst.fromAst(CelMutableAst.fromCelAst(ast)); + + CelNavigableMutableExpr comprehensionNode = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.getKind() == Kind.COMPREHENSION) + .findFirst() + .get(); + + CelMutableComprehension comprehension = comprehensionNode.expr().comprehension(); + long resultId = comprehension.result().id(); + + CelNavigableMutableExpr resultNode = + comprehensionNode.allNodes().filter(node -> node.id() == resultId).findFirst().get(); + + // In result branch, accuVar is in scope, but iterVar is not + assertThat(CelNavigableExprUtil.isVariableShadowed(resultNode, comprehension.accuVar())) + .isTrue(); + assertThat(CelNavigableExprUtil.isVariableShadowed(resultNode, comprehension.iterVar())) + .isFalse(); + } + + @Test + public void areVariablesShadowed_multipleVariables() throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile("[1, 2].all(x, x > 0)").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + CelNavigableExpr identX = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.expr().identOrDefault().name().equals("x")) + .findFirst() + .get(); + + assertThat(CelNavigableExprUtil.areVariablesShadowed(identX, ImmutableSet.of("y", "z", "x"))) + .isTrue(); + assertThat(CelNavigableExprUtil.areVariablesShadowed(identX, ImmutableSet.of("y", "z"))) + .isFalse(); + assertThat(CelNavigableExprUtil.areVariablesShadowed(identX, ImmutableList.of())).isFalse(); + } + + @Test + public void isComprehensionVariable_identNode() throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile("[a].all(x, x > a)").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + CelNavigableExpr identX = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.expr().identOrDefault().name().equals("x")) + .findFirst() + .get(); + CelNavigableExpr identAInLoop = + navigableAst + .getRoot() + .allNodes() + .filter( + node -> + node.expr().identOrDefault().name().equals("a") + && node.parent().isPresent() + && node.parent().get().getKind() == Kind.CALL) + .findFirst() + .get(); + CelNavigableExpr constNode = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.getKind() == Kind.CONSTANT) + .findFirst() + .get(); + + assertThat(CelNavigableExprUtil.isComprehensionVariable(identX)).isTrue(); + assertThat(CelNavigableExprUtil.isComprehensionVariable(identAInLoop)).isFalse(); + assertThat(CelNavigableExprUtil.isComprehensionVariable(constNode)).isFalse(); + } + + @Test + public void hasComprehensionVariable_subtreeCheck() throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile("[a].all(x, x > a)").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + CelNavigableExpr root = navigableAst.getRoot(); + CelNavigableExpr iterRange = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.getKind() == Kind.LIST) + .findFirst() + .get(); + CelNavigableExpr loopStepCall = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.expr().callOrDefault().function().equals("@not_strictly_false")) + .findFirst() + .get(); + + assertThat(CelNavigableExprUtil.hasComprehensionVariable(root)).isTrue(); + assertThat(CelNavigableExprUtil.hasComprehensionVariable(loopStepCall)).isTrue(); + assertThat(CelNavigableExprUtil.hasComprehensionVariable(iterRange)).isFalse(); + } + + @Test + public void mutableAst_parityWithImmutableAst() throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile("[1, 2].all(x, x > 0)").getAst(); + CelNavigableAst immutableNavAst = CelNavigableAst.fromAst(ast); + CelNavigableMutableAst mutableNavAst = + CelNavigableMutableAst.fromAst(CelMutableAst.fromCelAst(ast)); + + CelNavigableExpr immutableIdentX = + immutableNavAst + .getRoot() + .allNodes() + .filter(node -> node.expr().identOrDefault().name().equals("x")) + .findFirst() + .get(); + CelNavigableMutableExpr mutableIdentX = + mutableNavAst + .getRoot() + .allNodes() + .filter(node -> node.getKind() == Kind.IDENT && node.expr().ident().name().equals("x")) + .findFirst() + .get(); + + assertThat(CelNavigableExprUtil.isVariableShadowed(mutableIdentX, "x")) + .isEqualTo(CelNavigableExprUtil.isVariableShadowed(immutableIdentX, "x")); + assertThat(CelNavigableExprUtil.isComprehensionVariable(mutableIdentX)) + .isEqualTo(CelNavigableExprUtil.isComprehensionVariable(immutableIdentX)); + assertThat(CelNavigableExprUtil.hasComprehensionVariable(mutableNavAst.getRoot())) + .isEqualTo(CelNavigableExprUtil.hasComprehensionVariable(immutableNavAst.getRoot())); + } + + @Test + public void isVariableShadowed_zeroedOutIds_scopedCorrectly() { + // Construct a mutable comprehension where ALL expression IDs are 0 (e.g. freshly minted AST) + CelMutableExpr iterRange = CelMutableExpr.ofList(0, CelMutableList.create()); + CelMutableExpr accuInit = CelMutableExpr.ofConstant(0, CelConstant.ofValue(true)); + CelMutableExpr loopCond = CelMutableExpr.ofConstant(0, CelConstant.ofValue(true)); + CelMutableExpr identX = CelMutableExpr.ofIdent(0, "x"); + CelMutableExpr loopStep = CelMutableExpr.ofCall(0, CelMutableCall.create("!_", identX)); + CelMutableExpr result = CelMutableExpr.ofIdent(0, "accu"); + + CelMutableExpr comp = + CelMutableExpr.ofComprehension( + 0, + CelMutableComprehension.create( + "x", iterRange, "accu", accuInit, loopCond, loopStep, result)); + + CelNavigableMutableExpr root = CelNavigableMutableExpr.fromExpr(comp); + + CelNavigableMutableExpr navIdentX = + root.allNodes() + .filter(node -> node.getKind() == Kind.IDENT && node.expr().ident().name().equals("x")) + .findFirst() + .get(); + CelNavigableMutableExpr navIterRange = + root.allNodes().filter(node -> node.getKind() == Kind.LIST).findFirst().get(); + CelNavigableMutableExpr navResult = + root.allNodes() + .filter( + node -> node.getKind() == Kind.IDENT && node.expr().ident().name().equals("accu")) + .findFirst() + .get(); + + assertThat(CelNavigableExprUtil.isVariableShadowed(navIdentX, "x")).isTrue(); + assertThat(CelNavigableExprUtil.isVariableShadowed(navIdentX, "accu")).isTrue(); + assertThat(CelNavigableExprUtil.isVariableShadowed(navIterRange, "x")).isFalse(); + assertThat(CelNavigableExprUtil.isVariableShadowed(navResult, "x")).isFalse(); + assertThat(CelNavigableExprUtil.isVariableShadowed(navResult, "accu")).isTrue(); + } +} diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel index da722d521..0c4b78826 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -29,6 +29,7 @@ java_library( "//common/ast:mutable_expr", "//common/internal:date_time_helpers", "//common/navigation:common", + "//common/navigation:expr_util", "//common/navigation:mutable_navigation", "//common/types", "//common/types:type_providers", @@ -95,6 +96,7 @@ java_library( "//common:operator", "//common/ast", "//common/ast:mutable_expr", + "//common/navigation:expr_util", "//common/navigation:mutable_navigation", "//common/types", "//common/types:type_providers", diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java index 1cf52bcbe..266059426 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java @@ -36,12 +36,12 @@ import dev.cel.common.ast.CelExpr.ExprKind.Kind; import dev.cel.common.ast.CelMutableExpr; import dev.cel.common.ast.CelMutableExpr.CelMutableCall; -import dev.cel.common.ast.CelMutableExpr.CelMutableComprehension; import dev.cel.common.ast.CelMutableExpr.CelMutableList; import dev.cel.common.ast.CelMutableExpr.CelMutableMap; import dev.cel.common.ast.CelMutableExpr.CelMutableStruct; import dev.cel.common.ast.CelMutableExprConverter; import dev.cel.common.internal.DateTimeHelpers; +import dev.cel.common.navigation.CelNavigableExprUtil; import dev.cel.common.navigation.CelNavigableMutableAst; import dev.cel.common.navigation.CelNavigableMutableExpr; import dev.cel.common.navigation.TraversalOrder; @@ -247,7 +247,7 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) { if (functionName.equals(Operator.EQUALS.getFunction()) || functionName.equals(Operator.NOT_EQUALS.getFunction())) { - if (hasComprehensionVar(navigableExpr)) { + if (CelNavigableExprUtil.hasComprehensionVariable(navigableExpr)) { return false; } if (mutableCall.args().stream() @@ -259,7 +259,7 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) { } if (functionName.equals(Operator.IN.getFunction())) { - return !hasComprehensionVar(navigableExpr); + return !CelNavigableExprUtil.hasComprehensionVariable(navigableExpr); } // Default case: all call arguments must be constants. If the argument is a container (ex: @@ -288,33 +288,6 @@ private static boolean isCallTimestampOrDuration(CelMutableCall call) { || call.function().equals(DURATION.functionName()); } - private static boolean hasComprehensionVar(CelNavigableMutableExpr expr) { - return expr.allNodes() - .filter(node -> node.getKind().equals(Kind.IDENT)) - .anyMatch( - identNode -> { - String identName = identNode.expr().ident().name(); - CelNavigableMutableExpr curr = identNode; - Optional maybeParent = curr.parent(); - while (maybeParent.isPresent()) { - CelNavigableMutableExpr parent = maybeParent.get(); - if (parent.getKind().equals(Kind.COMPREHENSION)) { - CelMutableComprehension compre = parent.expr().comprehension(); - if ((compre.accuVar().equals(identName) - || compre.iterVar().equals(identName) - || compre.iterVar2().equals(identName)) - && curr.id() != compre.iterRange().id() - && curr.id() != compre.accuInit().id()) { - return true; - } - } - curr = parent; - maybeParent = parent.parent(); - } - return false; - }); - } - private static boolean areChildrenArgConstant(CelNavigableMutableExpr expr) { if (expr.getKind().equals(Kind.CONSTANT)) { return true; @@ -350,7 +323,8 @@ private Optional maybeFold( CelMutableAst mutableAst, CelNavigableMutableExpr node) throws CelOptimizationException, CelEvaluationException { - if (!node.getKind().equals(Kind.COMPREHENSION) && hasComprehensionVar(node)) { + if (!node.getKind().equals(Kind.COMPREHENSION) + && CelNavigableExprUtil.hasComprehensionVariable(node)) { return Optional.empty(); } Object result; diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/InliningOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/InliningOptimizer.java index e4051f82f..147673e47 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/InliningOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/InliningOptimizer.java @@ -27,8 +27,8 @@ import dev.cel.common.ast.CelExpr.ExprKind.Kind; import dev.cel.common.ast.CelMutableExpr; import dev.cel.common.ast.CelMutableExpr.CelMutableCall; -import dev.cel.common.ast.CelMutableExpr.CelMutableComprehension; import dev.cel.common.ast.CelMutableExpr.CelMutableStruct; +import dev.cel.common.navigation.CelNavigableExprUtil; import dev.cel.common.navigation.CelNavigableMutableAst; import dev.cel.common.navigation.CelNavigableMutableExpr; import dev.cel.common.types.CelKind; @@ -41,7 +41,6 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.Optional; -import java.util.stream.Stream; /** * Performs optimization for inlining variables within function calls and select statements with @@ -222,23 +221,7 @@ private static boolean canInline(CelNavigableMutableExpr node, String identifier return false; } - for (CelNavigableMutableExpr p = node.parent().orElse(null); - p != null; - p = p.parent().orElse(null)) { - if (p.getKind() != Kind.COMPREHENSION) { - continue; - } - - CelMutableComprehension comp = p.expr().comprehension(); - boolean shadows = - Stream.of(comp.iterVar(), comp.iterVar2(), comp.accuVar()).anyMatch(identifier::equals); - - if (shadows) { - return false; - } - } - - return true; + return !CelNavigableExprUtil.isVariableShadowed(node, identifier); } private static Optional maybeToQualifiedName(CelNavigableMutableExpr node) { From 29fd271f37a4d53a59d9f47dd69204c14d7ac71c Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Fri, 7 Aug 2026 15:03:49 -0700 Subject: [PATCH 166/204] Add timestamp, duration and optional types to CEL Verifier CLI PiperOrigin-RevId: 961139137 --- .../CelZ3CounterexampleGenerator.java | 2 +- .../cel/verifier/tools/CelVerifierRepl.java | 12 ++- .../verifier/tools/VerificationOptions.java | 33 +++++++- .../cel/verifier/CelVerifierZ3ImplTest.java | 2 +- .../verifier/tools/CelVerifierReplTest.java | 63 ++++++++++++++- .../verifier/tools/CelVerifierToolTest.java | 68 ++++++++++++++-- .../tools/VerificationOptionsTest.java | 80 ++++++++++++++++++- verifier/tools/README.md | 5 +- 8 files changed, 245 insertions(+), 20 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java index 6e5c519fe..cef976608 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java @@ -88,7 +88,7 @@ private static String formatExpr( } else if (decl.equals(typeSystem.timestampCons().ConstructorDecl())) { return "timestamp(" + formatExpr(ctx, typeSystem, model, expr.getArgs()[0]) + ")"; } else if (decl.equals(typeSystem.durationCons().ConstructorDecl())) { - return "duration(" + formatExpr(ctx, typeSystem, model, expr.getArgs()[0]) + ")"; + return "duration('" + formatExpr(ctx, typeSystem, model, expr.getArgs()[0]) + "s')"; } else if (decl.equals(typeSystem.uintCons().ConstructorDecl())) { return formatExpr(ctx, typeSystem, model, expr.getArgs()[0]) + "u"; } else if (decl.equals(typeSystem.boolCons().ConstructorDecl())) { diff --git a/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierRepl.java b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierRepl.java index 94348ff15..82a93c3d7 100644 --- a/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierRepl.java +++ b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierRepl.java @@ -314,15 +314,21 @@ private static void printHelp(String topic, PrintStream out) { out.println("Declares a variable in the REPL session with a specific type."); out.println(); out.println("Supported Types:"); - out.println(" - Primitive types: int, uint, string, bool, double, bytes"); - out.println(" - List types: list (e.g., list, list)"); - out.println(" - Map types: map (e.g., map, map)"); + out.println(" - Primitive types: int, uint, string, bool, double, bytes, dyn"); + out.println(" - Well-known types: timestamp, duration"); + out.println(" - List types: list (e.g., list, list)"); + out.println(" - Map types: map (e.g., map, map)"); + out.println(" - Optional types: optional (e.g., optional, optional)"); + out.println(" - Protobuf types: coming soon"); out.println(); out.println("Examples:"); out.println(" cel-verifier> :var role string"); out.println(" cel-verifier> :var port int"); out.println(" cel-verifier> :var scores map"); out.println(" cel-verifier> :var tags list"); + out.println(" cel-verifier> :var created_at timestamp"); + out.println(" cel-verifier> :var timeout duration"); + out.println(" cel-verifier> :var opt_flag optional"); break; case "unknown": out.println("Command: :unknown "); diff --git a/verifier/src/main/java/dev/cel/verifier/tools/VerificationOptions.java b/verifier/src/main/java/dev/cel/verifier/tools/VerificationOptions.java index f2b3bf742..6e61a182d 100644 --- a/verifier/src/main/java/dev/cel/verifier/tools/VerificationOptions.java +++ b/verifier/src/main/java/dev/cel/verifier/tools/VerificationOptions.java @@ -21,6 +21,7 @@ import dev.cel.common.types.CelType; import dev.cel.common.types.ListType; import dev.cel.common.types.MapType; +import dev.cel.common.types.OptionalType; import dev.cel.common.types.SimpleType; import java.time.Duration; import java.util.ArrayList; @@ -147,17 +148,20 @@ static ImmutableMap parseVariables(List varSpecs) { } static CelType parseCelType(String typeStr) { + // TODO: Replace with shorthand type parser once it is available. Preconditions.checkNotNull(typeStr, "Type string cannot be null."); String str = typeStr.trim().toLowerCase(Locale.US); if (str.startsWith("list<") && str.endsWith(">")) { - String inner = str.substring(5, str.length() - 1).trim(); + // Strip "list<" prefix and trailing ">" to extract the element type "T". + String inner = str.substring("list<".length(), str.length() - 1).trim(); CelType elemType = parseCelType(inner); return ListType.create(elemType); } if (str.startsWith("map<") && str.endsWith(">")) { - String inner = str.substring(4, str.length() - 1).trim(); + // Strip "map<" prefix and trailing ">" to extract the key and value types "K, V". + String inner = str.substring("map<".length(), str.length() - 1).trim(); List parts = splitGenericArgs(inner); if (parts.size() != 2) { throw new IllegalArgumentException( @@ -170,6 +174,20 @@ static CelType parseCelType(String typeStr) { return MapType.create(keyType, valueType); } + if (str.startsWith("optional<") && str.endsWith(">")) { + // Strip "optional<" prefix and trailing ">" to extract the wrapped type "T". + String inner = str.substring("optional<".length(), str.length() - 1).trim(); + CelType elemType = parseCelType(inner); + return OptionalType.create(elemType); + } + + if (str.startsWith("optional_type<") && str.endsWith(">")) { + // Strip "optional_type<" prefix and trailing ">" to extract the wrapped type "T". + String inner = str.substring("optional_type<".length(), str.length() - 1).trim(); + CelType elemType = parseCelType(inner); + return OptionalType.create(elemType); + } + switch (str) { case "int": return SimpleType.INT; @@ -187,12 +205,19 @@ static CelType parseCelType(String typeStr) { return SimpleType.BYTES; case "dyn": return SimpleType.DYN; + case "timestamp": + case "google.protobuf.timestamp": + return SimpleType.TIMESTAMP; + case "duration": + case "google.protobuf.duration": + return SimpleType.DURATION; default: + // TODO: Support protobuf message types (coming soon). throw new IllegalArgumentException( "Unsupported type for CLI variable declaration: '" + typeStr - + "'. Supported types: int, uint, string, bool, double, bytes, dyn, list, map."); + + "'. Supported types: int, uint, string, bool, double, bytes, dyn, timestamp," + + " duration, list, map, optional."); } } diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index d7724ac91..003256e0c 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -1312,7 +1312,7 @@ private enum IsAlwaysTrueViolationTestCase { "dur != dur", "Condition is not always true\\.", "Counterexample input:", - "dur = duration\\(-?\\d+\\)"), + "dur = duration\\('-?\\d+s'\\)"), TIMESTAMP_VARIABLE_COUNTEREXAMPLE( "ts != ts", "Condition is not always true\\.", diff --git a/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierReplTest.java b/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierReplTest.java index 88cd62b88..cbe5fffd5 100644 --- a/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierReplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierReplTest.java @@ -53,9 +53,11 @@ private String[] runReplWithCommands(String... commands) throws Exception { @Test public void repl_quitAndExit() throws Exception { String[] output1 = runReplWithCommands(":quit"); + assertThat(output1[0]).contains("Goodbye!"); String[] output2 = runReplWithCommands(":exit"); + assertThat(output2[0]).contains("Goodbye!"); } @@ -73,6 +75,7 @@ public void repl_helpCommands() throws Exception { ":help equiv", ":help non_existent_topic", ":quit"); + assertThat(output[0]).contains("REPL Commands:"); assertThat(output[0]).contains("Command: :var "); assertThat(output[0]).contains("Command: :unknown "); @@ -81,6 +84,9 @@ public void repl_helpCommands() throws Exception { assertThat(output[0]).contains("Query: sat "); assertThat(output[0]).contains("Query: valid "); assertThat(output[0]).contains("Query: equiv <=> "); + assertThat(output[0]).contains("Well-known types: timestamp, duration"); + assertThat(output[0]).contains("Optional types: optional"); + assertThat(output[0]).contains("Protobuf types: coming soon"); } @Test @@ -91,19 +97,27 @@ public void repl_varDeclarations() throws Exception { ":var port int", ":var scores map", ":var tags list", + ":var created_at timestamp", + ":var timeout duration", + ":var opt_user optional", ":vars", ":quit"); + assertThat(output[0]).contains("Variable declared: role : string"); assertThat(output[0]).contains("Variable declared: port : int"); assertThat(output[0]).contains("Variable declared: scores : map(string, int)"); assertThat(output[0]).contains("Variable declared: tags : list(string)"); - assertThat(output[0]).contains("Variables (4):"); + assertThat(output[0]).contains("Variable declared: created_at : google.protobuf.Timestamp"); + assertThat(output[0]).contains("Variable declared: timeout : google.protobuf.Duration"); + assertThat(output[0]).contains("Variable declared: opt_user : optional_type(string)"); + assertThat(output[0]).contains("Variables (7):"); } @Test public void repl_unknownIdentifiers() throws Exception { String[] output = runReplWithCommands(":unknown request.headers", ":unknown request.auth", ":vars", ":quit"); + assertThat(output[0]).contains("Added unknown identifier: 'request.headers'"); assertThat(output[0]).contains("Added unknown identifier: 'request.auth'"); assertThat(output[0]).contains("Unknowns: [request.headers, request.auth]"); @@ -114,6 +128,7 @@ public void repl_timeoutConfiguration() throws Exception { String[] output = runReplWithCommands( ":timeout 15", ":vars", ":timeout -5", ":timeout abc", ":timeout", ":quit"); + assertThat(output[0]).contains("Timeout set to 15s."); assertThat(output[0]).contains("Timeout: 15s"); assertThat(output[1]).contains("Timeout must be a positive integer."); @@ -125,6 +140,7 @@ public void repl_timeoutConfiguration() throws Exception { public void repl_unrollConfiguration() throws Exception { String[] output = runReplWithCommands(":unroll 10", ":vars", ":unroll -1", ":unroll xyz", ":unroll", ":quit"); + assertThat(output[0]).contains("Comprehension unroll limit set to 10."); assertThat(output[0]).contains("Unroll limit: 10"); assertThat(output[1]).contains("Unroll limit must be non-negative."); @@ -137,6 +153,7 @@ public void repl_sessionStateAndClear() throws Exception { String[] output = runReplWithCommands( ":var role string", ":unknown req.headers", ":vars", ":clear", ":vars", ":quit"); + assertThat(output[0]).contains("Variables (1):"); assertThat(output[0]).contains("Session state reset."); assertThat(output[0]).contains("Variables (0):"); @@ -147,6 +164,7 @@ public void repl_sessionStateAndClear() throws Exception { public void repl_satQueries() throws Exception { String[] output = runReplWithCommands(":var port int", "sat port > 1024", "port > 1024", "sat", ":quit"); + assertThat(output[0]).contains("[VERIFIED]"); assertThat(output[1]).contains("Usage: sat "); } @@ -155,6 +173,7 @@ public void repl_satQueries() throws Exception { public void repl_validQueries() throws Exception { String[] output = runReplWithCommands(":var x int", "valid x > 0 || x <= 0", "valid x > 0", "valid", ":quit"); + assertThat(output[0]).contains("[VERIFIED]"); assertThat(output[0]).contains("[VIOLATED]"); assertThat(output[1]).contains("Usage: valid "); @@ -164,6 +183,7 @@ public void repl_validQueries() throws Exception { public void repl_equivQueries() throws Exception { String[] output = runReplWithCommands(":var x int", "equiv x > 10 <=> 10 < x", "equiv x > 10", ":quit"); + assertThat(output[0]).contains("[VERIFIED]"); assertThat(output[1]).contains("Equivalence query format: equiv <=> "); } @@ -171,6 +191,7 @@ public void repl_equivQueries() throws Exception { @Test public void repl_equivDoubleNegation() throws Exception { String[] output = runReplWithCommands(":var x int", "equiv !!(x == 10) <=> (x == 10)", ":quit"); + assertThat(output[0]).contains("[VERIFIED]"); } @@ -184,6 +205,45 @@ public void repl_equivCanonicalization() throws Exception { + " v, v == 1 && k == 'foo')", "equiv int_list.all(e, e > 0) <=> int_list.all(elem, elem > 0)", ":quit"); + + assertThat(output[0]).contains("[VERIFIED]"); + assertThat(output[1]).isEmpty(); + } + + @Test + public void repl_timestampAndDurationQueries() throws Exception { + String[] output = + runReplWithCommands( + ":var t timestamp", + ":var d duration", + "sat t > timestamp(1000)", + "sat d > duration('60s')", + "sat t + d > timestamp(2000)", + ":quit"); + + assertThat(output[0]).contains("[VERIFIED]"); + assertThat(output[1]).isEmpty(); + } + + @Test + public void repl_durationSatisfyingInputFormat() throws Exception { + String[] output = + runReplWithCommands(":var dur duration", "timestamp(100) - timestamp(50) == dur", ":quit"); + + assertThat(output[0]).contains("[VERIFIED]"); + assertThat(output[0]).contains("dur = duration('50s')"); + assertThat(output[1]).isEmpty(); + } + + @Test + public void repl_optionalQueries() throws Exception { + String[] output = + runReplWithCommands( + ":var opt_val optional", + "sat opt_val.hasValue() && opt_val.value() > 100", + "sat !opt_val.hasValue()", + ":quit"); + assertThat(output[0]).contains("[VERIFIED]"); assertThat(output[1]).isEmpty(); } @@ -199,6 +259,7 @@ public void repl_unknownCommandsAndErrors() throws Exception { ":unknown", "invalid + + syntax", ":quit"); + assertThat(output[1]).contains("Unknown command: :unknowncommand"); assertThat(output[1]).contains("Usage: :var "); assertThat(output[1]).contains("Unsupported type"); diff --git a/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierToolTest.java b/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierToolTest.java index e3b2a21a6..2d01e7a0f 100644 --- a/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierToolTest.java +++ b/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierToolTest.java @@ -22,6 +22,7 @@ import dev.cel.common.types.CelType; import dev.cel.common.types.ListType; import dev.cel.common.types.MapType; +import dev.cel.common.types.OptionalType; import dev.cel.common.types.SimpleType; import dev.cel.verifier.CelVerificationResult; import dev.cel.verifier.CelVerificationResult.VerificationStatus; @@ -65,6 +66,7 @@ public void celVerifierTool_checkSat_jsonOutputFormat() { String output = executeToolWithOutput( "check-sat", "--expr", "x > 0", "--var", "x:int", "--output_format", "json"); + assertThat(output).startsWith("{\n"); assertThat(output).contains("\"status\": \"VERIFIED\""); assertThat(output).contains("satisfiable"); @@ -75,6 +77,7 @@ public void celVerifierTool_checkSat_jsonOutputFormat() { public void celVerifierTool_checkSat_textOutputFormat() { String output = executeToolWithOutput("check-sat", "--expr", "x > 0", "--var", "x:int", "-fmt", "text"); + assertThat(output).contains("[VERIFIED]"); assertThat(output).contains("satisfiable"); } @@ -84,6 +87,7 @@ public void celVerifierTool_checkSat_withDynVariable() { String output = executeToolWithOutput( "check-sat", "--expr", "x == 'hello'", "--var", "x:dyn", "-fmt", "json"); + assertThat(output).contains("\"status\": \"VERIFIED\""); } @@ -100,6 +104,7 @@ public void celVerifierTool_checkSat_withUnknownOption() { "request.headers", "-fmt", "json"); + assertThat(output).contains("\"status\": \"VERIFIED\""); } @@ -116,12 +121,33 @@ public void celVerifierTool_checkSat_withTimeoutAndUnrollLimit() { "5", "-fmt", "json"); + + assertThat(output).contains("\"status\": \"VERIFIED\""); + } + + @Test + public void celVerifierTool_checkSat_withTimestampDurationAndOptional() { + String output = + executeToolWithOutput( + "check-sat", + "--expr", + "t + d > timestamp(1000) && opt.hasValue()", + "--var", + "t:timestamp", + "--var", + "d:duration", + "--var", + "opt:optional", + "-fmt", + "json"); + assertThat(output).contains("\"status\": \"VERIFIED\""); } @Test public void celVerifierTool_verifyPolicy_fileNotFound() { String output = executeToolWithOutput("verify-policy", "--file", "non_existent_policy.yaml"); + assertThat(output).contains("File not found: non_existent_policy.yaml"); } @@ -134,12 +160,19 @@ public void parseVariables_success() { "role:string", "is_admin:bool", "tags:list", - "scores:map")); + "scores:map", + "created_at:timestamp", + "timeout:duration", + "opt_user:optional")); + assertThat(vars).containsEntry("x", SimpleType.INT); assertThat(vars).containsEntry("role", SimpleType.STRING); assertThat(vars).containsEntry("is_admin", SimpleType.BOOL); assertThat(vars).containsEntry("tags", ListType.create(SimpleType.STRING)); assertThat(vars).containsEntry("scores", MapType.create(SimpleType.STRING, SimpleType.INT)); + assertThat(vars).containsEntry("created_at", SimpleType.TIMESTAMP); + assertThat(vars).containsEntry("timeout", SimpleType.DURATION); + assertThat(vars).containsEntry("opt_user", OptionalType.create(SimpleType.STRING)); } @Test @@ -153,14 +186,21 @@ public void parseVariables_allTypesIncludingDyn() { "b:bytes", "dyn_val:dyn", "flag:boolean", + "t:google.protobuf.timestamp", + "dur:google.protobuf.duration", + "opt:optional", "nested_list:list", "nested_map:map")); + assertThat(vars).containsEntry("u", SimpleType.UINT); assertThat(vars).containsEntry("d", SimpleType.DOUBLE); assertThat(vars).containsEntry("fl", SimpleType.DOUBLE); assertThat(vars).containsEntry("b", SimpleType.BYTES); assertThat(vars).containsEntry("dyn_val", SimpleType.DYN); assertThat(vars).containsEntry("flag", SimpleType.BOOL); + assertThat(vars).containsEntry("t", SimpleType.TIMESTAMP); + assertThat(vars).containsEntry("dur", SimpleType.DURATION); + assertThat(vars).containsEntry("opt", OptionalType.create(SimpleType.INT)); assertThat(vars).containsEntry("nested_list", ListType.create(SimpleType.DYN)); assertThat(vars).containsEntry("nested_map", MapType.create(SimpleType.STRING, SimpleType.DYN)); } @@ -178,9 +218,12 @@ public void parseVariables_unsupportedType_throws() { assertThrows( IllegalArgumentException.class, () -> VerificationOptions.parseVariables(Arrays.asList("x:foo_bar"))); + assertThat(ex) .hasMessageThat() - .contains("Supported types: int, uint, string, bool, double, bytes, dyn"); + .contains( + "Supported types: int, uint, string, bool, double, bytes, dyn, timestamp," + + " duration, list, map, optional."); } @Test @@ -203,6 +246,7 @@ public void parseVariables_nestedTypes() { Arrays.asList( "nested_map:map>", "nested_list_map:map>")); + assertThat(vars) .containsEntry( "nested_map", @@ -298,7 +342,6 @@ public void verifyPolicyInvariants_success() throws Exception { + " - id: port_check\n" + " assert:\n" + " - port == 80 || port != 80\n"; - VerificationOptions options = VerificationOptions.builder().setTimeout(Duration.ofSeconds(5)).build(); ImmutableMap vars = ImmutableMap.of("port", SimpleType.INT); @@ -319,7 +362,6 @@ public void verifyPolicyEquivalence_equivalent() throws Exception { + " - condition: port == 80\n" + " output: 'true'\n" + " - output: 'false'\n"; - String policyB = "name: policy_b\n" + "rule:\n" @@ -327,7 +369,6 @@ public void verifyPolicyEquivalence_equivalent() throws Exception { + " - condition: 80 == port\n" + " output: 'true'\n" + " - output: 'false'\n"; - VerificationOptions options = VerificationOptions.builder().setTimeout(Duration.ofSeconds(5)).build(); ImmutableMap vars = ImmutableMap.of("port", SimpleType.INT); @@ -346,11 +387,11 @@ public void formatTextPolicyResults_verifiedAndViolated() throws Exception { CelVerifierToolCore.checkSatisfiable("true", ImmutableMap.of(), options); CelVerificationResult violatedRes = CelVerifierToolCore.checkValid("x > 0", ImmutableMap.of("x", SimpleType.INT), options); - ImmutableMap results = ImmutableMap.of("inv_1", verifiedRes, "inv_2", violatedRes); String text = FormatUtils.formatTextPolicyResults("test_policy", results); + assertThat(text).contains("Policy Invariant Verification for 'test_policy':"); assertThat(text).contains("✓ Invariant 'inv_1': VERIFIED"); assertThat(text).contains("✗ Invariant 'inv_2': VIOLATED"); @@ -362,10 +403,10 @@ public void formatJsonPolicyResults_structuredJson() throws Exception { VerificationOptions.builder().setTimeout(Duration.ofSeconds(5)).build(); CelVerificationResult result = CelVerifierToolCore.checkSatisfiable("true", ImmutableMap.of(), options); - ImmutableMap results = ImmutableMap.of("inv_1", result); String json = FormatUtils.formatJsonPolicyResults("my_policy", results); + assertThat(json).startsWith("{\n"); assertThat(json).contains("\"policyName\": \"my_policy\""); assertThat(json).contains("\"id\": \"inv_1\""); @@ -488,7 +529,9 @@ public void formatUtils_jsonResult() throws Exception { VerificationOptions.builder().setTimeout(Duration.ofSeconds(5)).build(); CelVerificationResult result = CelVerifierToolCore.checkSatisfiable("true", ImmutableMap.of(), options); + String json = FormatUtils.formatJsonResult(result); + assertThat(json).contains("\"status\": \"VERIFIED\""); assertThat(json).contains("satisfiable"); } @@ -498,6 +541,7 @@ public void celVerifierTool_checkSat_verified() { int exitCode = new CommandLine(new CelVerifierTool()) .execute("check-sat", "--expr", "x > 0", "--var", "x:int"); + assertThat(exitCode).isEqualTo(CelVerifierTool.EXIT_CODE_VERIFIED); } @@ -506,6 +550,7 @@ public void celVerifierTool_checkValid_violated() { int exitCode = new CommandLine(new CelVerifierTool()) .execute("check-valid", "--expr", "x > 0", "--var", "x:int"); + assertThat(exitCode).isEqualTo(CelVerifierTool.EXIT_CODE_VIOLATED); } @@ -514,6 +559,7 @@ public void celVerifierTool_verifyEquiv_verified() { int exitCode = new CommandLine(new CelVerifierTool()) .execute("verify-equiv", "--expr1", "x > 10", "--expr2", "10 < x", "--var", "x:int"); + assertThat(exitCode).isEqualTo(CelVerifierTool.EXIT_CODE_VERIFIED); } @@ -521,6 +567,7 @@ public void celVerifierTool_verifyEquiv_verified() { public void celVerifierTool_checkSat_compilationError() { String output = executeToolWithOutput("check-sat", "--expr", "invalid + + syntax", "--var", "x:int"); + assertThat(output).contains("Compilation error"); } @@ -529,6 +576,7 @@ public void celVerifierTool_checkValid_withUnknownOption_violated() { int exitCode = new CommandLine(new CelVerifierTool()) .execute("check-valid", "--expr", "x == x", "--var", "x:int", "-u", "x"); + assertThat(exitCode).isEqualTo(CelVerifierTool.EXIT_CODE_VIOLATED); } @@ -537,6 +585,7 @@ public void celVerifierTool_checkValid_inconclusive() { int exitCode = new CommandLine(new CelVerifierTool()) .execute("check-valid", "--expr", "int('123') == 123"); + assertThat(exitCode).isEqualTo(CelVerifierTool.EXIT_CODE_INCONCLUSIVE); } @@ -569,6 +618,7 @@ public void celVerifierTool_invalidOutputFormat_defaultsToText() { String output = executeToolWithOutput( "check-sat", "--expr", "x > 0", "--var", "x:int", "-fmt", "invalid_fmt"); + assertThat(output).contains("[VERIFIED]"); } @@ -580,6 +630,7 @@ public void formatTextPolicyResults_inconclusive() throws Exception { CelVerifierToolCore.checkValid("int('123') == 123", ImmutableMap.of(), options); String text = FormatUtils.formatTextPolicyResults("test_policy", ImmutableMap.of("inv_1", res)); + assertThat(text).contains("Invariant 'inv_1': INCONCLUSIVE"); } @@ -589,9 +640,11 @@ public void formatJson_escapesSpecialCharacters() throws Exception { VerificationOptions.builder().setTimeout(Duration.ofSeconds(5)).build(); CelVerificationResult res = CelVerifierToolCore.checkValid("int('123') == 123", ImmutableMap.of(), options); + String json = FormatUtils.formatJsonPolicyResults( "policy_with_\"quote\"\nand_newline", ImmutableMap.of("inv\ttab", res)); + assertThat(json).contains("policy_with_\\\"quote\\\"\\nand_newline"); assertThat(json).contains("inv\\ttab"); } @@ -599,6 +652,7 @@ public void formatJson_escapesSpecialCharacters() throws Exception { @Test public void celVerifierTool_version() { int exitCode = new CommandLine(new CelVerifierTool()).execute("--version"); + assertThat(exitCode).isEqualTo(0); } } diff --git a/verifier/src/test/java/dev/cel/verifier/tools/VerificationOptionsTest.java b/verifier/src/test/java/dev/cel/verifier/tools/VerificationOptionsTest.java index 28aac751a..e52868ea0 100644 --- a/verifier/src/test/java/dev/cel/verifier/tools/VerificationOptionsTest.java +++ b/verifier/src/test/java/dev/cel/verifier/tools/VerificationOptionsTest.java @@ -20,6 +20,9 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import dev.cel.common.types.CelType; +import dev.cel.common.types.ListType; +import dev.cel.common.types.MapType; +import dev.cel.common.types.OptionalType; import dev.cel.common.types.SimpleType; import dev.cel.verifier.tools.VerificationOptions.OutputFormat; import java.time.Duration; @@ -62,31 +65,89 @@ public void customOptions_allFieldsSet() { @Test public void setTimeout_null_throwsException() { VerificationOptions.Builder builder = VerificationOptions.builder(); + assertThrows(NullPointerException.class, () -> builder.setTimeout(null)); } @Test public void setComprehensionUnrollLimit_negative_throwsException() { VerificationOptions.Builder builder = VerificationOptions.builder(); + assertThrows(IllegalArgumentException.class, () -> builder.setComprehensionUnrollLimit(-1)); } @Test public void setOutputFormat_null_throwsException() { VerificationOptions.Builder builder = VerificationOptions.builder(); + assertThrows(NullPointerException.class, () -> builder.setOutputFormat(null)); } @Test public void parseVariables_validSpecs() { ImmutableMap vars = - VerificationOptions.parseVariables(ImmutableList.of("x:int", "name:string", "flag:bool")); + VerificationOptions.parseVariables( + ImmutableList.of( + "x:int", + "name:string", + "flag:bool", + "created_at:timestamp", + "timeout:duration", + "opt_user:optional", + "opt_list:optional>", + "opt_map:optional>")); assertThat(vars) .containsExactly( "x", SimpleType.INT, "name", SimpleType.STRING, - "flag", SimpleType.BOOL); + "flag", SimpleType.BOOL, + "created_at", SimpleType.TIMESTAMP, + "timeout", SimpleType.DURATION, + "opt_user", OptionalType.create(SimpleType.STRING), + "opt_list", OptionalType.create(ListType.create(SimpleType.INT)), + "opt_map", OptionalType.create(MapType.create(SimpleType.STRING, SimpleType.INT))); + } + + @Test + public void parseCelType_timestampAndDuration() { + assertThat(VerificationOptions.parseCelType("timestamp")).isEqualTo(SimpleType.TIMESTAMP); + assertThat(VerificationOptions.parseCelType("google.protobuf.timestamp")) + .isEqualTo(SimpleType.TIMESTAMP); + assertThat(VerificationOptions.parseCelType("google.protobuf.Timestamp")) + .isEqualTo(SimpleType.TIMESTAMP); + assertThat(VerificationOptions.parseCelType("duration")).isEqualTo(SimpleType.DURATION); + assertThat(VerificationOptions.parseCelType("google.protobuf.duration")) + .isEqualTo(SimpleType.DURATION); + assertThat(VerificationOptions.parseCelType("google.protobuf.Duration")) + .isEqualTo(SimpleType.DURATION); + } + + @Test + public void parseCelType_optionalTypes() { + assertThat(VerificationOptions.parseCelType("optional")) + .isEqualTo(OptionalType.create(SimpleType.INT)); + assertThat(VerificationOptions.parseCelType("optional")) + .isEqualTo(OptionalType.create(SimpleType.STRING)); + assertThat(VerificationOptions.parseCelType("optional_type")) + .isEqualTo(OptionalType.create(SimpleType.BOOL)); + assertThat(VerificationOptions.parseCelType("optional>")) + .isEqualTo(OptionalType.create(OptionalType.create(SimpleType.INT))); + assertThat(VerificationOptions.parseCelType("map>")) + .isEqualTo(MapType.create(SimpleType.STRING, OptionalType.create(SimpleType.INT))); + } + + @Test + public void parseCelType_parenthesesSyntax_throwsException() { + assertThrows( + IllegalArgumentException.class, () -> VerificationOptions.parseCelType("optional(int)")); + assertThrows( + IllegalArgumentException.class, + () -> VerificationOptions.parseCelType("optional_type(bool)")); + assertThrows( + IllegalArgumentException.class, () -> VerificationOptions.parseCelType("list(string)")); + assertThrows( + IllegalArgumentException.class, () -> VerificationOptions.parseCelType("map(string, int)")); } @Test @@ -98,6 +159,21 @@ public void parseVariables_nullOrEmpty_returnsEmptyMap() { @Test public void parseVariables_invalidSpec_throwsException() { ImmutableList specs = ImmutableList.of("invalid_spec_without_colon"); + assertThrows(IllegalArgumentException.class, () -> VerificationOptions.parseVariables(specs)); } + + @Test + public void parseVariables_unsupportedType_throwsException() { + ImmutableList specs = ImmutableList.of("x:unknown_type"); + + IllegalArgumentException ex = + assertThrows( + IllegalArgumentException.class, () -> VerificationOptions.parseVariables(specs)); + assertThat(ex) + .hasMessageThat() + .contains( + "Supported types: int, uint, string, bool, double, bytes, dyn, timestamp," + + " duration, list, map, optional."); + } } diff --git a/verifier/tools/README.md b/verifier/tools/README.md index b34422b27..09b83ad70 100644 --- a/verifier/tools/README.md +++ b/verifier/tools/README.md @@ -66,12 +66,15 @@ repeating the `--var` option. Supported types: * Primitive types: `int`, `uint`, `string`, `bool`, `double`, `bytes`, `dyn` +* Well-known types: `timestamp`, `duration` * List types: `list` (e.g., `--var "tags:list"`) * Map types: `map` (e.g., `--var "scores:map"`) +* Optional types: `optional` (e.g., `--var "opt_flag:optional"`) +* Protobuf types: Coming soon Examples: ```bash ---var "role:string" --var "port:int" --var "tags:list" +--var "role:string" --var "port:int" --var "tags:list" --var "created_at:timestamp" --var "opt_flag:optional" ``` ### Unknown Identifiers (`--unknown`, `-u`) From 0cdb3b60b054d85ceb71c107c129ca2597c9494c Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Fri, 7 Aug 2026 15:26:41 -0700 Subject: [PATCH 167/204] Refactor Canonicalization Optimizer Granular nested classes for comparator, safety check, and NNF normalization has been introduced. No functional changes PiperOrigin-RevId: 961149990 --- .../main/java/dev/cel/verifier/BUILD.bazel | 1 + .../verifier/CanonicalizationOptimizer.java | 924 ++++++++++-------- .../CanonicalizationOptimizerTest.java | 84 +- 3 files changed, 588 insertions(+), 421 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel index a0de7948a..e7f19fa1b 100644 --- a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel +++ b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel @@ -145,6 +145,7 @@ java_library( "//optimizer:ast_optimizer", "//optimizer:mutable_ast", "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", ], ) diff --git a/verifier/src/main/java/dev/cel/verifier/CanonicalizationOptimizer.java b/verifier/src/main/java/dev/cel/verifier/CanonicalizationOptimizer.java index 6532afd7f..c21558323 100644 --- a/verifier/src/main/java/dev/cel/verifier/CanonicalizationOptimizer.java +++ b/verifier/src/main/java/dev/cel/verifier/CanonicalizationOptimizer.java @@ -70,188 +70,6 @@ final class CanonicalizationOptimizer implements CelAstOptimizer { private final CanonicalizationOptions canonicalizationOptions; - private static final Comparator EXPR_COMPARATOR = - new Comparator() { - @Override - public int compare(CelMutableExpr e1, CelMutableExpr e2) { - int kindCmp = - Integer.compare(getKindPriority(e1.getKind()), getKindPriority(e2.getKind())); - if (kindCmp != 0) { - return kindCmp; - } - switch (e1.getKind()) { - case CONSTANT: - return compareConstants(e1.constant(), e2.constant()); - case IDENT: - return e1.ident().name().compareTo(e2.ident().name()); - case SELECT: - return compareSelect(e1.select(), e2.select()); - case CALL: - return compareCall(e1.call(), e2.call()); - case LIST: - return compareList(e1.list().elements(), e2.list().elements()); - case MAP: - return compareMap(e1.map(), e2.map()); - case STRUCT: - return compareStruct(e1.struct(), e2.struct()); - case COMPREHENSION: - return compareComprehension(e1.comprehension(), e2.comprehension()); - case NOT_SET: - return 0; - } - throw new UnsupportedOperationException("Unsupported expression kind: " + e1.getKind()); - } - - private int compareConstants(CelConstant c1, CelConstant c2) { - int constKindCmp = c1.getKind().name().compareTo(c2.getKind().name()); - if (constKindCmp != 0) { - return constKindCmp; - } - switch (c1.getKind()) { - case NULL_VALUE: - case NOT_SET: - return 0; - case BOOLEAN_VALUE: - return Boolean.compare(c1.booleanValue(), c2.booleanValue()); - case INT64_VALUE: - return Long.compare(c1.int64Value(), c2.int64Value()); - case UINT64_VALUE: - return c1.uint64Value().compareTo(c2.uint64Value()); - case DOUBLE_VALUE: - return Double.compare(c1.doubleValue(), c2.doubleValue()); - case STRING_VALUE: - return c1.stringValue().compareTo(c2.stringValue()); - case BYTES_VALUE: - return CelByteString.unsignedLexicographicalComparator() - .compare(c1.bytesValue(), c2.bytesValue()); - default: - throw new UnsupportedOperationException("Unsupported constant kind: " + c1.getKind()); - } - } - - private int compareSelect(CelMutableSelect s1, CelMutableSelect s2) { - return ComparisonChain.start() - .compare(s1.operand(), s2.operand(), this) - .compare(s1.field(), s2.field()) - .compareFalseFirst(s1.testOnly(), s2.testOnly()) - .result(); - } - - private int compareCall(CelMutableCall c1, CelMutableCall c2) { - int fnCmp = c1.function().compareTo(c2.function()); - if (fnCmp != 0) { - return fnCmp; - } - boolean hasT1 = c1.target().isPresent(); - boolean hasT2 = c2.target().isPresent(); - if (hasT1 != hasT2) { - return Boolean.compare(hasT1, hasT2); - } - if (hasT1) { - int tCmp = compare(c1.target().get(), c2.target().get()); - if (tCmp != 0) { - return tCmp; - } - } - return compareList(c1.args(), c2.args()); - } - - private int compareMap(CelMutableMap m1, CelMutableMap m2) { - int mapSizeCmp = Integer.compare(m1.entries().size(), m2.entries().size()); - if (mapSizeCmp != 0) { - return mapSizeCmp; - } - Iterator it2 = m2.entries().iterator(); - for (CelMutableMap.Entry entry1 : m1.entries()) { - CelMutableMap.Entry entry2 = it2.next(); - int cmp = - ComparisonChain.start() - .compare(entry1.key(), entry2.key(), this) - .compare(entry1.value(), entry2.value(), this) - .result(); - if (cmp != 0) { - return cmp; - } - } - return 0; - } - - private int compareStruct(CelMutableStruct s1, CelMutableStruct s2) { - int msgCmp = s1.messageName().compareTo(s2.messageName()); - if (msgCmp != 0) { - return msgCmp; - } - int structSizeCmp = Integer.compare(s1.entries().size(), s2.entries().size()); - if (structSizeCmp != 0) { - return structSizeCmp; - } - Iterator it2 = s2.entries().iterator(); - for (CelMutableStruct.Entry entry1 : s1.entries()) { - CelMutableStruct.Entry entry2 = it2.next(); - int cmp = - ComparisonChain.start() - .compare(entry1.fieldKey(), entry2.fieldKey()) - .compare(entry1.value(), entry2.value(), this) - .result(); - if (cmp != 0) { - return cmp; - } - } - return 0; - } - - private int compareComprehension(CelMutableComprehension c1, CelMutableComprehension c2) { - return ComparisonChain.start() - .compare(c1.iterVar(), c2.iterVar()) - .compare(c1.iterVar2(), c2.iterVar2()) - .compare(c1.accuVar(), c2.accuVar()) - .compare(c1.iterRange(), c2.iterRange(), this) - .compare(c1.accuInit(), c2.accuInit(), this) - .compare(c1.loopCondition(), c2.loopCondition(), this) - .compare(c1.loopStep(), c2.loopStep(), this) - .compare(c1.result(), c2.result(), this) - .result(); - } - - private int compareList(List l1, List l2) { - int sizeCmp = Integer.compare(l1.size(), l2.size()); - if (sizeCmp != 0) { - return sizeCmp; - } - Iterator it2 = l2.iterator(); - for (CelMutableExpr elem1 : l1) { - int cmp = compare(elem1, it2.next()); - if (cmp != 0) { - return cmp; - } - } - return 0; - } - - private int getKindPriority(Kind kind) { - switch (kind) { - case IDENT: - return 1; - case SELECT: - return 2; - case CALL: - return 3; - case LIST: - return 4; - case MAP: - return 5; - case STRUCT: - return 6; - case COMPREHENSION: - return 7; - case CONSTANT: - return 8; - default: - return 99; - } - } - }; - /** * Returns a new instance of canonicalization optimizer configured with the provided {@link * CanonicalizationOptions}. @@ -322,31 +140,6 @@ private static boolean canCanonicalize(CelNavigableMutableExpr navigable) { || isCallWithArgCount(expr, Operator.LOGICAL_NOT.getFunction(), 1); } - private static boolean isComprehensionAccuVar(CelNavigableMutableExpr expr) { - return expr.allNodes() - .filter(node -> node.getKind().equals(Kind.IDENT)) - .anyMatch( - identNode -> { - String identName = identNode.expr().ident().name(); - CelNavigableMutableExpr curr = identNode; - Optional maybeParent = curr.parent(); - while (maybeParent.isPresent()) { - CelNavigableMutableExpr parent = maybeParent.get(); - if (parent.getKind().equals(Kind.COMPREHENSION)) { - CelMutableComprehension compre = parent.expr().comprehension(); - if (compre.accuVar().equals(identName) - && curr.id() != compre.iterRange().id() - && curr.id() != compre.accuInit().id()) { - return true; - } - } - curr = parent; - maybeParent = parent.parent(); - } - return false; - }); - } - private static Optional maybeCanonicalize( CelMutableAst mutableAst, CelNavigableMutableExpr navigableExpr) { CelMutableExpr expr = navigableExpr.expr(); @@ -360,134 +153,111 @@ private static Optional maybeCanonicalize( if ((functionName.equals(Operator.LOGICAL_AND.getFunction()) || functionName.equals(Operator.LOGICAL_OR.getFunction())) && args.size() == 2) { - List navigableOperands = - flattenNavigableOperands(navigableExpr, functionName); - if (navigableOperands.stream().anyMatch(CanonicalizationOptimizer::isComprehensionAccuVar)) { - return Optional.empty(); - } - List operands = new ArrayList<>(); - for (CelNavigableMutableExpr navOp : navigableOperands) { - operands.add(navOp.expr()); - } - operands.sort(EXPR_COMPARATOR); - List uniqueSorted = new ArrayList<>(); - for (CelMutableExpr op : operands) { - if (uniqueSorted.isEmpty() - || EXPR_COMPARATOR.compare(op, Iterables.getLast(uniqueSorted)) != 0) { - uniqueSorted.add(op); - } - } - CelMutableExpr rebuilt = uniqueSorted.get(0); - for (int i = 1; i < uniqueSorted.size(); i++) { - rebuilt = - CelMutableExpr.ofCall( - expr.id(), CelMutableCall.create(functionName, rebuilt, uniqueSorted.get(i))); - } - if (EXPR_COMPARATOR.compare(rebuilt, expr) == 0) { - return Optional.empty(); - } - return Optional.of(rebuilt); + return maybeCanonicalizeCommutativeCall(navigableExpr, functionName); } if ((functionName.equals(Operator.EQUALS.getFunction()) || functionName.equals(Operator.NOT_EQUALS.getFunction())) && args.size() == 2) { - CelMutableExpr arg0 = args.get(0); - CelMutableExpr arg1 = args.get(1); - if (EXPR_COMPARATOR.compare(arg0, arg1) > 0) { - return Optional.of( - CelMutableExpr.ofCall(expr.id(), CelMutableCall.create(functionName, arg1, arg0))); - } - return Optional.empty(); + return maybeCanonicalizeSymmetricCall(navigableExpr, functionName, args); } if (functionName.equals(Operator.LOGICAL_NOT.getFunction()) && args.size() == 1) { - CelMutableExpr target = args.get(0); - if (isCallWithArgCount(target, Operator.LOGICAL_NOT.getFunction(), 1)) { - return Optional.of(target.call().args().get(0)); - } - if (isCallWithArgCount(target, Operator.LOGICAL_AND.getFunction(), 2)) { - List subArgs = target.call().args(); - return Optional.of( - CelMutableExpr.ofCall( - expr.id(), - CelMutableCall.create( - Operator.LOGICAL_OR.getFunction(), - negate(subArgs.get(0)), - negate(subArgs.get(1))))); - } - if (isCallWithArgCount(target, Operator.LOGICAL_OR.getFunction(), 2)) { - List subArgs = target.call().args(); - return Optional.of( - CelMutableExpr.ofCall( - expr.id(), - CelMutableCall.create( - Operator.LOGICAL_AND.getFunction(), - negate(subArgs.get(0)), - negate(subArgs.get(1))))); - } - if (isCallWithArgCount(target, Operator.EQUALS.getFunction(), 2)) { - List subArgs = target.call().args(); - return Optional.of( - CelMutableExpr.ofCall( - expr.id(), - CelMutableCall.create( - Operator.NOT_EQUALS.getFunction(), subArgs.get(0), subArgs.get(1)))); - } - if (isCallWithArgCount(target, Operator.NOT_EQUALS.getFunction(), 2)) { - List subArgs = target.call().args(); - return Optional.of( - CelMutableExpr.ofCall( - expr.id(), - CelMutableCall.create( - Operator.EQUALS.getFunction(), subArgs.get(0), subArgs.get(1)))); - } - if (target.getKind() == Kind.COMPREHENSION) { - CelMutableComprehension comp = target.comprehension(); - if (isExistsMacro(mutableAst, target.id(), comp)) { - return negateComprehension(mutableAst, target.id(), comp, true); - } else if (isAllMacro(mutableAst, target.id(), comp)) { - return negateComprehension(mutableAst, target.id(), comp, false); - } + return maybeCanonicalizeLogicalNot(mutableAst, expr.id(), args.get(0)); + } + + return Optional.empty(); + } + + private static Optional maybeCanonicalizeCommutativeCall( + CelNavigableMutableExpr navigableExpr, String functionName) { + // TODO: Consider supporting associative/commutative reassociation for arithmetic + // operators (+, *) + List navigableOperands = + flattenNavigableOperands(navigableExpr, functionName); + if (navigableOperands.stream() + .anyMatch(op -> AccuVarSafetyChecker.containsEnclosingAccuVar(op, navigableExpr))) { + return Optional.empty(); + } + List operands = new ArrayList<>(); + for (CelNavigableMutableExpr navOp : navigableOperands) { + operands.add(navOp.expr()); + } + operands.sort(AstComparator.INSTANCE); + List uniqueSorted = new ArrayList<>(); + for (CelMutableExpr op : operands) { + if (uniqueSorted.isEmpty() + || AstComparator.INSTANCE.compare(op, Iterables.getLast(uniqueSorted)) != 0) { + uniqueSorted.add(op); } } + CelMutableExpr rebuilt = uniqueSorted.get(0); + for (int i = 1; i < uniqueSorted.size(); i++) { + rebuilt = + CelMutableExpr.ofCall( + navigableExpr.id(), + CelMutableCall.create(functionName, rebuilt, uniqueSorted.get(i))); + } + if (AstComparator.INSTANCE.compare(rebuilt, navigableExpr.expr()) == 0) { + return Optional.empty(); + } + return Optional.of(rebuilt); + } + private static Optional maybeCanonicalizeSymmetricCall( + CelNavigableMutableExpr navigableExpr, String functionName, List args) { + CelMutableExpr arg0 = args.get(0); + CelMutableExpr arg1 = args.get(1); + if (AstComparator.INSTANCE.compare(arg0, arg1) > 0) { + return Optional.of( + CelMutableExpr.ofCall( + navigableExpr.id(), CelMutableCall.create(functionName, arg1, arg0))); + } return Optional.empty(); } - private static Optional negateComprehension( - CelMutableAst mutableAst, long compId, CelMutableComprehension comp, boolean isExists) { - CelMutableCall stepCall = comp.loopStep().call(); - CelMutableExpr predicate = getPredicateFromLoopStep(stepCall); - CelMutableExpr newLoopStep = - CelMutableExpr.ofCall( - comp.loopStep().id(), - CelMutableCall.create( - (isExists ? Operator.LOGICAL_AND : Operator.LOGICAL_OR).getFunction(), - CelMutableExpr.ofIdent(comp.accuVar()), - negate(predicate))); - CelMutableExpr newAccuInit = CelMutableExpr.ofConstant(CelConstant.ofValue(isExists)); - CelMutableExpr newLoopCondition = - CelMutableExpr.ofCall( - comp.loopCondition().id(), - CelMutableCall.create( - Operator.NOT_STRICTLY_FALSE.getFunction(), - isExists - ? CelMutableExpr.ofIdent(comp.accuVar()) - : negate(CelMutableExpr.ofIdent(comp.accuVar())))); - CelMutableComprehension newComp = - CelMutableComprehension.create( - comp.iterVar(), - comp.iterVar2(), - comp.iterRange(), - comp.accuVar(), - newAccuInit, - newLoopCondition, - newLoopStep, - comp.result()); - updateMacroCallForQuantifier( - mutableAst, compId, (isExists ? Operator.ALL : Operator.EXISTS).getFunction()); - return Optional.of(CelMutableExpr.ofComprehension(compId, newComp)); + private static Optional maybeCanonicalizeLogicalNot( + CelMutableAst mutableAst, long exprId, CelMutableExpr target) { + if (isCallWithArgCount(target, Operator.LOGICAL_NOT.getFunction(), 1)) { + return Optional.of(target.call().args().get(0)); + } + if (isCallWithArgCount(target, Operator.LOGICAL_AND.getFunction(), 2)) { + List subArgs = target.call().args(); + return Optional.of( + CelMutableExpr.ofCall( + exprId, + CelMutableCall.create( + Operator.LOGICAL_OR.getFunction(), + negate(subArgs.get(0)), + negate(subArgs.get(1))))); + } + if (isCallWithArgCount(target, Operator.LOGICAL_OR.getFunction(), 2)) { + List subArgs = target.call().args(); + return Optional.of( + CelMutableExpr.ofCall( + exprId, + CelMutableCall.create( + Operator.LOGICAL_AND.getFunction(), + negate(subArgs.get(0)), + negate(subArgs.get(1))))); + } + if (isCallWithArgCount(target, Operator.EQUALS.getFunction(), 2)) { + List subArgs = target.call().args(); + return Optional.of( + CelMutableExpr.ofCall( + exprId, + CelMutableCall.create( + Operator.NOT_EQUALS.getFunction(), subArgs.get(0), subArgs.get(1)))); + } + if (isCallWithArgCount(target, Operator.NOT_EQUALS.getFunction(), 2)) { + List subArgs = target.call().args(); + return Optional.of( + CelMutableExpr.ofCall( + exprId, + CelMutableCall.create( + Operator.EQUALS.getFunction(), subArgs.get(0), subArgs.get(1)))); + } + return QuantifierDeMorganRewriter.maybeRewrite(mutableAst, target); } private static CelMutableExpr negate(CelMutableExpr expr) { @@ -495,39 +265,6 @@ private static CelMutableExpr negate(CelMutableExpr expr) { expr.id(), CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), expr)); } - private static void updateMacroCallForQuantifier( - CelMutableAst mutableAst, long compId, String newFunctionName) { - if (!mutableAst.source().getMacroCalls().containsKey(compId)) { - return; - } - CelMutableExpr macroCall = mutableAst.source().getMacroCalls().get(compId); - if (macroCall.getKind() != Kind.CALL) { - throw new IllegalStateException( - "Expected macro call to be of kind CALL, but got: " + macroCall.getKind()); - } - CelMutableCall call = macroCall.call(); - if (call.args().size() < 2) { - throw new IllegalStateException( - "Expected macro call to have at least 2 arguments, but got: " + call.args().size()); - } - CelMutableExpr predicateArg = Iterables.getLast(call.args()); - CelMutableExpr notPredicate; - if (isCallWithArgCount(predicateArg, Operator.LOGICAL_NOT.getFunction(), 1)) { - notPredicate = predicateArg.call().args().get(0); - } else { - notPredicate = - CelMutableExpr.ofCall( - 0, CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), predicateArg)); - } - List newArgs = new ArrayList<>(call.args()); - newArgs.set(newArgs.size() - 1, notPredicate); - CelMutableCall newCall = - call.target().isPresent() - ? CelMutableCall.create(call.target().get(), newFunctionName, newArgs) - : CelMutableCall.create(newFunctionName, newArgs); - mutableAst.source().addMacroCalls(compId, CelMutableExpr.ofCall(macroCall.id(), newCall)); - } - private static List flattenNavigableOperands( CelNavigableMutableExpr expr, String functionName) { List result = new ArrayList<>(); @@ -550,89 +287,436 @@ private static void flattenNavigableOperandsRec( result.add(expr); } - private static CelMutableExpr getPredicateFromLoopStep(CelMutableCall stepCall) { - return stepCall.args().get(1); + private static boolean isCallWithArgCount( + CelMutableExpr expr, String functionName, int argCount) { + return expr.getKind() == Kind.CALL + && expr.call().function().equals(functionName) + && expr.call().args().size() == argCount; } - private static boolean isExistsMacro( - CelMutableAst mutableAst, long compId, CelMutableComprehension comp) { - return isStandardMacroCall(mutableAst, compId, Operator.EXISTS.getFunction()) - && isBooleanAccuInit(comp, false) - && isNotStrictlyFalseLoopCondition(comp, true) - && isLoopStepWithAccuVar(comp, Operator.LOGICAL_OR.getFunction()); - } + /** Total ordering comparator for CEL mutable AST expressions. */ + private static final class AstComparator implements Comparator { + private static final AstComparator INSTANCE = new AstComparator(); - private static boolean isAllMacro( - CelMutableAst mutableAst, long compId, CelMutableComprehension comp) { - return isStandardMacroCall(mutableAst, compId, Operator.ALL.getFunction()) - && isBooleanAccuInit(comp, true) - && isNotStrictlyFalseLoopCondition(comp, false) - && isLoopStepWithAccuVar(comp, Operator.LOGICAL_AND.getFunction()); - } + @Override + public int compare(CelMutableExpr e1, CelMutableExpr e2) { + int kindCmp = Integer.compare(getKindPriority(e1.getKind()), getKindPriority(e2.getKind())); + if (kindCmp != 0) { + return kindCmp; + } + switch (e1.getKind()) { + case CONSTANT: + return compareConstants(e1.constant(), e2.constant()); + case IDENT: + return e1.ident().name().compareTo(e2.ident().name()); + case SELECT: + return compareSelect(e1.select(), e2.select()); + case CALL: + return compareCall(e1.call(), e2.call()); + case LIST: + return compareList(e1.list().elements(), e2.list().elements()); + case MAP: + return compareMap(e1.map(), e2.map()); + case STRUCT: + return compareStruct(e1.struct(), e2.struct()); + case COMPREHENSION: + return compareComprehension(e1.comprehension(), e2.comprehension()); + case NOT_SET: + return 0; + } + throw new UnsupportedOperationException("Unsupported expression kind: " + e1.getKind()); + } - private static boolean isStandardMacroCall( - CelMutableAst mutableAst, long compId, String expectedMacroFunction) { - if (!mutableAst.source().getMacroCalls().containsKey(compId)) { - return true; + private static int compareConstants(CelConstant c1, CelConstant c2) { + int constKindCmp = c1.getKind().name().compareTo(c2.getKind().name()); + if (constKindCmp != 0) { + return constKindCmp; + } + switch (c1.getKind()) { + case NULL_VALUE: + case NOT_SET: + return 0; + case BOOLEAN_VALUE: + return Boolean.compare(c1.booleanValue(), c2.booleanValue()); + case INT64_VALUE: + return Long.compare(c1.int64Value(), c2.int64Value()); + case UINT64_VALUE: + return c1.uint64Value().compareTo(c2.uint64Value()); + case DOUBLE_VALUE: + return Double.compare(c1.doubleValue(), c2.doubleValue()); + case STRING_VALUE: + return c1.stringValue().compareTo(c2.stringValue()); + case BYTES_VALUE: + return CelByteString.unsignedLexicographicalComparator() + .compare(c1.bytesValue(), c2.bytesValue()); + default: + throw new UnsupportedOperationException("Unsupported constant kind: " + c1.getKind()); + } + } + + private int compareSelect(CelMutableSelect s1, CelMutableSelect s2) { + return ComparisonChain.start() + .compare(s1.operand(), s2.operand(), this) + .compare(s1.field(), s2.field()) + .compareFalseFirst(s1.testOnly(), s2.testOnly()) + .result(); + } + + private int compareCall(CelMutableCall c1, CelMutableCall c2) { + int fnCmp = c1.function().compareTo(c2.function()); + if (fnCmp != 0) { + return fnCmp; + } + boolean hasT1 = c1.target().isPresent(); + boolean hasT2 = c2.target().isPresent(); + if (hasT1 != hasT2) { + return Boolean.compare(hasT1, hasT2); + } + if (hasT1) { + int tCmp = compare(c1.target().get(), c2.target().get()); + if (tCmp != 0) { + return tCmp; + } + } + return compareList(c1.args(), c2.args()); + } + + private int compareMap(CelMutableMap m1, CelMutableMap m2) { + int mapSizeCmp = Integer.compare(m1.entries().size(), m2.entries().size()); + if (mapSizeCmp != 0) { + return mapSizeCmp; + } + Iterator it2 = m2.entries().iterator(); + for (CelMutableMap.Entry entry1 : m1.entries()) { + CelMutableMap.Entry entry2 = it2.next(); + int cmp = + ComparisonChain.start() + .compare(entry1.key(), entry2.key(), this) + .compare(entry1.value(), entry2.value(), this) + .result(); + if (cmp != 0) { + return cmp; + } + } + return 0; + } + + private int compareStruct(CelMutableStruct s1, CelMutableStruct s2) { + int msgCmp = s1.messageName().compareTo(s2.messageName()); + if (msgCmp != 0) { + return msgCmp; + } + int structSizeCmp = Integer.compare(s1.entries().size(), s2.entries().size()); + if (structSizeCmp != 0) { + return structSizeCmp; + } + Iterator it2 = s2.entries().iterator(); + for (CelMutableStruct.Entry entry1 : s1.entries()) { + CelMutableStruct.Entry entry2 = it2.next(); + int cmp = + ComparisonChain.start() + .compare(entry1.fieldKey(), entry2.fieldKey()) + .compare(entry1.value(), entry2.value(), this) + .result(); + if (cmp != 0) { + return cmp; + } + } + return 0; + } + + private int compareComprehension(CelMutableComprehension c1, CelMutableComprehension c2) { + return ComparisonChain.start() + .compare(c1.iterVar(), c2.iterVar()) + .compare(c1.iterVar2(), c2.iterVar2()) + .compare(c1.accuVar(), c2.accuVar()) + .compare(c1.iterRange(), c2.iterRange(), this) + .compare(c1.accuInit(), c2.accuInit(), this) + .compare(c1.loopCondition(), c2.loopCondition(), this) + .compare(c1.loopStep(), c2.loopStep(), this) + .compare(c1.result(), c2.result(), this) + .result(); + } + + private int compareList(List l1, List l2) { + int sizeCmp = Integer.compare(l1.size(), l2.size()); + if (sizeCmp != 0) { + return sizeCmp; + } + Iterator it2 = l2.iterator(); + for (CelMutableExpr elem1 : l1) { + int cmp = compare(elem1, it2.next()); + if (cmp != 0) { + return cmp; + } + } + return 0; } - CelMutableExpr macroCall = mutableAst.source().getMacroCalls().get(compId); - return macroCall.getKind() == Kind.CALL - && macroCall.call().function().equals(expectedMacroFunction); - } - private static boolean isBooleanAccuInit(CelMutableComprehension comp, boolean expectedValue) { - return comp.accuInit().getKind() == Kind.CONSTANT - && comp.accuInit().constant().getKind() == CelConstant.Kind.BOOLEAN_VALUE - && comp.accuInit().constant().booleanValue() == expectedValue; + private static int getKindPriority(Kind kind) { + switch (kind) { + case IDENT: + return 1; + case SELECT: + return 2; + case CALL: + return 3; + case LIST: + return 4; + case MAP: + return 5; + case STRUCT: + return 6; + case COMPREHENSION: + return 7; + case CONSTANT: + return 8; + default: + return 99; + } + } } - private static boolean isNotStrictlyFalseLoopCondition( - CelMutableComprehension comp, boolean expectNot) { - if (comp.loopCondition().getKind() != Kind.CALL) { - throw new IllegalStateException( - "Expected comprehension loopCondition to be a CALL, but got: " - + comp.loopCondition().getKind()); - } - CelMutableCall call = comp.loopCondition().call(); - if (!call.function().equals(Operator.NOT_STRICTLY_FALSE.getFunction()) - && !call.function().equals(Operator.OLD_NOT_STRICTLY_FALSE.getFunction())) { - throw new IllegalStateException( - "Expected comprehension loopCondition to be @not_strictly_false, but got: " - + call.function()); - } - if (call.args().size() != 1) { - throw new IllegalStateException( - "Expected @not_strictly_false to have exactly 1 argument, but got: " - + call.args().size()); - } - CelMutableExpr arg = call.args().get(0); - if (expectNot) { - if (!isCallWithArgCount(arg, Operator.LOGICAL_NOT.getFunction(), 1)) { + /** + * Safety analyzer for verifying whether an operand contains references to enclosing comprehension + * accumulator variables. + */ + private static final class AccuVarSafetyChecker { + + static boolean containsEnclosingAccuVar( + CelNavigableMutableExpr operand, CelNavigableMutableExpr contextExpr) { + List enclosingAccuVars = collectEnclosingAccuVars(contextExpr); + if (enclosingAccuVars.isEmpty()) { return false; } - arg = arg.call().args().get(0); + return operand + .allNodes() + .filter(node -> node.getKind() == Kind.IDENT) + .anyMatch(identNode -> referencesEnclosingAccuVar(identNode, operand, enclosingAccuVars)); } - return isIdent(arg, comp.accuVar()); - } - private static boolean isLoopStepWithAccuVar( - CelMutableComprehension comp, String expectedFunction) { - if (!isCallWithArgCount(comp.loopStep(), expectedFunction, 2)) { + private static List collectEnclosingAccuVars(CelNavigableMutableExpr contextExpr) { + List accuVars = new ArrayList<>(); + CelNavigableMutableExpr curr = contextExpr; + Optional maybeParent = curr.parent(); + while (maybeParent.isPresent()) { + CelNavigableMutableExpr parent = maybeParent.get(); + if (parent.getKind() == Kind.COMPREHENSION) { + CelMutableComprehension comp = parent.expr().comprehension(); + long currId = curr.id(); + if ((currId == comp.loopCondition().id() || currId == comp.loopStep().id()) + && !comp.accuVar().isEmpty()) { + accuVars.add(comp.accuVar()); + } + } + curr = parent; + maybeParent = parent.parent(); + } + return accuVars; + } + + private static boolean referencesEnclosingAccuVar( + CelNavigableMutableExpr identNode, + CelNavigableMutableExpr operandRoot, + List enclosingAccuVars) { + String name = identNode.expr().ident().name(); + if (!enclosingAccuVars.contains(name)) { + return false; + } + return !isAccuVarShadowed(identNode, operandRoot, name); + } + + private static boolean isAccuVarShadowed( + CelNavigableMutableExpr identNode, + CelNavigableMutableExpr operandRoot, + String accuVarName) { + CelNavigableMutableExpr curr = identNode; + while (curr.id() != operandRoot.id()) { + Optional nextParent = curr.parent(); + if (!nextParent.isPresent()) { + break; + } + CelNavigableMutableExpr parent = nextParent.get(); + if (parent.getKind() == Kind.COMPREHENSION) { + CelMutableComprehension comp = parent.expr().comprehension(); + if (comp.accuVar().equals(accuVarName) + && curr.id() != comp.iterRange().id() + && curr.id() != comp.accuInit().id()) { + return true; + } + } + curr = parent; + } return false; } - List args = comp.loopStep().call().args(); - return isIdent(args.get(0), comp.accuVar()) || isIdent(args.get(1), comp.accuVar()); } - private static boolean isIdent(CelMutableExpr expr, String name) { - return expr.getKind() == Kind.IDENT && expr.ident().name().equals(name); - } + /** + * Rewriter for De Morgan quantifier dualities over single-variable and two-variable + * comprehensions. + */ + private static final class QuantifierDeMorganRewriter { - private static boolean isCallWithArgCount( - CelMutableExpr expr, String functionName, int argCount) { - return expr.getKind() == Kind.CALL - && expr.call().function().equals(functionName) - && expr.call().args().size() == argCount; + static Optional maybeRewrite( + CelMutableAst mutableAst, CelMutableExpr notTargetExpr) { + if (notTargetExpr.getKind() != Kind.COMPREHENSION) { + return Optional.empty(); + } + CelMutableComprehension comp = notTargetExpr.comprehension(); + long compId = notTargetExpr.id(); + if (isExistsMacro(mutableAst, compId, comp)) { + return Optional.of(negateComprehension(mutableAst, compId, comp, /* isExists= */ true)); + } else if (isAllMacro(mutableAst, compId, comp)) { + return Optional.of(negateComprehension(mutableAst, compId, comp, /* isExists= */ false)); + } + return Optional.empty(); + } + + private static CelMutableExpr negateComprehension( + CelMutableAst mutableAst, long compId, CelMutableComprehension comp, boolean isExists) { + CelMutableCall stepCall = comp.loopStep().call(); + CelMutableExpr predicate = getPredicateFromLoopStep(stepCall); + CelMutableExpr newLoopStep = + CelMutableExpr.ofCall( + comp.loopStep().id(), + CelMutableCall.create( + (isExists ? Operator.LOGICAL_AND : Operator.LOGICAL_OR).getFunction(), + CelMutableExpr.ofIdent(comp.accuVar()), + negate(predicate))); + CelMutableExpr newAccuInit = CelMutableExpr.ofConstant(CelConstant.ofValue(isExists)); + CelMutableExpr newLoopCondition = + CelMutableExpr.ofCall( + comp.loopCondition().id(), + CelMutableCall.create( + Operator.NOT_STRICTLY_FALSE.getFunction(), + isExists + ? CelMutableExpr.ofIdent(comp.accuVar()) + : negate(CelMutableExpr.ofIdent(comp.accuVar())))); + CelMutableComprehension newComp = + CelMutableComprehension.create( + comp.iterVar(), + comp.iterVar2(), + comp.iterRange(), + comp.accuVar(), + newAccuInit, + newLoopCondition, + newLoopStep, + comp.result()); + updateMacroCallForQuantifier( + mutableAst, compId, (isExists ? Operator.ALL : Operator.EXISTS).getFunction()); + return CelMutableExpr.ofComprehension(compId, newComp); + } + + private static void updateMacroCallForQuantifier( + CelMutableAst mutableAst, long compId, String newFunctionName) { + if (!mutableAst.source().getMacroCalls().containsKey(compId)) { + return; + } + CelMutableExpr macroCall = mutableAst.source().getMacroCalls().get(compId); + if (macroCall.getKind() != Kind.CALL) { + throw new IllegalStateException( + "Expected macro call to be of kind CALL, but got: " + macroCall.getKind()); + } + CelMutableCall call = macroCall.call(); + if (call.args().size() < 2) { + throw new IllegalStateException( + "Expected macro call to have at least 2 arguments, but got: " + call.args().size()); + } + CelMutableExpr predicateArg = Iterables.getLast(call.args()); + CelMutableExpr notPredicate; + if (isCallWithArgCount(predicateArg, Operator.LOGICAL_NOT.getFunction(), 1)) { + notPredicate = predicateArg.call().args().get(0); + } else { + notPredicate = + CelMutableExpr.ofCall( + 0, CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), predicateArg)); + } + List newArgs = new ArrayList<>(call.args()); + newArgs.set(newArgs.size() - 1, notPredicate); + CelMutableCall newCall = + call.target().isPresent() + ? CelMutableCall.create(call.target().get(), newFunctionName, newArgs) + : CelMutableCall.create(newFunctionName, newArgs); + mutableAst.source().addMacroCalls(compId, CelMutableExpr.ofCall(macroCall.id(), newCall)); + } + + private static CelMutableExpr getPredicateFromLoopStep(CelMutableCall stepCall) { + return stepCall.args().get(1); + } + + private static boolean isExistsMacro( + CelMutableAst mutableAst, long compId, CelMutableComprehension comp) { + return isStandardMacroCall(mutableAst, compId, Operator.EXISTS.getFunction()) + && isBooleanAccuInit(comp, false) + && isNotStrictlyFalseLoopCondition(comp, true) + && isLoopStepWithAccuVar(comp, Operator.LOGICAL_OR.getFunction()); + } + + private static boolean isAllMacro( + CelMutableAst mutableAst, long compId, CelMutableComprehension comp) { + return isStandardMacroCall(mutableAst, compId, Operator.ALL.getFunction()) + && isBooleanAccuInit(comp, true) + && isNotStrictlyFalseLoopCondition(comp, false) + && isLoopStepWithAccuVar(comp, Operator.LOGICAL_AND.getFunction()); + } + + private static boolean isStandardMacroCall( + CelMutableAst mutableAst, long compId, String expectedMacroFunction) { + if (!mutableAst.source().getMacroCalls().containsKey(compId)) { + return true; + } + CelMutableExpr macroCall = mutableAst.source().getMacroCalls().get(compId); + return macroCall.getKind() == Kind.CALL + && macroCall.call().function().equals(expectedMacroFunction); + } + + private static boolean isBooleanAccuInit(CelMutableComprehension comp, boolean expectedValue) { + return comp.accuInit().getKind() == Kind.CONSTANT + && comp.accuInit().constant().getKind() == CelConstant.Kind.BOOLEAN_VALUE + && comp.accuInit().constant().booleanValue() == expectedValue; + } + + private static boolean isNotStrictlyFalseLoopCondition( + CelMutableComprehension comp, boolean expectNot) { + if (comp.loopCondition().getKind() != Kind.CALL) { + throw new IllegalStateException( + "Expected comprehension loopCondition to be a CALL, but got: " + + comp.loopCondition().getKind()); + } + CelMutableCall call = comp.loopCondition().call(); + if (!call.function().equals(Operator.NOT_STRICTLY_FALSE.getFunction()) + && !call.function().equals(Operator.OLD_NOT_STRICTLY_FALSE.getFunction())) { + throw new IllegalStateException( + "Expected comprehension loopCondition to be @not_strictly_false, but got: " + + call.function()); + } + if (call.args().size() != 1) { + throw new IllegalStateException( + "Expected @not_strictly_false to have exactly 1 argument, but got: " + + call.args().size()); + } + CelMutableExpr arg = call.args().get(0); + if (expectNot) { + if (!isCallWithArgCount(arg, Operator.LOGICAL_NOT.getFunction(), 1)) { + return false; + } + arg = arg.call().args().get(0); + } + return isIdent(arg, comp.accuVar()); + } + + private static boolean isLoopStepWithAccuVar( + CelMutableComprehension comp, String expectedFunction) { + if (!isCallWithArgCount(comp.loopStep(), expectedFunction, 2)) { + return false; + } + List args = comp.loopStep().call().args(); + return isIdent(args.get(0), comp.accuVar()) || isIdent(args.get(1), comp.accuVar()); + } + + private static boolean isIdent(CelMutableExpr expr, String name) { + return expr.getKind() == Kind.IDENT && expr.ident().name().equals(name); + } } /** Options to configure how Canonicalization behaves. */ diff --git a/verifier/src/test/java/dev/cel/verifier/CanonicalizationOptimizerTest.java b/verifier/src/test/java/dev/cel/verifier/CanonicalizationOptimizerTest.java index ead53d46f..4b494e666 100644 --- a/verifier/src/test/java/dev/cel/verifier/CanonicalizationOptimizerTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CanonicalizationOptimizerTest.java @@ -27,6 +27,7 @@ import dev.cel.common.CelContainer; import dev.cel.common.CelMutableAst; import dev.cel.common.CelOptions; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; import dev.cel.common.ast.CelMutableExpr; import dev.cel.common.ast.CelMutableExpr.CelMutableCall; import dev.cel.common.types.ListType; @@ -464,7 +465,58 @@ private enum CanonicalizationTestCase { IDENT_INEQUALITY_SYMMETRY( "dyn_b != dyn_a || dyn_d != dyn_c", "dyn_a != dyn_b || dyn_c != dyn_d"), IDENT_SAME_NAME_DIFFERENT_OPERATORS( - "dyn_a != dyn_b && dyn_a == dyn_b", "dyn_a != dyn_b && dyn_a == dyn_b"); + "dyn_a != dyn_b && dyn_a == dyn_b", "dyn_a != dyn_b && dyn_a == dyn_b"), + + // Comprehension Sorting & Structure Comparison (iterRange, accuInit, loopStep, iterVar2) + COMPREHENSIONS_DIFFERENT_ITER_RANGE_EQUALITY( + "[2, 3].all(x, x > 0) == [1, 2].all(x, x > 0)", + "[1, 2].all(x, x > 0) == [2, 3].all(x, x > 0)"), + COMPREHENSIONS_DIFFERENT_ITER_RANGE_AND( + "[2, 3].all(x, x > 0) && [1, 2].all(x, x > 0)", + "[1, 2].all(x, x > 0) && [2, 3].all(x, x > 0)"), + COMPREHENSIONS_REVERSED_LIST_ITER_RANGE_AND( + "[2, 1].all(x, x > 0) && [1, 2].all(x, x > 0)", + "[1, 2].all(x, x > 0) && [2, 1].all(x, x > 0)"), + COMPREHENSIONS_DIFFERENT_PREDICATES_AND( + "[1, 2].all(x, x > 10) && [1, 2].all(x, x > 0)", + "[1, 2].all(x, x > 0) && [1, 2].all(x, x > 10)"), + COMPREHENSIONS_REVERSED_LIST_DIFFERENT_PREDICATES_AND( + "[2, 1].all(x, x > 10) && [2, 1].all(x, x > 0)", + "[2, 1].all(x, x > 0) && [2, 1].all(x, x > 10)"), + COMPREHENSIONS_EXISTS_VS_ALL_AND( + "[1, 2].all(x, x == 1) && [1, 2].exists(x, x == 1)", + "[1, 2].exists(x, x == 1) && [1, 2].all(x, x == 1)"), + COMPREHENSIONS_REVERSED_LIST_EXISTS_VS_ALL_AND( + "[2, 1].all(x, x == 1) && [2, 1].exists(x, x == 1)", + "[2, 1].exists(x, x == 1) && [2, 1].all(x, x == 1)"), + COMPREHENSIONS_ONE_VAR_VS_TWO_VAR_AND( + "string_int_map.all(k, v, v > 0) && string_int_map.all(k, k == 'a')", + "string_int_map.all(k, k == \"a\") && string_int_map.all(k, v, v > 0)"), + + // Macro Scope Coverage (filter, map, exists_one, optMap, optFlatMap) + FILTER_MACRO_PREDICATE_ORDER( + "int_list.filter(x, x > 10 && x > 0)", "int_list.filter(x, x > 0 && x > 10)"), + MAP_MACRO_PREDICATE_ORDER( + "int_list.map(x, x == 2 && x == 1)", "int_list.map(x, x == 1 && x == 2)"), + EXISTS_ONE_MACRO_PREDICATE_ORDER( + "int_list.exists_one(x, x > 10 && x > 0)", "int_list.exists_one(x, x > 0 && x > 10)"), + OPT_MAP_MACRO_PREDICATE_ORDER( + "optional.of(int_var).optMap(x, x == 2 && x == 1)", + "optional.of(int_var).optMap(x, x == 1 && x == 2)"), + OPT_FLAT_MAP_MACRO_PREDICATE_ORDER( + "optional.of(int_var).optFlatMap(x, optional.of(x == 2 && x == 1))", + "optional.of(int_var).optFlatMap(x, optional.of(x == 1 && x == 2))"), + + // Literal & Constant Comparator Branches + CONST_UINT_SYMMETRIC_EQUALITY("20u == 10u", "10u == 20u"), + CONST_DOUBLE_SYMMETRIC_EQUALITY("2.5 == 1.5", "1.5 == 2.5"), + CONST_BYTES_SYMMETRIC_EQUALITY( + "b'xyz' == b'abc'", "b\"\\141\\142\\143\" == b\"\\170\\171\\172\""), + MAP_DIFFERENT_KEYS_EQUALITY("{'b': 1} == {'a': 1}", "{\"a\": 1} == {\"b\": 1}"), + MAP_DIFFERENT_VALUES_EQUALITY("{'a': 2} == {'a': 1}", "{\"a\": 1} == {\"a\": 2}"), + LIST_DIFFERENT_ELEMENTS_EQUALITY("[2, 1] == [1, 2]", "[1, 2] == [2, 1]"), + SELECT_DIFFERENT_FIELDS_EQUALITY( + "msg2.single_int64 == msg.single_int64", "msg.single_int64 == msg2.single_int64"); private final String input; private final String expected; @@ -563,4 +615,34 @@ public void optimize_customMacroWithExistsStructure_notCanonicalized() throws Ex .optimizedAst(); assertThat(UNPARSER.unparse(optimizedAst)).isEqualTo("!int_list.my_custom_exists(e, e == 1)"); } + + @Test + public void optimize_comprehensionWithoutMacroCalls_deMorganSucceeds() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("!int_list.exists(e, e == 1)").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + mutableAst.source().getMacroCalls().clear(); + + CelAbstractSyntaxTree optimizedAst = + CanonicalizationOptimizer.newInstance(CanonicalizationOptions.newBuilder().build()) + .optimize(mutableAst.toParsedAst(), CEL) + .optimizedAst(); + assertThat(optimizedAst.getExpr().getKind()).isEqualTo(Kind.COMPREHENSION); + assertThat(optimizedAst.getExpr().comprehension().accuInit().constant().booleanValue()) + .isTrue(); + } + + @Test + public void optimize_comprehensionAllWithoutMacroCalls_deMorganSucceeds() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("!int_list.all(e, e == 1)").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + mutableAst.source().getMacroCalls().clear(); + + CelAbstractSyntaxTree optimizedAst = + CanonicalizationOptimizer.newInstance(CanonicalizationOptions.newBuilder().build()) + .optimize(mutableAst.toParsedAst(), CEL) + .optimizedAst(); + assertThat(optimizedAst.getExpr().getKind()).isEqualTo(Kind.COMPREHENSION); + assertThat(optimizedAst.getExpr().comprehension().accuInit().constant().booleanValue()) + .isFalse(); + } } From 271671457a94eba3efa93096890f03916b80054e Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Fri, 7 Aug 2026 16:41:34 -0700 Subject: [PATCH 168/204] Add a helper function for finding the declaring comprehension from an identifier PiperOrigin-RevId: 961182715 --- .../navigation/CelNavigableExprUtil.java | 84 +++++++---- .../navigation/CelNavigableExprUtilTest.java | 130 ++++++++++++++++++ 2 files changed, 186 insertions(+), 28 deletions(-) diff --git a/common/src/main/java/dev/cel/common/navigation/CelNavigableExprUtil.java b/common/src/main/java/dev/cel/common/navigation/CelNavigableExprUtil.java index 9229214eb..788158b7c 100644 --- a/common/src/main/java/dev/cel/common/navigation/CelNavigableExprUtil.java +++ b/common/src/main/java/dev/cel/common/navigation/CelNavigableExprUtil.java @@ -27,6 +27,58 @@ @CheckReturnValue public final class CelNavigableExprUtil { + /** + * Returns the nearest enclosing comprehension that declares {@code variableName} in scope for + * {@code expr}, or {@code Optional.empty()} if none exists. + * + *

A comprehension declares {@code variableName} in scope for {@code expr} if {@code + * variableName} matches {@code iterVar}, {@code iterVar2}, or {@code accuVar}, and {@code expr} + * resides within the branch where that variable is active: + * + *

    + *
  • In {@code loopCondition} and {@code loopStep}: {@code iterVar}, {@code iterVar2}, and + * {@code accuVar} are in scope. + *
  • In {@code result}: only {@code accuVar} is in scope. + *
  • In {@code iterRange} and {@code accuInit}: none of the comprehension variables are in + * scope. + *
+ */ + @SuppressWarnings("ReferenceEquality") // Disambiguates mutable child branches + public static > + Optional findDeclaringComprehension(T expr, String variableName) { + checkNotNull(expr); + checkNotNull(variableName); + if (variableName.isEmpty()) { + return Optional.empty(); + } + T curr = expr; + Optional maybeParent = curr.parent(); + while (maybeParent.isPresent()) { + T parent = maybeParent.get(); + if (parent.getKind() == Kind.COMPREHENSION) { + Expression.Comprehension comp = parent.expr().comprehension(); + Expression currExpr = curr.expr(); + + if (currExpr != comp.iterRange() && currExpr != comp.accuInit()) { + if (currExpr == comp.result()) { + if (comp.accuVar().equals(variableName)) { + return Optional.of(parent); + } + } else { + if (comp.iterVar().equals(variableName) + || comp.iterVar2().equals(variableName) + || comp.accuVar().equals(variableName)) { + return Optional.of(parent); + } + } + } + } + curr = parent; + maybeParent = parent.parent(); + } + return Optional.empty(); + } + /** * Returns true if {@code variableName} is in scope and shadowed by an enclosing comprehension * above {@code expr}. @@ -56,7 +108,7 @@ public final class CelNavigableExprUtil { * */ public static boolean isVariableShadowed(BaseNavigableExpr expr, String variableName) { - return areVariablesShadowed(expr, Collections.singleton(variableName)); + return findDeclaringComprehension(expr, variableName).isPresent(); } /** @@ -72,38 +124,14 @@ public static boolean isVariableShadowed(BaseNavigableExpr expr, String varia * At {@code y > 0}, {@code areVariablesShadowed(node, ImmutableSet.of("x", "z"))} is {@code true} * because {@code x} is in scope from the outer comprehension. */ - @SuppressWarnings("ReferenceEquality") // Required to disambiguate child branches public static boolean areVariablesShadowed( BaseNavigableExpr expr, Collection variableNames) { checkNotNull(expr); checkNotNull(variableNames); - if (variableNames.isEmpty()) { - return false; - } - BaseNavigableExpr curr = expr; - Optional> maybeParent = curr.parent(); - while (maybeParent.isPresent()) { - BaseNavigableExpr parent = maybeParent.get(); - if (parent.getKind() == Kind.COMPREHENSION) { - Expression.Comprehension comp = parent.expr().comprehension(); - Expression currExpr = curr.expr(); - - if (currExpr != comp.iterRange() && currExpr != comp.accuInit()) { - if (currExpr == comp.result()) { - if (variableNames.contains(comp.accuVar())) { - return true; - } - } else { - if (variableNames.contains(comp.iterVar()) - || variableNames.contains(comp.iterVar2()) - || variableNames.contains(comp.accuVar())) { - return true; - } - } - } + for (String varName : variableNames) { + if (findDeclaringComprehension(expr, varName).isPresent()) { + return true; } - curr = parent; - maybeParent = parent.parent(); } return false; } diff --git a/common/src/test/java/dev/cel/common/navigation/CelNavigableExprUtilTest.java b/common/src/test/java/dev/cel/common/navigation/CelNavigableExprUtilTest.java index 56e06d187..88eda90f2 100644 --- a/common/src/test/java/dev/cel/common/navigation/CelNavigableExprUtilTest.java +++ b/common/src/test/java/dev/cel/common/navigation/CelNavigableExprUtilTest.java @@ -195,6 +195,76 @@ public void isVariableShadowed_comprehensionResultBranch() throws Exception { .isFalse(); } + @Test + public void isVariableShadowed_twoVarComprehension_resultBranch() throws Exception { + CelAbstractSyntaxTree ast = + COMPILER.compile("{'k1': 1, 'k2': 2}.all(k, v, k != '' && v > 0)").getAst(); + CelNavigableMutableAst navigableAst = + CelNavigableMutableAst.fromAst(CelMutableAst.fromCelAst(ast)); + + CelNavigableMutableExpr comprehensionNode = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.getKind() == Kind.COMPREHENSION) + .findFirst() + .get(); + + CelMutableComprehension comprehension = comprehensionNode.expr().comprehension(); + long resultId = comprehension.result().id(); + + CelNavigableMutableExpr resultNode = + comprehensionNode.allNodes().filter(node -> node.id() == resultId).findFirst().get(); + + // In result branch of two-var comprehension: accuVar is in scope, but iterVar and iterVar2 are + // not + assertThat(CelNavigableExprUtil.isVariableShadowed(resultNode, comprehension.accuVar())) + .isTrue(); + assertThat(CelNavigableExprUtil.isVariableShadowed(resultNode, comprehension.iterVar())) + .isFalse(); + assertThat(CelNavigableExprUtil.isVariableShadowed(resultNode, comprehension.iterVar2())) + .isFalse(); + } + + @Test + public void isVariableShadowed_accuInit_notShadowed() { + CelMutableExpr iterRange = CelMutableExpr.ofList(0, CelMutableList.create()); + CelMutableExpr accuInitIdent = CelMutableExpr.ofIdent(1, "x"); + CelMutableExpr loopCond = CelMutableExpr.ofConstant(2, CelConstant.ofValue(true)); + CelMutableExpr loopStep = CelMutableExpr.ofConstant(3, CelConstant.ofValue(true)); + CelMutableExpr result = CelMutableExpr.ofIdent(4, "accu"); + + CelMutableExpr comp = + CelMutableExpr.ofComprehension( + 5, + CelMutableComprehension.create( + "x", iterRange, "accu", accuInitIdent, loopCond, loopStep, result)); + + CelNavigableMutableExpr root = CelNavigableMutableExpr.fromExpr(comp); + CelNavigableMutableExpr navAccuInit = + root.allNodes().filter(node -> node.id() == 1).findFirst().get(); + + assertThat(CelNavigableExprUtil.isVariableShadowed(navAccuInit, "x")).isFalse(); + assertThat(CelNavigableExprUtil.isVariableShadowed(navAccuInit, "accu")).isFalse(); + } + + @Test + public void findDeclaringComprehension_emptyVariableName_returnsEmpty() throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile("[1, 2].all(x, x > 0)").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + CelNavigableExpr identX = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.expr().identOrDefault().name().equals("x")) + .findFirst() + .get(); + + assertThat(CelNavigableExprUtil.findDeclaringComprehension(identX, "")).isEmpty(); + assertThat(CelNavigableExprUtil.isVariableShadowed(identX, "")).isFalse(); + } + @Test public void areVariablesShadowed_multipleVariables() throws Exception { CelAbstractSyntaxTree ast = COMPILER.compile("[1, 2].all(x, x > 0)").getAst(); @@ -345,4 +415,64 @@ public void isVariableShadowed_zeroedOutIds_scopedCorrectly() { assertThat(CelNavigableExprUtil.isVariableShadowed(navResult, "x")).isFalse(); assertThat(CelNavigableExprUtil.isVariableShadowed(navResult, "accu")).isTrue(); } + + @Test + public void + findDeclaringComprehension_nestedComprehensions_resolvesToInnermostDeclaringComprehension() + throws Exception { + CelAbstractSyntaxTree ast = + COMPILER + .compile("[1, 2].all(x, {'k': 1}.exists(k, v, x > 0 && k != '' && v > 0))") + .getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + CelNavigableExpr outerComp = + navigableAst + .getRoot() + .allNodes() + .filter( + node -> + node.getKind() == Kind.COMPREHENSION + && node.expr().comprehension().iterVar().equals("x")) + .findFirst() + .get(); + + CelNavigableExpr innerComp = + navigableAst + .getRoot() + .allNodes() + .filter( + node -> + node.getKind() == Kind.COMPREHENSION + && node.expr().comprehension().iterVar().equals("k")) + .findFirst() + .get(); + + CelNavigableExpr identX = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.expr().identOrDefault().name().equals("x")) + .findFirst() + .get(); + CelNavigableExpr identK = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.expr().identOrDefault().name().equals("k")) + .findFirst() + .get(); + CelNavigableExpr identV = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.expr().identOrDefault().name().equals("v")) + .findFirst() + .get(); + + assertThat(CelNavigableExprUtil.findDeclaringComprehension(identX, "x")).hasValue(outerComp); + assertThat(CelNavigableExprUtil.findDeclaringComprehension(identK, "k")).hasValue(innerComp); + assertThat(CelNavigableExprUtil.findDeclaringComprehension(identV, "v")).hasValue(innerComp); + assertThat(CelNavigableExprUtil.findDeclaringComprehension(identX, "unknown")).isEmpty(); + } } From d86bfbebc13da76506161a892112539152432d91 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Fri, 7 Aug 2026 16:52:56 -0700 Subject: [PATCH 169/204] Add canonicalization for two-variable comprehensions PiperOrigin-RevId: 961186694 --- .../main/java/dev/cel/verifier/BUILD.bazel | 1 + .../verifier/CanonicalizationOptimizer.java | 293 ++++++++++++------ .../dev/cel/verifier/CelAstAlphaHasher.java | 4 +- .../cel/verifier/CelAstToZ3Translator.java | 25 +- .../CanonicalizationOptimizerTest.java | 27 +- .../cel/verifier/CelVerifierZ3ImplTest.java | 60 +++- 6 files changed, 306 insertions(+), 104 deletions(-) diff --git a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel index e7f19fa1b..3396b6df4 100644 --- a/verifier/src/main/java/dev/cel/verifier/BUILD.bazel +++ b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel @@ -140,6 +140,7 @@ java_library( "//common/ast", "//common/ast:mutable_expr", "//common/navigation:common", + "//common/navigation:expr_util", "//common/navigation:mutable_navigation", "//common/values:cel_byte_string", "//optimizer:ast_optimizer", diff --git a/verifier/src/main/java/dev/cel/verifier/CanonicalizationOptimizer.java b/verifier/src/main/java/dev/cel/verifier/CanonicalizationOptimizer.java index c21558323..5e2a0a8ec 100644 --- a/verifier/src/main/java/dev/cel/verifier/CanonicalizationOptimizer.java +++ b/verifier/src/main/java/dev/cel/verifier/CanonicalizationOptimizer.java @@ -14,11 +14,14 @@ package dev.cel.verifier; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableMap.toImmutableMap; import com.google.auto.value.AutoValue; import com.google.common.collect.ComparisonChain; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import dev.cel.bundle.Cel; import dev.cel.common.CelAbstractSyntaxTree; @@ -33,6 +36,7 @@ import dev.cel.common.ast.CelMutableExpr.CelMutableMap; import dev.cel.common.ast.CelMutableExpr.CelMutableSelect; import dev.cel.common.ast.CelMutableExpr.CelMutableStruct; +import dev.cel.common.navigation.CelNavigableExprUtil; import dev.cel.common.navigation.CelNavigableMutableAst; import dev.cel.common.navigation.CelNavigableMutableExpr; import dev.cel.common.navigation.TraversalOrder; @@ -46,6 +50,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import org.jspecify.annotations.Nullable; /** * Standalone AST canonicalization pass that normalizes commutative operator ordering and De Morgan @@ -81,12 +86,8 @@ static CanonicalizationOptimizer newInstance(CanonicalizationOptions canonicaliz @Override public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) { CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); - mutableAst = runCanonicalizationLoop(mutableAst); - for (Map.Entry entry : - new HashMap<>(mutableAst.source().getMacroCalls()).entrySet()) { - CelMutableExpr canonicalMacro = canonicalize(entry.getValue()); - mutableAst.source().addMacroCalls(entry.getKey(), canonicalMacro); - } + mutableAst = runCanonicalizationLoop(mutableAst, CanonicalizationScope.EMPTY); + canonicalizeMacroCalls(mutableAst); CelAbstractSyntaxTree optimizedAst = AstMutator.newInstance(canonicalizationOptions.maxIterationLimit()) .renumberIdsConsecutively(mutableAst) @@ -94,14 +95,42 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) { return OptimizationResult.create(optimizedAst); } + private void canonicalizeMacroCalls(CelMutableAst mutableAst) { + if (mutableAst.source().getMacroCalls().isEmpty()) { + return; + } + CelNavigableMutableAst navigableAst = CelNavigableMutableAst.fromAst(mutableAst); + ImmutableMap comprehensionNodesById = + navigableAst + .getRoot() + .allNodes() + .filter(n -> n.getKind() == Kind.COMPREHENSION) + .collect(toImmutableMap(CelNavigableMutableExpr::id, n -> n)); + + for (Map.Entry entry : + new HashMap<>(mutableAst.source().getMacroCalls()).entrySet()) { + long compId = entry.getKey(); + CelNavigableMutableExpr compNode = comprehensionNodesById.get(compId); + CanonicalizationScope compScope = CanonicalizationScope.EMPTY; + if (compNode != null) { + compScope = + CanonicalizationScope.fromNavigableExpr(compNode, CanonicalizationScope.EMPTY) + .forComprehensionLoop(compNode.expr().comprehension()); + } + CelMutableExpr canonicalMacro = canonicalize(entry.getValue(), compScope); + mutableAst.source().addMacroCalls(entry.getKey(), canonicalMacro); + } + } + /** Canonicalizes a single CelMutableExpr subtree. */ - private CelMutableExpr canonicalize(CelMutableExpr root) { + private CelMutableExpr canonicalize(CelMutableExpr root, CanonicalizationScope baseScope) { CelMutableAst mutableAst = CelMutableAst.of(root, CelMutableSource.newInstance()); - mutableAst = runCanonicalizationLoop(mutableAst); + mutableAst = runCanonicalizationLoop(mutableAst, baseScope); return mutableAst.expr(); } - private CelMutableAst runCanonicalizationLoop(CelMutableAst mutableAst) { + private CelMutableAst runCanonicalizationLoop( + CelMutableAst mutableAst, CanonicalizationScope baseScope) { AstMutator astMutator = AstMutator.newInstance(canonicalizationOptions.maxIterationLimit()); int iterCount = 0; boolean continueCanonicalizing = true; @@ -120,7 +149,7 @@ private CelMutableAst runCanonicalizationLoop(CelMutableAst mutableAst) { .collect(toImmutableList()); for (CelNavigableMutableExpr candidate : candidateExprs) { iterCount++; - Optional newExpr = maybeCanonicalize(mutableAst, candidate); + Optional newExpr = maybeCanonicalize(mutableAst, candidate, baseScope); if (newExpr.isPresent()) { continueCanonicalizing = true; mutableAst = astMutator.replaceSubtree(mutableAst, newExpr.get(), candidate.id()); @@ -141,7 +170,9 @@ private static boolean canCanonicalize(CelNavigableMutableExpr navigable) { } private static Optional maybeCanonicalize( - CelMutableAst mutableAst, CelNavigableMutableExpr navigableExpr) { + CelMutableAst mutableAst, + CelNavigableMutableExpr navigableExpr, + CanonicalizationScope baseScope) { CelMutableExpr expr = navigableExpr.expr(); if (expr.getKind() != Kind.CALL) { return Optional.empty(); @@ -153,24 +184,24 @@ private static Optional maybeCanonicalize( if ((functionName.equals(Operator.LOGICAL_AND.getFunction()) || functionName.equals(Operator.LOGICAL_OR.getFunction())) && args.size() == 2) { - return maybeCanonicalizeCommutativeCall(navigableExpr, functionName); + return maybeCanonicalizeCommutativeCall(navigableExpr, functionName, baseScope); } if ((functionName.equals(Operator.EQUALS.getFunction()) || functionName.equals(Operator.NOT_EQUALS.getFunction())) && args.size() == 2) { - return maybeCanonicalizeSymmetricCall(navigableExpr, functionName, args); + return maybeCanonicalizeSymmetricCall(navigableExpr, functionName, args, baseScope); } if (functionName.equals(Operator.LOGICAL_NOT.getFunction()) && args.size() == 1) { - return maybeCanonicalizeLogicalNot(mutableAst, expr.id(), args.get(0)); + return maybeCanonicalizeLogicalNot(mutableAst, args.get(0)); } return Optional.empty(); } private static Optional maybeCanonicalizeCommutativeCall( - CelNavigableMutableExpr navigableExpr, String functionName) { + CelNavigableMutableExpr navigableExpr, String functionName, CanonicalizationScope baseScope) { // TODO: Consider supporting associative/commutative reassociation for arithmetic // operators (+, *) List navigableOperands = @@ -183,11 +214,13 @@ private static Optional maybeCanonicalizeCommutativeCall( for (CelNavigableMutableExpr navOp : navigableOperands) { operands.add(navOp.expr()); } - operands.sort(AstComparator.INSTANCE); + CanonicalizationScope scope = CanonicalizationScope.fromNavigableExpr(navigableExpr, baseScope); + AstComparator scopedComparator = AstComparator.of(scope); + operands.sort(scopedComparator); List uniqueSorted = new ArrayList<>(); for (CelMutableExpr op : operands) { if (uniqueSorted.isEmpty() - || AstComparator.INSTANCE.compare(op, Iterables.getLast(uniqueSorted)) != 0) { + || scopedComparator.compare(op, Iterables.getLast(uniqueSorted)) != 0) { uniqueSorted.add(op); } } @@ -195,29 +228,30 @@ private static Optional maybeCanonicalizeCommutativeCall( for (int i = 1; i < uniqueSorted.size(); i++) { rebuilt = CelMutableExpr.ofCall( - navigableExpr.id(), - CelMutableCall.create(functionName, rebuilt, uniqueSorted.get(i))); + 0, CelMutableCall.create(functionName, rebuilt, uniqueSorted.get(i))); } - if (AstComparator.INSTANCE.compare(rebuilt, navigableExpr.expr()) == 0) { + if (scopedComparator.compare(rebuilt, navigableExpr.expr()) == 0) { return Optional.empty(); } return Optional.of(rebuilt); } private static Optional maybeCanonicalizeSymmetricCall( - CelNavigableMutableExpr navigableExpr, String functionName, List args) { + CelNavigableMutableExpr navigableExpr, + String functionName, + List args, + CanonicalizationScope baseScope) { CelMutableExpr arg0 = args.get(0); CelMutableExpr arg1 = args.get(1); - if (AstComparator.INSTANCE.compare(arg0, arg1) > 0) { - return Optional.of( - CelMutableExpr.ofCall( - navigableExpr.id(), CelMutableCall.create(functionName, arg1, arg0))); + CanonicalizationScope scope = CanonicalizationScope.fromNavigableExpr(navigableExpr, baseScope); + if (AstComparator.of(scope).compare(arg0, arg1) > 0) { + return Optional.of(CelMutableExpr.ofCall(0, CelMutableCall.create(functionName, arg1, arg0))); } return Optional.empty(); } private static Optional maybeCanonicalizeLogicalNot( - CelMutableAst mutableAst, long exprId, CelMutableExpr target) { + CelMutableAst mutableAst, CelMutableExpr target) { if (isCallWithArgCount(target, Operator.LOGICAL_NOT.getFunction(), 1)) { return Optional.of(target.call().args().get(0)); } @@ -225,7 +259,7 @@ private static Optional maybeCanonicalizeLogicalNot( List subArgs = target.call().args(); return Optional.of( CelMutableExpr.ofCall( - exprId, + 0, CelMutableCall.create( Operator.LOGICAL_OR.getFunction(), negate(subArgs.get(0)), @@ -235,7 +269,7 @@ private static Optional maybeCanonicalizeLogicalNot( List subArgs = target.call().args(); return Optional.of( CelMutableExpr.ofCall( - exprId, + 0, CelMutableCall.create( Operator.LOGICAL_AND.getFunction(), negate(subArgs.get(0)), @@ -245,7 +279,7 @@ private static Optional maybeCanonicalizeLogicalNot( List subArgs = target.call().args(); return Optional.of( CelMutableExpr.ofCall( - exprId, + 0, CelMutableCall.create( Operator.NOT_EQUALS.getFunction(), subArgs.get(0), subArgs.get(1)))); } @@ -253,7 +287,7 @@ private static Optional maybeCanonicalizeLogicalNot( List subArgs = target.call().args(); return Optional.of( CelMutableExpr.ofCall( - exprId, + 0, CelMutableCall.create( Operator.EQUALS.getFunction(), subArgs.get(0), subArgs.get(1)))); } @@ -262,7 +296,7 @@ private static Optional maybeCanonicalizeLogicalNot( private static CelMutableExpr negate(CelMutableExpr expr) { return CelMutableExpr.ofCall( - expr.id(), CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), expr)); + 0, CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), expr)); } private static List flattenNavigableOperands( @@ -294,9 +328,85 @@ private static boolean isCallWithArgCount( && expr.call().args().size() == argCount; } + /** + * Immutable lexical scope chain for tracking comprehension binder depths during canonical AST + * ordering. + */ + private static final class CanonicalizationScope { + private static final CanonicalizationScope EMPTY = new CanonicalizationScope("", null); + + private final String varName; + private final @Nullable CanonicalizationScope parent; + + private CanonicalizationScope(String varName, @Nullable CanonicalizationScope parent) { + this.varName = checkNotNull(varName); + this.parent = parent; + } + + CanonicalizationScope push(String varName) { + checkNotNull(varName); + if (varName.isEmpty()) { + return this; + } + return new CanonicalizationScope(varName, this); + } + + CanonicalizationScope forComprehensionLoop(CelMutableComprehension comp) { + return push(comp.iterVar()).push(comp.iterVar2()).push(comp.accuVar()); + } + + CanonicalizationScope forComprehensionResult(CelMutableComprehension comp) { + return push(comp.accuVar()); + } + + int indexOf(String name) { + checkNotNull(name); + int idx = 0; + CanonicalizationScope curr = this; + while (curr != null && curr != EMPTY) { + if (curr.varName.equals(name)) { + return idx; + } + idx++; + curr = curr.parent; + } + return -1; + } + + @SuppressWarnings("ReferenceEquality") // Disambiguates mutable child branches + static CanonicalizationScope fromNavigableExpr( + CelNavigableMutableExpr node, CanonicalizationScope baseScope) { + checkNotNull(node); + checkNotNull(baseScope); + if (!node.parent().isPresent()) { + return baseScope; + } + CelNavigableMutableExpr parent = node.parent().get(); + CanonicalizationScope scope = fromNavigableExpr(parent, baseScope); + if (parent.getKind() == Kind.COMPREHENSION) { + CelMutableComprehension comp = parent.expr().comprehension(); + CelMutableExpr nodeExpr = node.expr(); + if (nodeExpr == comp.loopCondition() || nodeExpr == comp.loopStep()) { + return scope.forComprehensionLoop(comp); + } else if (nodeExpr == comp.result()) { + return scope.forComprehensionResult(comp); + } + } + return scope; + } + } + /** Total ordering comparator for CEL mutable AST expressions. */ private static final class AstComparator implements Comparator { - private static final AstComparator INSTANCE = new AstComparator(); + private final CanonicalizationScope scope; + + private AstComparator(CanonicalizationScope scope) { + this.scope = checkNotNull(scope); + } + + static AstComparator of(CanonicalizationScope scope) { + return new AstComparator(scope); + } @Override public int compare(CelMutableExpr e1, CelMutableExpr e2) { @@ -308,7 +418,7 @@ public int compare(CelMutableExpr e1, CelMutableExpr e2) { case CONSTANT: return compareConstants(e1.constant(), e2.constant()); case IDENT: - return e1.ident().name().compareTo(e2.ident().name()); + return compareIdent(e1.ident().name(), e2.ident().name(), scope); case SELECT: return compareSelect(e1.select(), e2.select()); case CALL: @@ -354,6 +464,21 @@ private static int compareConstants(CelConstant c1, CelConstant c2) { } } + private static int compareIdent(String name1, String name2, CanonicalizationScope scope) { + int bIdx1 = scope.indexOf(name1); + int bIdx2 = scope.indexOf(name2); + if (bIdx1 >= 0 && bIdx2 >= 0) { + return Integer.compare(bIdx2, bIdx1); // Outer/earlier binder first + } + if (bIdx1 >= 0) { + return -1; // Bound variable comes before free variable + } + if (bIdx2 >= 0) { + return 1; // Free variable comes after bound variable + } + return name1.compareTo(name2); + } + private int compareSelect(CelMutableSelect s1, CelMutableSelect s2) { return ComparisonChain.start() .compare(s1.operand(), s2.operand(), this) @@ -426,16 +551,30 @@ private int compareStruct(CelMutableStruct s1, CelMutableStruct s2) { } private int compareComprehension(CelMutableComprehension c1, CelMutableComprehension c2) { - return ComparisonChain.start() - .compare(c1.iterVar(), c2.iterVar()) - .compare(c1.iterVar2(), c2.iterVar2()) - .compare(c1.accuVar(), c2.accuVar()) - .compare(c1.iterRange(), c2.iterRange(), this) - .compare(c1.accuInit(), c2.accuInit(), this) - .compare(c1.loopCondition(), c2.loopCondition(), this) - .compare(c1.loopStep(), c2.loopStep(), this) - .compare(c1.result(), c2.result(), this) - .result(); + int cmp = + ComparisonChain.start() + .compare(c1.iterRange(), c2.iterRange(), this) + .compare(c1.accuInit(), c2.accuInit(), this) + .compareTrueFirst(!c1.accuVar().isEmpty(), !c2.accuVar().isEmpty()) + .compareTrueFirst(!c1.iterVar().isEmpty(), !c2.iterVar().isEmpty()) + .compareFalseFirst(!c1.iterVar2().isEmpty(), !c2.iterVar2().isEmpty()) + .result(); + if (cmp != 0) { + return cmp; + } + + AstComparator loopComparator = AstComparator.of(scope.forComprehensionLoop(c1)); + cmp = loopComparator.compare(c1.loopCondition(), c2.loopCondition()); + if (cmp != 0) { + return cmp; + } + cmp = loopComparator.compare(c1.loopStep(), c2.loopStep()); + if (cmp != 0) { + return cmp; + } + + AstComparator resultComparator = AstComparator.of(scope.forComprehensionResult(c1)); + return resultComparator.compare(c1.result(), c2.result()); } private int compareList(List l1, List l2) { @@ -485,69 +624,45 @@ private static final class AccuVarSafetyChecker { static boolean containsEnclosingAccuVar( CelNavigableMutableExpr operand, CelNavigableMutableExpr contextExpr) { - List enclosingAccuVars = collectEnclosingAccuVars(contextExpr); - if (enclosingAccuVars.isEmpty()) { + List enclosingComprehensions = + collectEnclosingComprehensions(contextExpr); + if (enclosingComprehensions.isEmpty()) { return false; } return operand .allNodes() .filter(node -> node.getKind() == Kind.IDENT) - .anyMatch(identNode -> referencesEnclosingAccuVar(identNode, operand, enclosingAccuVars)); - } - - private static List collectEnclosingAccuVars(CelNavigableMutableExpr contextExpr) { - List accuVars = new ArrayList<>(); + .anyMatch( + identNode -> { + String varName = identNode.expr().ident().name(); + Optional declaringComp = + CelNavigableExprUtil.findDeclaringComprehension(identNode, varName); + return declaringComp.isPresent() + && declaringComp.get().expr().comprehension().accuVar().equals(varName) + && enclosingComprehensions.contains(declaringComp.get()); + }); + } + + @SuppressWarnings("ReferenceEquality") // Disambiguates mutable child branches + private static List collectEnclosingComprehensions( + CelNavigableMutableExpr contextExpr) { + List comps = new ArrayList<>(); CelNavigableMutableExpr curr = contextExpr; Optional maybeParent = curr.parent(); while (maybeParent.isPresent()) { CelNavigableMutableExpr parent = maybeParent.get(); if (parent.getKind() == Kind.COMPREHENSION) { CelMutableComprehension comp = parent.expr().comprehension(); - long currId = curr.id(); - if ((currId == comp.loopCondition().id() || currId == comp.loopStep().id()) + CelMutableExpr currExpr = curr.expr(); + if ((currExpr == comp.loopCondition() || currExpr == comp.loopStep()) && !comp.accuVar().isEmpty()) { - accuVars.add(comp.accuVar()); + comps.add(parent); } } curr = parent; maybeParent = parent.parent(); } - return accuVars; - } - - private static boolean referencesEnclosingAccuVar( - CelNavigableMutableExpr identNode, - CelNavigableMutableExpr operandRoot, - List enclosingAccuVars) { - String name = identNode.expr().ident().name(); - if (!enclosingAccuVars.contains(name)) { - return false; - } - return !isAccuVarShadowed(identNode, operandRoot, name); - } - - private static boolean isAccuVarShadowed( - CelNavigableMutableExpr identNode, - CelNavigableMutableExpr operandRoot, - String accuVarName) { - CelNavigableMutableExpr curr = identNode; - while (curr.id() != operandRoot.id()) { - Optional nextParent = curr.parent(); - if (!nextParent.isPresent()) { - break; - } - CelNavigableMutableExpr parent = nextParent.get(); - if (parent.getKind() == Kind.COMPREHENSION) { - CelMutableComprehension comp = parent.expr().comprehension(); - if (comp.accuVar().equals(accuVarName) - && curr.id() != comp.iterRange().id() - && curr.id() != comp.accuInit().id()) { - return true; - } - } - curr = parent; - } - return false; + return comps; } } diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstAlphaHasher.java b/verifier/src/main/java/dev/cel/verifier/CelAstAlphaHasher.java index a7e2be8b7..31a7e3b2a 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelAstAlphaHasher.java +++ b/verifier/src/main/java/dev/cel/verifier/CelAstAlphaHasher.java @@ -215,9 +215,9 @@ private static final class HasherContext { private static final class Scope { final String varName; - final Scope parent; + final @Nullable Scope parent; - Scope(String varName, Scope parent) { + Scope(String varName, @Nullable Scope parent) { this.varName = varName; this.parent = parent; } diff --git a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java index 3964d68a1..ba63e9693 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java @@ -1202,11 +1202,13 @@ private TranslatedValue reduceAllOrExists( } private static boolean isAllMacro(CelComprehension comp) { - return isBooleanAccuInit(comp, true) && isNotStrictlyFalseLoopCondition(comp); + return isBooleanAccuInit(comp, true) + && isNotStrictlyFalseLoopCondition(comp, /* expectNot= */ false); } private static boolean isExistsMacro(CelComprehension comp) { - return isBooleanAccuInit(comp, false) && isNotStrictlyFalseLoopCondition(comp); + return isBooleanAccuInit(comp, false) + && isNotStrictlyFalseLoopCondition(comp, /* expectNot= */ true); } private static boolean isBooleanAccuInit(CelComprehension comp, boolean expectedValue) { @@ -1214,12 +1216,21 @@ private static boolean isBooleanAccuInit(CelComprehension comp, boolean expected && comp.accuInit().constant().booleanValue() == expectedValue; } - private static boolean isNotStrictlyFalseLoopCondition(CelComprehension comp) { + private static boolean isNotStrictlyFalseLoopCondition(CelComprehension comp, boolean expectNot) { CelExpr.CelCall call = comp.loopCondition().callOrDefault(); - return (call.function().equals(Operator.NOT_STRICTLY_FALSE.getFunction()) - || call.function().equals(Operator.OLD_NOT_STRICTLY_FALSE.getFunction())) - && call.args().size() == 1 - && call.args().get(0).identOrDefault().name().equals(comp.accuVar()); + if (!call.function().equals(Operator.NOT_STRICTLY_FALSE.getFunction()) + && !call.function().equals(Operator.OLD_NOT_STRICTLY_FALSE.getFunction())) { + return false; + } + CelExpr arg = call.args().get(0); + if (expectNot) { + CelExpr.CelCall notCall = arg.callOrDefault(); + if (!notCall.function().equals(Operator.LOGICAL_NOT.getFunction())) { + return false; + } + arg = notCall.args().get(0); + } + return arg.identOrDefault().name().equals(comp.accuVar()); } private BoolExpr createTypeConstraint(Expr val, long exprId, CelAbstractSyntaxTree ast) { diff --git a/verifier/src/test/java/dev/cel/verifier/CanonicalizationOptimizerTest.java b/verifier/src/test/java/dev/cel/verifier/CanonicalizationOptimizerTest.java index 4b494e666..d5f309c6d 100644 --- a/verifier/src/test/java/dev/cel/verifier/CanonicalizationOptimizerTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CanonicalizationOptimizerTest.java @@ -264,9 +264,15 @@ private enum CanonicalizationTestCase { TWO_VAR_EXISTS_COMMUTATIVE_AND( "string_int_map.exists(k, v, v == 1 && k == 'foo')", "string_int_map.exists(k, v, k == \"foo\" && v == 1)"), + TWO_VAR_EXISTS_COMMUTATIVE_AND_REVERSE_ALPHABETICAL_VARS( + "string_int_map.exists(z_key, a_val, a_val == 1 && z_key == 'foo')", + "string_int_map.exists(z_key, a_val, z_key == \"foo\" && a_val == 1)"), TWO_VAR_ALL_COMMUTATIVE_OR( "string_int_map.all(k, v, v == 1 || k == 'foo')", "string_int_map.all(k, v, k == \"foo\" || v == 1)"), + TWO_VAR_ALL_COMMUTATIVE_OR_REVERSE_ALPHABETICAL_VARS( + "string_int_map.all(z_key, a_val, a_val == 1 || z_key == 'foo')", + "string_int_map.all(z_key, a_val, z_key == \"foo\" || a_val == 1)"), TWO_VAR_EXISTS_SYMMETRIC_EQUALITY( "string_int_map.exists(k, v, v == 1)", "string_int_map.exists(k, v, v == 1)"), TWO_VAR_ALL_SYMMETRIC_INEQUALITY( @@ -274,6 +280,9 @@ private enum CanonicalizationTestCase { TWO_VAR_EXISTS_INT_STRING_MAP( "int_string_map.exists(k, v, v == 'bar' && k == 1)", "int_string_map.exists(k, v, k == 1 && v == \"bar\")"), + TWO_VAR_EXISTS_INT_STRING_MAP_REVERSE_ALPHABETICAL_VARS( + "int_string_map.exists(z_key, a_val, a_val == 'bar' && z_key == 1)", + "int_string_map.exists(z_key, a_val, z_key == 1 && a_val == \"bar\")"), TWO_VAR_ALL_INT_STRING_MAP( "!int_string_map.all(k, v, k == 1 || v == 'bar')", "k != 1 && v != \"bar\""), TWO_VAR_EXISTS_LIST_INDEX_VALUE( @@ -286,10 +295,10 @@ private enum CanonicalizationTestCase { "int_list.all(i, v, v == 100 || i == 0)", "int_list.all(i, v, i == 0 || v == 100)"), TWO_VAR_NESTED_COMPREHENSIONS( "string_int_map.exists(k, v, k == 'foo' && int_list.all(i, e, e == v && i == 0))", - "string_int_map.exists(k, v, k == \"foo\" && int_list.all(i, e, e == v && i == 0))"), + "string_int_map.exists(k, v, k == \"foo\" && int_list.all(i, e, v == e && i == 0))"), DE_MORGAN_2VAR_NESTED_COMPREHENSIONS( "string_int_map.exists(k, v, k == 'foo' && !int_list.exists(i, e, e == v))", - "string_int_map.exists(k, v, k == \"foo\" && e != v)"), + "string_int_map.exists(k, v, k == \"foo\" && v != e)"), TWO_VAR_COMPREHENSION_WITH_OPTIONALS( "!string_int_map.exists(k, v, optional.of(v).hasValue() && k == 'foo')", "!optional.of(v).hasValue() || k != \"foo\""), @@ -305,10 +314,10 @@ private enum CanonicalizationTestCase { // Extension Coverage - cel.bind Macro CEL_BIND_COMMUTATIVE_AND( "cel.bind(x, int_var + 10, 1 == x && 2 == int_var2)", - "cel.bind(x, int_var + 10, int_var2 == 2 && x == 1)"), + "cel.bind(x, int_var + 10, x == 1 && int_var2 == 2)"), CEL_BIND_COMMUTATIVE_OR( "cel.bind(x, int_var + 10, 1 == x || 2 == int_var2)", - "cel.bind(x, int_var + 10, int_var2 == 2 || x == 1)"), + "cel.bind(x, int_var + 10, x == 1 || int_var2 == 2)"), CEL_BIND_SYMMETRIC_EQUALITY( "cel.bind(x, int_var + 10, 20 == x)", "cel.bind(x, int_var + 10, x == 20)"), CEL_BIND_NESTED( @@ -316,7 +325,7 @@ private enum CanonicalizationTestCase { "cel.bind(x, int_var + 10, cel.bind(y, int_var2 + 20, x == 1 && y == 2))"), CEL_BIND_DE_MORGAN( "cel.bind(x, int_var == 1, !(2 == int_var2 && x == true))", - "cel.bind(x, int_var == 1, int_var2 != 2 || x != true)"), + "cel.bind(x, int_var == 1, x != true || int_var2 != 2)"), // Nested Lists, Maps, and Structs NESTED_LIST_EQUALITY_SYMMETRY( @@ -492,6 +501,14 @@ private enum CanonicalizationTestCase { COMPREHENSIONS_ONE_VAR_VS_TWO_VAR_AND( "string_int_map.all(k, v, v > 0) && string_int_map.all(k, k == 'a')", "string_int_map.all(k, k == \"a\") && string_int_map.all(k, v, v > 0)"), + COMPREHENSION_BOUND_VS_FREE_VAR_EQUALITY( + "int_list.all(x, int_var == x)", "int_list.all(x, x == int_var)"), + COMPREHENSION_NESTED_OUTER_VS_INNER_BOUND_VAR_EQUALITY( + "[1, 2].all(x, [1, 2].all(y, y == x))", "[1, 2].all(x, [1, 2].all(y, x == y))"), + COMPREHENSION_2VAR_KEY_VS_VAL_BOUND_VAR_EQUALITY( + "{'a': 'b'}.all(k, v, v == k)", "{\"a\": \"b\"}.all(k, v, k == v)"), + COMPREHENSION_2VAR_INDEX_VS_VAL_BOUND_VAR_EQUALITY( + "[1, 2].all(i, v, v == i)", "[1, 2].all(i, v, i == v)"), // Macro Scope Coverage (filter, map, exists_one, optMap, optFlatMap) FILTER_MACRO_PREDICATE_ORDER( diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index 003256e0c..55bc5bccf 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -1996,11 +1996,28 @@ private enum EquivalenceTestCase { CANONICALIZE_MAP_TWO_VAR_ALPHA_RENAME( "string_int_map.exists(k, v, k == 'foo' && v == 1)", "string_int_map.exists(key, val, key == 'foo' && val == 1)"), + CANONICALIZE_MAP_TWO_VAR_ALPHA_RENAME_INVERTED_ORDER( + "string_int_map.exists(k, v, k == 'foo' && v == 1)", + "string_int_map.exists(z_key, a_val, a_val == 1 && z_key == 'foo')"), + CANONICALIZE_MAP_TWO_VAR_ALL_ALPHA_RENAME_PREDICATE_ORDER( + "string_int_map.all(k, v, k == 'foo' || v == 1)", + "string_int_map.all(z_key, a_val, a_val == 1 || z_key == 'foo')"), CANONICALIZE_MAP_TWO_VAR_DE_MORGAN( "!string_int_map.exists(k, v, !(v > 0))", "string_int_map.all(k, v, v > 0)"), CANONICALIZE_LIST_PREDICATE_ORDER( "int_list.all(e, e > 0 && e < 100)", "int_list.all(e, e < 100 && e > 0)"), - CANONICALIZE_LIST_ALPHA_RENAME("int_list.all(e, e > 0)", "int_list.all(elem, elem > 0)"); + CANONICALIZE_LIST_ALPHA_RENAME("int_list.all(e, e > 0)", "int_list.all(elem, elem > 0)"), + CANONICALIZE_LIST_TWO_VAR_PREDICATE_ORDER( + "int_list.exists(i, v, i == 0 && v == 100)", "int_list.exists(i, v, v == 100 && i == 0)"), + CANONICALIZE_LIST_TWO_VAR_ALPHA_RENAME_INVERTED_ORDER( + "int_list.all(i, v, i == 0 || v == 100)", "int_list.all(row, col, col == 100 || row == 0)"), + CANONICALIZE_NESTED_TWO_VAR_COMPREHENSIONS( + "string_int_map.exists(k, v, k == 'foo' && [1, 2].all(i, e, e == v && i == 0))", + "string_int_map.exists(z_key, a_val, [1, 2].all(idx, elem, idx == 0 && elem == a_val)" + + " && z_key == 'foo')"), + CANONICALIZE_CEL_BIND_ALPHA_RENAME_PREDICATE_ORDER( + "cel.bind(a, x + 10, cel.bind(b, x + 20, 2 == b && 1 == a))", + "cel.bind(c, x + 10, cel.bind(d, x + 20, c == 1 && d == 2))"); private final String exprA; private final String exprB; @@ -2979,6 +2996,47 @@ public void verifyEquivalence_zeroUnrollLimit_returnsInconclusive( assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); } + private enum EquivalenceZeroUnrollLimitVerifiedTestCase { + MAP_TWO_VAR_EXISTS_BRANCH_ORDER( + "string_int_map.exists(k, v, k == 'foo' && v == 1)", + "string_int_map.exists(k, v, v == 1 && k == 'foo')"), + MAP_TWO_VAR_EXISTS_RENAME_INVERTED( + "string_int_map.exists(k, v, k == 'foo' && v == 1)", + "string_int_map.exists(z_key, a_val, a_val == 1 && z_key == 'foo')"), + MAP_TWO_VAR_ALL_RENAME_INVERTED( + "string_int_map.all(k, v, k == 'foo' || v == 1)", + "string_int_map.all(z_key, a_val, a_val == 1 || z_key == 'foo')"), + LIST_TWO_VAR_ALL_RENAME_INVERTED( + "int_list.all(i, v, i == 0 || v == 100)", "int_list.all(row, col, col == 100 || row == 0)"), + NESTED_TWO_VAR_DYNAMIC( + "string_int_map.exists(k, v, k == 'foo' && int_list.all(i, e, e == v && i == 0))", + "string_int_map.exists(z_key, a_val, int_list.all(idx, elem, idx == 0 && elem == a_val)" + + " && z_key == 'foo')"); + + final String exprA; + final String exprB; + + EquivalenceZeroUnrollLimitVerifiedTestCase(String exprA, String exprB) { + this.exprA = exprA; + this.exprB = exprB; + } + } + + @Test + public void verifyEquivalence_zeroUnrollLimit_twoVarComprehensions_returnsVerified( + @TestParameter EquivalenceZeroUnrollLimitVerifiedTestCase testCase) throws Exception { + CelAbstractSyntaxTree astA = CEL.compile(testCase.exprA).getAst(); + CelAbstractSyntaxTree astB = CEL.compile(testCase.exprB).getAst(); + + CelVerifier verifier = + CelVerifierFactory.newVerifier(CEL).setComprehensionUnrollLimit(0).build(); + CelVerificationResult result = verifier.verifyEquivalence(astA, astB); + + assertWithMessage(result.message()) + .that(result.status()) + .isEqualTo(VerificationStatus.VERIFIED); + } + @Test public void verifyEquivalence_comprehensionScopeShadowing_returnsInconclusive() throws Exception { CelMacro macro1 = From d8f2fcaf94d356a3749b60c47dbbb1078a4acc23 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Mon, 10 Aug 2026 16:20:00 -0700 Subject: [PATCH 170/204] Add helpers for performing fixed point optimization PiperOrigin-RevId: 962427676 --- .../java/dev/cel/optimizer/AstMutator.java | 221 +++++++++++++++++- .../dev/cel/optimizer/optimizers/BUILD.bazel | 1 + .../optimizers/ConstantFoldingOptimizer.java | 154 ++++++------ .../optimizers/InliningOptimizer.java | 46 ++-- .../optimizers/SubexpressionOptimizer.java | 29 ++- .../dev/cel/optimizer/AstMutatorTest.java | 181 +++++++++++++- .../test/java/dev/cel/optimizer/BUILD.bazel | 1 + .../ConstantFoldingOptimizerTest.java | 17 +- 8 files changed, 499 insertions(+), 151 deletions(-) diff --git a/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java b/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java index 0f428f75a..c2ce7e2e4 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java +++ b/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java @@ -18,17 +18,19 @@ import static java.lang.Math.max; import static java.util.stream.Collectors.toCollection; +import com.google.auto.value.AutoOneOf; import com.google.auto.value.AutoValue; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.HashBasedTable; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Streams; import com.google.common.collect.Table; import com.google.errorprone.annotations.Immutable; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelMutableAst; import dev.cel.common.CelMutableSource; -import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import dev.cel.common.ast.CelExpr.ExprKind; import dev.cel.common.ast.CelExprIdGeneratorFactory; import dev.cel.common.ast.CelExprIdGeneratorFactory.ExprIdGenerator; import dev.cel.common.ast.CelExprIdGeneratorFactory.StableIdGenerator; @@ -49,6 +51,7 @@ import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Optional; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -213,7 +216,7 @@ public MangledComprehensionAst mangleComprehensionIdentifierNames( Predicate comprehensionIdentifierPredicate = x -> true; comprehensionIdentifierPredicate = comprehensionIdentifierPredicate - .and(node -> node.getKind().equals(Kind.COMPREHENSION)) + .and(node -> node.getKind().equals(ExprKind.Kind.COMPREHENSION)) .and(node -> !node.expr().comprehension().iterVar().startsWith(newIterVarPrefix + ":")) .and(node -> !node.expr().comprehension().accuVar().startsWith(newAccuVarPrefix + ":")) .and( @@ -235,7 +238,7 @@ public MangledComprehensionAst mangleComprehensionIdentifierNames( String result = node.expr().comprehension().result().ident().name(); return CelNavigableMutableExpr.fromExpr(node.expr().comprehension().loopStep()) .allNodes() - .filter(subNode -> subNode.getKind().equals(Kind.IDENT)) + .filter(subNode -> subNode.getKind().equals(ExprKind.Kind.IDENT)) .map(subNode -> subNode.expr().ident()) .anyMatch( ident -> @@ -259,7 +262,7 @@ public MangledComprehensionAst mangleComprehensionIdentifierNames( .allNodes() .filter( loopStepNode -> - loopStepNode.getKind().equals(Kind.IDENT) + loopStepNode.getKind().equals(ExprKind.Kind.IDENT) && loopStepNode.expr().ident().name().equals(iterVar)) .map(CelNavigableMutableExpr::id) .findAny(); @@ -269,7 +272,7 @@ public MangledComprehensionAst mangleComprehensionIdentifierNames( .filter( loopStepNode -> !iterVar2.isEmpty() - && loopStepNode.getKind().equals(Kind.IDENT) + && loopStepNode.getKind().equals(ExprKind.Kind.IDENT) && loopStepNode.expr().ident().name().equals(iterVar2)) .map(CelNavigableMutableExpr::id) .findAny(); @@ -406,13 +409,13 @@ private static MangledComprehensionName getMangledComprehensionName( private static int countComprehensionNestingLevel(CelNavigableMutableExpr comprehensionExpr) { return comprehensionExpr .descendants() - .filter(node -> node.getKind().equals(Kind.COMPREHENSION)) + .filter(node -> node.getKind().equals(ExprKind.Kind.COMPREHENSION)) .mapToInt( node -> { int nestedLevel = 1; CelNavigableMutableExpr maybeParent = node.parent().orElse(null); while (maybeParent != null && maybeParent.id() != comprehensionExpr.id()) { - if (maybeParent.getKind().equals(Kind.COMPREHENSION)) { + if (maybeParent.getKind().equals(ExprKind.Kind.COMPREHENSION)) { nestedLevel++; } maybeParent = maybeParent.parent().orElse(null); @@ -552,6 +555,151 @@ public CelMutableAst replaceSubtree( return CelMutableAst.of(mutatedRoot, newAstSource); } + /** + * Replaces a subtree in the given AST with the specified {@link SubtreeReplacement}. + * + *

This operation is intended for AST optimization purposes. + * + *

This is a very dangerous operation. Callers must re-typecheck the mutated AST and + * additionally verify that the resulting AST is semantically valid. + * + *

All expression IDs will be renumbered in a stable manner to ensure there's no ID collision + * between the nodes. The renumbering occurs even if the subtree was not replaced. + * + * @param ast Original AST to mutate. + * @param replacement Subtree replacement containing the target node ID and the new expression or + * AST. + */ + public CelMutableAst replaceSubtree(CelMutableAst ast, SubtreeReplacement replacement) { + Preconditions.checkNotNull(ast); + Preconditions.checkNotNull(replacement); + switch (replacement.replacement().kind()) { + case EXPR: + return replaceSubtree(ast, replacement.replacement().expr(), replacement.exprIdToReplace()); + case AST: + return replaceSubtree(ast, replacement.replacement().ast(), replacement.exprIdToReplace()); + } + throw new IllegalArgumentException( + "Unsupported replacement kind: " + replacement.replacement().kind()); + } + + /** + * Repeatedly applies AST mutations using the provided AST-level rewriter until no further + * replacements match (fixed point reached) or the mutator's iteration limit is exhausted. + * + *

Per iteration pass, the AST is traversed to find and perform at most one matching subtree + * substitution before restarting traversal on the newly mutated AST. + * + *

This operation is intended for AST optimization purposes. + * + *

This is a very dangerous operation. Callers must re-typecheck the mutated AST and + * additionally verify that the resulting AST is semantically valid. + * + *

All expression IDs will be renumbered in a stable manner to ensure there's no ID collision + * between the nodes. + * + * @param ast Initial mutable AST to mutate. + * @param astRewriter Function returning a {@link SubtreeReplacement} or {@code Optional.empty()} + * when no further rewrites are possible. + * @return Mutated {@link CelMutableAst} at fixed point. + * @throws IllegalStateException If the iteration count exceeds {@code iterationLimit}. + */ + public CelMutableAst mutateUntilFixedPoint( + CelMutableAst ast, + Function> astRewriter) { + Preconditions.checkNotNull(ast); + Preconditions.checkNotNull(astRewriter); + CelMutableAst mutableAst = ast; + for (long i = 0; i < iterationLimit; i++) { + CelNavigableMutableAst navAst = CelNavigableMutableAst.fromAst(mutableAst); + Optional replacement = astRewriter.apply(navAst); + if (!replacement.isPresent()) { + return mutableAst; + } + mutableAst = replaceSubtree(mutableAst, replacement.get()); + } + throw new IllegalStateException("Max iteration count reached."); + } + + /** + * Traverses nodes using the specified {@link TraversalOrder} and repeatedly rewrites matching + * subtrees until a fixed point is reached. + * + *

Per iteration pass, the AST is walked in the given order to find and perform the first + * matching substitution. The traversal then restarts on the freshly mutated AST until no nodes + * match or the mutator's iteration limit is exhausted. + * + *

This operation is intended for AST optimization purposes. + * + *

This is a very dangerous operation. Callers must re-typecheck the mutated AST and + * additionally verify that the resulting AST is semantically valid. + * + *

All expression IDs will be renumbered in a stable manner to ensure there's no ID collision + * between the nodes. + * + * @param ast Initial mutable AST to mutate. + * @param traversalOrder Order in which nodes are visited per iteration pass. + * @param nodeRewriter Function returning a {@link SubtreeReplacement} or {@code + * Optional.empty()}. + * @return Mutated {@link CelMutableAst} at fixed point. + * @throws IllegalStateException If the iteration count exceeds {@code iterationLimit}. + */ + public CelMutableAst mutateUntilFixedPoint( + CelMutableAst ast, + TraversalOrder traversalOrder, + Function> nodeRewriter) { + Preconditions.checkNotNull(traversalOrder); + Preconditions.checkNotNull(nodeRewriter); + return mutateUntilFixedPoint( + ast, + navAst -> + navAst + .getRoot() + .allNodes(traversalOrder) + .flatMap(node -> Streams.stream(nodeRewriter.apply(node))) + .findFirst()); + } + + /** + * Traverses nodes using the specified {@link TraversalOrder}, applies the node matcher, and + * substitutes matching nodes with the returned replacement expression (targeting {@code + * node.id()}). + * + *

Per iteration pass, the AST is walked in the given order to find and perform the first + * matching substitution. The traversal then restarts on the freshly mutated AST until no nodes + * match or the mutator's iteration limit is exhausted. + * + *

This operation is intended for AST optimization purposes. + * + *

This is a very dangerous operation. Callers must re-typecheck the mutated AST and + * additionally verify that the resulting AST is semantically valid. + * + *

All expression IDs will be renumbered in a stable manner to ensure there's no ID collision + * between the nodes. + * + * @param ast Initial mutable AST to mutate. + * @param traversalOrder Order in which nodes are visited per iteration pass. + * @param nodeMatcher Predicate to filter candidate nodes. + * @param nodeRewriter Function producing the new {@link CelMutableExpr} for matched nodes. + * @return Mutated {@link CelMutableAst} at fixed point. + * @throws IllegalStateException If the iteration count exceeds {@code iterationLimit}. + */ + public CelMutableAst mutateUntilFixedPoint( + CelMutableAst ast, + TraversalOrder traversalOrder, + Predicate nodeMatcher, + Function> nodeRewriter) { + Preconditions.checkNotNull(nodeMatcher); + Preconditions.checkNotNull(nodeRewriter); + return mutateUntilFixedPoint( + ast, + traversalOrder, + node -> + nodeMatcher.test(node) + ? nodeRewriter.apply(node).map(newExpr -> SubtreeReplacement.of(node.id(), newExpr)) + : Optional.empty()); + } + private CelMutableExpr mangleIdentsInComprehensionExpr( CelMutableExpr root, CelMutableExpr comprehensionExpr, @@ -590,7 +738,7 @@ private void replaceIdentName( .map(CelNavigableMutableExpr::expr) .filter( node -> - node.getKind().equals(Kind.IDENT) + node.getKind().equals(ExprKind.Kind.IDENT) && node.ident().name().equals(originalIdentName)) .findAny() .orElse(null); @@ -776,7 +924,7 @@ private CelMutableSource normalizeMacroSource( long replacedId = idGenerator.generate(exprIdToReplace); boolean isListExprBeingReplaced = allExprs.containsKey(replacedId) - && allExprs.get(replacedId).getKind().equals(Kind.LIST); + && allExprs.get(replacedId).getKind().equals(ExprKind.Kind.LIST); if (isListExprBeingReplaced) { unwrapListArgumentsInMacroCallExpr( allExprs.get(callId).comprehension(), newMacroCallExpr); @@ -791,7 +939,7 @@ private CelMutableSource normalizeMacroSource( CelMutableExpr macroCallExpr = macroCall.getValue(); CelNavigableMutableExpr.fromExpr(macroCallExpr) .allNodes() - .filter(node -> node.getKind().equals(Kind.COMPREHENSION)) + .filter(node -> node.getKind().equals(ExprKind.Kind.COMPREHENSION)) .map(CelNavigableMutableExpr::expr) .forEach( node -> { @@ -808,7 +956,7 @@ private CelMutableSource normalizeMacroSource( // This can occur from pulling out a nested comprehension into a separate cel.block index CelNavigableMutableExpr.fromExpr(macroCallExpr) .allNodes() - .filter(node -> node.getKind().equals(Kind.NOT_SET)) + .filter(node -> node.getKind().equals(ExprKind.Kind.NOT_SET)) .map(CelNavigableMutableExpr::id) .filter(id -> !allExprs.containsKey(id)) .forEach( @@ -840,7 +988,7 @@ private CelMutableSource normalizeMacroSource( private static void unwrapListArgumentsInMacroCallExpr( CelMutableComprehension comprehension, CelMutableExpr newMacroCallExpr) { CelMutableExpr accuInit = comprehension.accuInit(); - if (!accuInit.getKind().equals(Kind.LIST) || !accuInit.list().elements().isEmpty()) { + if (!accuInit.getKind().equals(ExprKind.Kind.LIST) || !accuInit.list().elements().isEmpty()) { // Does not contain an extraneous list. return; } @@ -983,4 +1131,53 @@ private static MangledComprehensionName of( iterVarName, iterVar2Name, resultName); } } + + /** + * Represents a planned subtree replacement containing the target node ID to replace and either a + * {@link CelMutableExpr} or {@link CelMutableAst}. + */ + @AutoValue + public abstract static class SubtreeReplacement { + + public abstract long exprIdToReplace(); + + public abstract Replacement replacement(); + + public static SubtreeReplacement of(long exprIdToReplace, CelMutableExpr replacementExpr) { + return new AutoValue_AstMutator_SubtreeReplacement( + exprIdToReplace, Replacement.ofExpr(replacementExpr)); + } + + public static SubtreeReplacement of(long exprIdToReplace, CelMutableAst replacementAst) { + return new AutoValue_AstMutator_SubtreeReplacement( + exprIdToReplace, Replacement.ofAst(replacementAst)); + } + + /** Discriminated union of either a {@link CelMutableExpr} or a {@link CelMutableAst}. */ + @AutoOneOf(Replacement.Kind.class) + public abstract static class Replacement { + + public abstract CelMutableExpr expr(); + + public abstract CelMutableAst ast(); + + public abstract Replacement.Kind kind(); + + public static Replacement ofExpr(CelMutableExpr expr) { + return AutoOneOf_AstMutator_SubtreeReplacement_Replacement.expr( + Preconditions.checkNotNull(expr)); + } + + public static Replacement ofAst(CelMutableAst ast) { + return AutoOneOf_AstMutator_SubtreeReplacement_Replacement.ast( + Preconditions.checkNotNull(ast)); + } + + /** Kind of {@link Replacement}. */ + public enum Kind { + EXPR, + AST + } + } + } } diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel index 0c4b78826..1012b19c2 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -96,6 +96,7 @@ java_library( "//common:operator", "//common/ast", "//common/ast:mutable_expr", + "//common/navigation:common", "//common/navigation:expr_util", "//common/navigation:mutable_navigation", "//common/types", diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java index 266059426..181cc4f75 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java @@ -55,6 +55,7 @@ import dev.cel.common.values.StructValue; import dev.cel.extensions.CelOptionalLibrary.Function; import dev.cel.optimizer.AstMutator; +import dev.cel.optimizer.AstMutator.SubtreeReplacement; import dev.cel.optimizer.CelAstOptimizer; import dev.cel.optimizer.CelOptimizationException; import dev.cel.runtime.CelAttributePattern; @@ -134,7 +135,15 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) Cel optimizerEnv = builder.setResultType(SimpleType.DYN).build(); CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + ImmutableMap identTypes = precomputeIdentTypes(mutableAst); + mutableAst = foldConstants(optimizerEnv, valueProvider, identTypes, mutableAst); + mutableAst = pruneOptionalElements(mutableAst); + + return OptimizationResult.create(astMutator.renumberIdsConsecutively(mutableAst).toParsedAst()); + } + + private static ImmutableMap precomputeIdentTypes(CelMutableAst mutableAst) { // HACK: The AstMutator strips type metadata during intermediate folds due to ID renumbering. // We pre-compute identifier types from the unmutated AST to safely evaluate boolean conditions // later. @@ -151,58 +160,48 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) Optional type = mutableAst.getType(node.id()); type.ifPresent(celType -> mutableIdentTypes.put(node.expr().ident().name(), celType)); } - ImmutableMap identTypes = ImmutableMap.copyOf(mutableIdentTypes); + return ImmutableMap.copyOf(mutableIdentTypes); + } - int iterCount = 0; - boolean continueFolding = true; - while (continueFolding) { - if (iterCount >= constantFoldingOptions.maxIterationLimit()) { - throw new IllegalStateException("Max iteration count reached."); + private CelMutableAst foldConstants( + Cel optimizerEnv, + @Nullable CelValueProvider valueProvider, + ImmutableMap identTypes, + CelMutableAst mutableAst) + throws CelOptimizationException { + for (int iterCount = 0; iterCount < constantFoldingOptions.maxIterationLimit(); iterCount++) { + Optional replacement = + findNextFoldableSubtree(optimizerEnv, valueProvider, identTypes, mutableAst); + if (!replacement.isPresent()) { + return mutableAst; } - iterCount++; - continueFolding = false; - ImmutableList foldableExprs = - CelNavigableMutableAst.fromAst(mutableAst) - .getRoot() - .allNodes(TraversalOrder.PRE_ORDER) - .filter(this::canFold) - .collect(toImmutableList()); - for (CelNavigableMutableExpr foldableExpr : foldableExprs) { - iterCount++; - - Optional mutatedResult; - // Attempt to prune if it is a non-strict call - mutatedResult = maybePruneBranches(mutableAst, identTypes, foldableExpr.expr()); - if (!mutatedResult.isPresent()) { - // Evaluate the call then fold - try { - mutatedResult = maybeFold(optimizerEnv, valueProvider, mutableAst, foldableExpr); - } catch (CelEvaluationException e) { - throw new CelOptimizationException( - "Constant folding failure. Failed to evaluate subtree due to: " + e.getMessage(), - e); - } - } - - if (!mutatedResult.isPresent()) { - // Skip this expr. It's neither prune-able nor foldable. - continue; - } + mutableAst = astMutator.replaceSubtree(mutableAst, replacement.get()); + } + throw new IllegalStateException("Max iteration count reached."); + } - continueFolding = true; - mutableAst = mutatedResult.get(); - // Break the loop because we mutated the AST. Since we traverse in PRE_ORDER (top-down), - // mutating a parent node means its children are now obsolete or folded. - // We restart the traversal to gather a fresh list of foldable expressions. - break; + private Optional findNextFoldableSubtree( + Cel optimizerEnv, + @Nullable CelValueProvider valueProvider, + ImmutableMap identTypes, + CelMutableAst mutableAst) + throws CelOptimizationException { + CelNavigableMutableAst navAst = CelNavigableMutableAst.fromAst(mutableAst); + Iterator foldableExprs = + navAst.getRoot().allNodes(TraversalOrder.PRE_ORDER).filter(this::canFold).iterator(); + while (foldableExprs.hasNext()) { + CelNavigableMutableExpr foldableExpr = foldableExprs.next(); + Optional pruned = + maybePruneBranches(mutableAst, identTypes, foldableExpr.expr()); + if (pruned.isPresent()) { + return Optional.of(SubtreeReplacement.of(foldableExpr.id(), pruned.get())); + } + Optional folded = maybeFold(optimizerEnv, valueProvider, foldableExpr); + if (folded.isPresent()) { + return Optional.of(SubtreeReplacement.of(foldableExpr.id(), folded.get())); } } - - // If the output is a list, map, or struct which contains optional entries, then prune it - // to make sure that the optionals, if resolved, do not surface in the output literal. - mutableAst = pruneOptionalElements(mutableAst); - - return OptimizationResult.create(astMutator.renumberIdsConsecutively(mutableAst).toParsedAst()); + return Optional.empty(); } private boolean canFold(CelNavigableMutableExpr navigableExpr) { @@ -317,12 +316,9 @@ private static boolean isNestedComprehension(CelNavigableMutableExpr expr) { return false; } - private Optional maybeFold( - Cel cel, - CelValueProvider valueProvider, - CelMutableAst mutableAst, - CelNavigableMutableExpr node) - throws CelOptimizationException, CelEvaluationException { + private Optional maybeFold( + Cel cel, @Nullable CelValueProvider valueProvider, CelNavigableMutableExpr node) + throws CelOptimizationException { if (!node.getKind().equals(Kind.COMPREHENSION) && CelNavigableExprUtil.hasComprehensionVariable(node)) { return Optional.empty(); @@ -330,7 +326,7 @@ private Optional maybeFold( Object result; try { result = evaluateExpr(cel, node); - } catch (CelValidationException e) { + } catch (CelEvaluationException | CelValidationException e) { throw new CelOptimizationException( "Constant folding failure. Failed to evaluate subtree due to: " + e.getMessage(), e); } @@ -340,17 +336,10 @@ private Optional maybeFold( // ex2: optional.ofNonZeroValue(5) -> optional.of(5) if (result instanceof Optional) { Optional optResult = ((Optional) result); - return maybeRewriteOptional( - cel.getTypeProvider(), valueProvider, optResult, mutableAst, node.expr()); - } - - CelMutableExpr adaptedResult = - maybeAdaptEvaluatedResult(cel.getTypeProvider(), valueProvider, result).orElse(null); - if (adaptedResult == null) { - return Optional.empty(); + return maybeRewriteOptional(cel.getTypeProvider(), valueProvider, optResult, node.expr()); } - return Optional.of(astMutator.replaceSubtree(mutableAst, adaptedResult, node.id())); + return maybeAdaptEvaluatedResult(cel.getTypeProvider(), valueProvider, result); } private Optional maybeAdaptEvaluatedResult( @@ -440,11 +429,10 @@ private Optional maybeAdaptEvaluatedResult( return Optional.empty(); } - private Optional maybeRewriteOptional( + private Optional maybeRewriteOptional( CelTypeProvider typeProvider, CelValueProvider valueProvider, Optional optResult, - CelMutableAst mutableAst, CelMutableExpr expr) { Object unwrappedResult = optResult.orElse(null); if (unwrappedResult == null) { @@ -454,7 +442,7 @@ private Optional maybeRewriteOptional( // An empty optional value was encountered. Rewrite the tree with optional.none call. // This is to account for other optional functions returning an empty optional value // e.g: optional.ofNonZeroValue(0) - return Optional.of(astMutator.replaceSubtree(mutableAst, newOptionalNoneExpr(), expr.id())); + return Optional.of(newOptionalNoneExpr()); } if (isCallToFunction(expr, Function.OPTIONAL_OF.getFunction())) { @@ -472,7 +460,7 @@ private Optional maybeRewriteOptional( CelMutableExpr.ofCall( CelMutableCall.create(Function.OPTIONAL_OF.getFunction(), adaptedResult)); - return Optional.of(astMutator.replaceSubtree(mutableAst, newOptionalOfCall, expr.id())); + return Optional.of(newOptionalOfCall); } private static boolean isCallToFunction(CelMutableExpr expr, String functionName) { @@ -480,7 +468,7 @@ private static boolean isCallToFunction(CelMutableExpr expr, String functionName } /** Inspects the non-strict calls to determine whether a branch can be removed. */ - private Optional maybePruneBranches( + private Optional maybePruneBranches( CelMutableAst mutableAst, Map identTypes, CelMutableExpr expr) { if (!expr.getKind().equals(Kind.CALL)) { return Optional.empty(); @@ -501,7 +489,7 @@ private Optional maybePruneBranches( } CelMutableExpr result = cond.constant().booleanValue() ? truthy : falsy; - return Optional.of(astMutator.replaceSubtree(mutableAst, result, expr.id())); + return Optional.of(result); } else if (function.equals(Operator.IN.getFunction())) { CelMutableExpr callArg = call.args().get(1); if (!callArg.getKind().equals(Kind.LIST)) { @@ -510,9 +498,7 @@ private Optional maybePruneBranches( CelMutableList haystack = callArg.list(); if (haystack.elements().isEmpty()) { - return Optional.of( - astMutator.replaceSubtree( - mutableAst, CelMutableExpr.ofConstant(CelConstant.ofValue(false)), expr.id())); + return Optional.of(CelMutableExpr.ofConstant(CelConstant.ofValue(false))); } CelMutableExpr needle = call.args().get(0); @@ -529,11 +515,7 @@ private Optional maybePruneBranches( && Double.isNaN(needle.constant().doubleValue())) { continue; } - return Optional.of( - astMutator.replaceSubtree( - mutableAst.expr(), - CelMutableExpr.ofConstant(CelConstant.ofValue(true)), - expr.id())); + return Optional.of(CelMutableExpr.ofConstant(CelConstant.ofValue(true))); } CelType needleType = @@ -542,11 +524,7 @@ private Optional maybePruneBranches( .orElseGet(() -> identTypes.get(needle.ident().name())); if (needleType != null && isSafeForExactEquality(needleType)) { - return Optional.of( - astMutator.replaceSubtree( - mutableAst.expr(), - CelMutableExpr.ofConstant(CelConstant.ofValue(true)), - expr.id())); + return Optional.of(CelMutableExpr.ofConstant(CelConstant.ofValue(true))); } } } @@ -586,13 +564,13 @@ private Optional maybePruneBranches( CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), lhs))); } - return replacementExpr.map(node -> astMutator.replaceSubtree(mutableAst, node, expr.id())); + return replacementExpr; } return Optional.empty(); } - private Optional maybeShortCircuitCall( + private Optional maybeShortCircuitCall( CelMutableAst mutableAst, Map identTypes, CelMutableExpr expr) { CelMutableCall call = expr.call(); boolean shortCircuit = false; @@ -613,7 +591,7 @@ private Optional maybeShortCircuitCall( } if (arg.constant().booleanValue() == shortCircuit) { - return Optional.of(astMutator.replaceSubtree(mutableAst, arg, expr.id())); + return Optional.of(arg); } } @@ -621,13 +599,13 @@ private Optional maybeShortCircuitCall( if (newArgs.isEmpty()) { CelMutableExpr shortCircuitTarget = call.args().get(0); // either args(0) or args(1) would work here - return Optional.of(astMutator.replaceSubtree(mutableAst, shortCircuitTarget, expr.id())); + return Optional.of(shortCircuitTarget); } if (newArgs.size() == 1) { CelMutableExpr remainingArg = newArgs.get(0); if (!constantFoldingOptions.enableSafeLogicalOptimization() || evaluatesToBoolean(mutableAst, identTypes, remainingArg)) { - return Optional.of(astMutator.replaceSubtree(mutableAst, remainingArg, expr.id())); + return Optional.of(remainingArg); } return Optional.empty(); } @@ -637,7 +615,7 @@ private Optional maybeShortCircuitCall( "Folding variadic logical operator is not supported yet."); } - private boolean evaluatesToBoolean( + private static boolean evaluatesToBoolean( CelMutableAst mutableAst, Map identTypes, CelMutableExpr expr) { if (isExprConstantOfKind(expr, CelConstant.Kind.BOOLEAN_VALUE)) { return true; diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/InliningOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/InliningOptimizer.java index 147673e47..61fd19347 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/InliningOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/InliningOptimizer.java @@ -14,8 +14,6 @@ package dev.cel.optimizer.optimizers; -import static com.google.common.collect.ImmutableList.toImmutableList; - import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import com.google.common.primitives.UnsignedLong; @@ -29,13 +27,14 @@ import dev.cel.common.ast.CelMutableExpr.CelMutableCall; import dev.cel.common.ast.CelMutableExpr.CelMutableStruct; import dev.cel.common.navigation.CelNavigableExprUtil; -import dev.cel.common.navigation.CelNavigableMutableAst; import dev.cel.common.navigation.CelNavigableMutableExpr; +import dev.cel.common.navigation.TraversalOrder; import dev.cel.common.types.CelKind; import dev.cel.common.types.CelType; import dev.cel.common.types.SimpleType; import dev.cel.common.values.NullValue; import dev.cel.optimizer.AstMutator; +import dev.cel.optimizer.AstMutator.SubtreeReplacement; import dev.cel.optimizer.CelAstOptimizer; import java.util.ArrayList; import java.util.List; @@ -105,28 +104,25 @@ public static InliningOptimizer newInstance( public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) { CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); for (InlineVariable inlineVariable : inlineVariables) { - ImmutableList inlinableExprs = - CelNavigableMutableAst.fromAst(mutableAst) - .getRoot() - .allNodes() - .filter(node -> canInline(node, inlineVariable.name())) - .collect(toImmutableList()); - - for (CelNavigableMutableExpr inlinableExpr : inlinableExprs) { - CelMutableAst inlineVariableAst = CelMutableAst.fromCelAst(inlineVariable.ast()); - CelMutableExpr replacementExpr = inlineVariableAst.expr(); - - if (inlinableExpr.getKind().equals(Kind.SELECT) - && inlinableExpr.expr().select().testOnly()) { - replacementExpr = rewritePresenceExpr(inlineVariable, replacementExpr); - } - - mutableAst = - astMutator.replaceSubtree( - mutableAst, - CelMutableAst.of(replacementExpr, inlineVariableAst.source()), - inlinableExpr.id()); - } + mutableAst = + astMutator.mutateUntilFixedPoint( + mutableAst, + TraversalOrder.POST_ORDER, + node -> { + if (!canInline(node, inlineVariable.name())) { + return Optional.empty(); + } + CelMutableAst inlineVariableAst = CelMutableAst.fromCelAst(inlineVariable.ast()); + CelMutableExpr replacementExpr = inlineVariableAst.expr(); + + if (node.getKind().equals(Kind.SELECT) && node.expr().select().testOnly()) { + replacementExpr = rewritePresenceExpr(inlineVariable, replacementExpr); + } + + return Optional.of( + SubtreeReplacement.of( + node.id(), CelMutableAst.of(replacementExpr, inlineVariableAst.source()))); + }); } return OptimizationResult.create(astMutator.renumberIdsConsecutively(mutableAst).toParsedAst()); diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java index 5eebb1c54..6a9860750 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java @@ -65,6 +65,7 @@ import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Stream; @@ -161,27 +162,23 @@ private OptimizationResult optimizeUsingCelBlock(CelAbstractSyntaxTree ast, Cel break; } + CelMutableExpr targetCseShape = normalizeForEquality(cseCandidates.get(0)); subexpressions.add(cseCandidates.get(0)); String blockIdentifier = BLOCK_INDEX_PREFIX + blockIdentifierIndex++; // Replace all CSE candidates with new block index identifier - for (CelMutableExpr cseCandidate : cseCandidates) { - iterCount++; - - astToModify = - astMutator.replaceSubtree( - navAst, - CelNavigableMutableAst.fromAst( - CelMutableAst.of( - CelMutableExpr.ofIdent(blockIdentifier), navAst.getAst().source())), - cseCandidate.id()); - - // Retain the existing macro calls in case if the block identifiers are replacing a subtree - // that contains a comprehension. - sourceToModify.addAllMacroCalls(astToModify.source().getMacroCalls()); - astToModify = CelMutableAst.of(astToModify.expr(), sourceToModify); - } + astToModify = + astMutator.mutateUntilFixedPoint( + astToModify, + TraversalOrder.POST_ORDER, + node -> normalizeForEquality(node.expr()).equals(targetCseShape), + node -> Optional.of(CelMutableExpr.ofIdent(blockIdentifier))); + + // Retain the existing macro calls in case if the block identifiers are replacing a subtree + // that contains a comprehension. + sourceToModify.addAllMacroCalls(astToModify.source().getMacroCalls()); + astToModify = CelMutableAst.of(astToModify.expr(), sourceToModify); } if (iterCount >= cseOptions.iterationLimit()) { diff --git a/optimizer/src/test/java/dev/cel/optimizer/AstMutatorTest.java b/optimizer/src/test/java/dev/cel/optimizer/AstMutatorTest.java index fa896ebca..f0c3a7045 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/AstMutatorTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/AstMutatorTest.java @@ -41,24 +41,31 @@ import dev.cel.common.ast.CelMutableExprConverter; import dev.cel.common.navigation.CelNavigableAst; import dev.cel.common.navigation.CelNavigableExpr; +import dev.cel.common.navigation.TraversalOrder; import dev.cel.common.types.SimpleType; import dev.cel.common.types.StructTypeReference; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.extensions.CelExtensions; import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.optimizer.AstMutator.SubtreeReplacement; import dev.cel.parser.CelStandardMacro; import dev.cel.parser.CelUnparser; import dev.cel.parser.CelUnparserFactory; import java.util.List; +import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) public class AstMutatorTest { private static final Cel CEL = - CelFactory.standardCelBuilder() + CelFactory.plannerCelBuilder() .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .setOptions(CelOptions.current().populateMacroCalls(true).build()) + .setOptions( + CelOptions.current() + .populateMacroCalls(true) + .enableHeterogeneousNumericComparisons(true) + .build()) .addMessageTypes(TestAllTypes.getDescriptor()) .addCompilerLibraries( CelOptionalLibrary.INSTANCE, CelExtensions.bindings(), CelExtensions.comprehensions()) @@ -66,6 +73,7 @@ public class AstMutatorTest { .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) .addVar("x", SimpleType.INT) + .addVar("b", SimpleType.BOOL) .build(); private static final CelUnparser CEL_UNPARSER = CelUnparserFactory.newUnparser(); @@ -440,7 +448,7 @@ public void memberCallExpr_replaceLeafTarget() throws Exception { // 10 [1] func [4] // 4 [3] 5 [5] Cel cel = - CelFactory.standardCelBuilder() + CelFactory.plannerCelBuilder() .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( "func", @@ -463,7 +471,7 @@ public void memberCallExpr_replaceLeafArgument() throws Exception { // 10 [1] func [4] // 4 [3] 5 [5] Cel cel = - CelFactory.standardCelBuilder() + CelFactory.plannerCelBuilder() .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( "func", @@ -486,7 +494,7 @@ public void memberCallExpr_replaceMiddleBranchTarget() throws Exception { // 10 [1] func [4] // 4 [3] 5 [5] Cel cel = - CelFactory.standardCelBuilder() + CelFactory.plannerCelBuilder() .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( "func", @@ -509,7 +517,7 @@ public void memberCallExpr_replaceMiddleBranchArgument() throws Exception { // 10 [1] func [4] // 4 [3] 5 [5] Cel cel = - CelFactory.standardCelBuilder() + CelFactory.plannerCelBuilder() .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( "func", @@ -864,9 +872,13 @@ public void mangleComprehensionVariable_adjacentMacros_differentIterVarTypes() t public void mangleComprehensionVariable_macroSourceDisabled_macroCallMapIsEmpty() throws Exception { Cel cel = - CelFactory.standardCelBuilder() + CelFactory.plannerCelBuilder() .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .setOptions(CelOptions.current().populateMacroCalls(false).build()) + .setOptions( + CelOptions.current() + .populateMacroCalls(false) + .enableHeterogeneousNumericComparisons(true) + .build()) .build(); CelAbstractSyntaxTree ast = cel.compile("[false].exists(i, i)").getAst(); @@ -1044,6 +1056,159 @@ public void newGlobalCallAst_success() throws Exception { .isEqualTo("func([1].exists(x, x >= 1), \"hello\")"); } + @Test + public void replaceSubtree_withSubtreeReplacement_expr() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("1 + 2").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + SubtreeReplacement replacement = + SubtreeReplacement.of(1, CelMutableExpr.ofConstant(CelConstant.ofValue(10))); + + CelMutableAst result = AST_MUTATOR.replaceSubtree(mutableAst, replacement); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())).isEqualTo("10 + 2"); + } + + @Test + public void replaceSubtree_withSubtreeReplacement_ast() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("true && false").getAst(); + CelAbstractSyntaxTree macroAst = CEL.compile("[1].exists(x, x > 0)").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + SubtreeReplacement replacement = SubtreeReplacement.of(3, CelMutableAst.fromCelAst(macroAst)); + + CelMutableAst result = AST_MUTATOR.replaceSubtree(mutableAst, replacement); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())) + .isEqualTo("true && [1].exists(x, x > 0)"); + assertThat(result.source().getMacroCalls()).hasSize(1); + } + + @Test + public void mutateUntilFixedPoint_astRewriter_success() throws Exception { + // Repeatedly simplifies addition with 0: "1 + 0 + 0" -> "1" + CelAbstractSyntaxTree ast = CEL.compile("1 + 0 + 0").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + + CelMutableAst result = + AST_MUTATOR.mutateUntilFixedPoint( + mutableAst, + navAst -> + navAst + .getRoot() + .allNodes() + .filter( + node -> + node.getKind().equals(Kind.CALL) + && node.expr().call().function().equals("_+_")) + .filter( + node -> { + List args = node.expr().call().args(); + return args.get(1).getKind().equals(Kind.CONSTANT) + && args.get(1).constant().int64Value() == 0; + }) + .map(node -> SubtreeReplacement.of(node.id(), node.expr().call().args().get(0))) + .findFirst()); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())).isEqualTo("1"); + } + + @Test + public void mutateUntilFixedPoint_nodeRewriter_success() throws Exception { + // Rewrites nested calls: "func(func(1))" -> "1" + Cel cel = + CelFactory.plannerCelBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "func", + CelOverloadDecl.newGlobalOverload( + "func_overload", SimpleType.INT, SimpleType.INT))) + .build(); + CelAbstractSyntaxTree ast = cel.compile("func(func(10))").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + + CelMutableAst result = + AST_MUTATOR.mutateUntilFixedPoint( + mutableAst, + TraversalOrder.POST_ORDER, + node -> { + if (node.getKind().equals(Kind.CALL) + && node.expr().call().function().equals("func")) { + return Optional.of( + SubtreeReplacement.of(node.id(), node.expr().call().args().get(0))); + } + return Optional.empty(); + }); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())).isEqualTo("10"); + } + + @Test + public void mutateUntilFixedPoint_matcherAndNodeRewriter_success() throws Exception { + // Replaces all variables named 'x' with constant 5 in "x + x + x" -> "5 + 5 + 5" + CelAbstractSyntaxTree ast = CEL.compile("x + x + x").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + + CelMutableAst result = + AST_MUTATOR.mutateUntilFixedPoint( + mutableAst, + TraversalOrder.POST_ORDER, + node -> node.getKind().equals(Kind.IDENT) && node.expr().ident().name().equals("x"), + node -> Optional.of(CelMutableExpr.ofConstant(CelConstant.ofValue(5)))); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())).isEqualTo("5 + 5 + 5"); + } + + @Test + public void mutateUntilFixedPoint_withReplacementAst_preservesMacroSource() throws Exception { + // Replaces identifier 'b' with macro AST in "b && true" + CelAbstractSyntaxTree ast = CEL.compile("b && true").getAst(); + CelAbstractSyntaxTree macroAst = CEL.compile("[1].exists(i, i > 0)").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + + CelMutableAst result = + AST_MUTATOR.mutateUntilFixedPoint( + mutableAst, + navAst -> + navAst + .getRoot() + .allNodes() + .filter( + node -> + node.getKind().equals(Kind.IDENT) + && node.expr().ident().name().equals("b")) + .map( + node -> + SubtreeReplacement.of(node.id(), CelMutableAst.fromCelAst(macroAst))) + .findFirst()); + + assertThat(CEL_UNPARSER.unparse(result.toParsedAst())) + .isEqualTo("[1].exists(i, i > 0) && true"); + assertThat(result.source().getMacroCalls()).hasSize(1); + assertThat(CEL.createProgram(CEL.check(result.toParsedAst()).getAst()).eval()).isEqualTo(true); + } + + @Test + public void mutateUntilFixedPoint_exceedsIterationLimit_throws() throws Exception { + // Circular rewrite rule that alternates between 1 and 2 + CelAbstractSyntaxTree ast = CEL.compile("1").getAst(); + CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + + IllegalStateException e = + assertThrows( + IllegalStateException.class, + () -> + AST_MUTATOR.mutateUntilFixedPoint( + mutableAst, + TraversalOrder.POST_ORDER, + node -> node.getKind().equals(Kind.CONSTANT), + node -> { + long currentVal = node.expr().constant().int64Value(); + long nextVal = currentVal == 1L ? 2L : 1L; + return Optional.of(CelMutableExpr.ofConstant(CelConstant.ofValue(nextVal))); + })); + + assertThat(e).hasMessageThat().isEqualTo("Max iteration count reached."); + } + @Test public void newMemberCallAst_success() throws Exception { CelMutableAst targetAst = CelMutableAst.fromCelAst(CEL.compile("'hello'").getAst()); diff --git a/optimizer/src/test/java/dev/cel/optimizer/BUILD.bazel b/optimizer/src/test/java/dev/cel/optimizer/BUILD.bazel index 8ea72a261..702fe23f3 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/BUILD.bazel +++ b/optimizer/src/test/java/dev/cel/optimizer/BUILD.bazel @@ -21,6 +21,7 @@ java_library( "//common/ast", "//common/ast:mutable_expr", "//common/navigation", + "//common/navigation:common", "//common/types", "//compiler", "//extensions", diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java index 613b53ea3..74b078097 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java @@ -107,6 +107,7 @@ private static Cel setupEnv(CelBuilder celBuilder) { .addVar("bool_var", SimpleType.BOOL) .addVar("list_var", ListType.create(SimpleType.STRING)) .addVar("map_var", MapType.create(SimpleType.STRING, SimpleType.STRING)) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .addFunctionDeclarations( CelFunctionDecl.newFunctionDeclaration( @@ -128,13 +129,13 @@ private static Cel setupEnv(CelBuilder celBuilder) { CelExtensions.comprehensions(), CelExtensions.bindings(), CelOptionalLibrary.INSTANCE, - CelExtensions.math(CEL_OPTIONS), + CelExtensions.math(), CelExtensions.strings(), CelExtensions.sets(CEL_OPTIONS), CelExtensions.encoders(CEL_OPTIONS)) .addRuntimeLibraries( CelOptionalLibrary.INSTANCE, - CelExtensions.math(CEL_OPTIONS), + CelExtensions.math(), CelExtensions.strings(), CelExtensions.sets(CEL_OPTIONS), CelExtensions.encoders(CEL_OPTIONS)) @@ -299,6 +300,12 @@ private static Cel setupEnv(CelBuilder celBuilder) { @TestParameters("{source: 'true == bool_var', expected: 'bool_var'}") @TestParameters("{source: 'bool_var == false', expected: '!bool_var'}") @TestParameters("{source: 'false == bool_var', expected: '!bool_var'}") + @TestParameters( + "{source: 'msg.?single_bool.orValue(false) == true', expected:" + + " 'msg.?single_bool.orValue(false)'}") + @TestParameters( + "{source: 'msg.?single_bool.orValue(false) == false', expected:" + + " '!msg.?single_bool.orValue(false)'}") @TestParameters("{source: 'true == false', expected: 'false'}") @TestParameters("{source: 'true == true', expected: 'true'}") @TestParameters("{source: 'false == true', expected: 'false'}") @@ -326,6 +333,12 @@ private static Cel setupEnv(CelBuilder celBuilder) { @TestParameters("{source: 'true != bool_var', expected: '!bool_var'}") @TestParameters("{source: 'bool_var != false', expected: 'bool_var'}") @TestParameters("{source: 'false != bool_var', expected: 'bool_var'}") + @TestParameters( + "{source: 'msg.?single_bool.orValue(false) != true', expected:" + + " '!msg.?single_bool.orValue(false)'}") + @TestParameters( + "{source: 'msg.?single_bool.orValue(false) != false', expected:" + + " 'msg.?single_bool.orValue(false)'}") @TestParameters("{source: 'true != false', expected: 'true'}") @TestParameters("{source: 'true != true', expected: 'false'}") @TestParameters("{source: 'false != true', expected: 'true'}") From f847b0ce2321a55fb7429211dcc1c4f7760d0dde Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Tue, 11 Aug 2026 12:36:56 -0700 Subject: [PATCH 171/204] Internal Changes PiperOrigin-RevId: 962935900 --- .../java/dev/cel/optimizer/AstMutator.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java b/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java index c2ce7e2e4..ca9fb8bdf 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java +++ b/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java @@ -1139,9 +1139,9 @@ private static MangledComprehensionName of( @AutoValue public abstract static class SubtreeReplacement { - public abstract long exprIdToReplace(); + abstract long exprIdToReplace(); - public abstract Replacement replacement(); + abstract Replacement replacement(); public static SubtreeReplacement of(long exprIdToReplace, CelMutableExpr replacementExpr) { return new AutoValue_AstMutator_SubtreeReplacement( @@ -1155,26 +1155,26 @@ public static SubtreeReplacement of(long exprIdToReplace, CelMutableAst replacem /** Discriminated union of either a {@link CelMutableExpr} or a {@link CelMutableAst}. */ @AutoOneOf(Replacement.Kind.class) - public abstract static class Replacement { + abstract static class Replacement { - public abstract CelMutableExpr expr(); + abstract CelMutableExpr expr(); - public abstract CelMutableAst ast(); + abstract CelMutableAst ast(); - public abstract Replacement.Kind kind(); + abstract Kind kind(); - public static Replacement ofExpr(CelMutableExpr expr) { + static Replacement ofExpr(CelMutableExpr expr) { return AutoOneOf_AstMutator_SubtreeReplacement_Replacement.expr( Preconditions.checkNotNull(expr)); } - public static Replacement ofAst(CelMutableAst ast) { + static Replacement ofAst(CelMutableAst ast) { return AutoOneOf_AstMutator_SubtreeReplacement_Replacement.ast( Preconditions.checkNotNull(ast)); } /** Kind of {@link Replacement}. */ - public enum Kind { + enum Kind { EXPR, AST } From 94ba8ad588fde23bed13c0ff6fc2be8ea8969358 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Tue, 11 Aug 2026 14:02:12 -0700 Subject: [PATCH 172/204] Consolidate emit to output for aggregate policies PiperOrigin-RevId: 962986450 --- .../dev/cel/policy/CelPolicyYamlParser.java | 33 +++---------------- .../cel/policy/CelPolicyCompilerImplTest.java | 18 +++++----- .../cel/policy/CelPolicyYamlParserTest.java | 14 ++------ 3 files changed, 15 insertions(+), 50 deletions(-) diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java b/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java index 09702e77c..8db1d725c 100644 --- a/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java +++ b/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java @@ -298,7 +298,7 @@ public CelPolicy.Rule parseRule( } hasMatch = true; ruleBuilder - .addMatches(parseMatches(ctx, policyBuilder, value, false)) + .addMatches(parseMatches(ctx, policyBuilder, value)) .setSemantic(EvaluationSemantic.FIRST_MATCH); break; case "aggregate": @@ -307,7 +307,7 @@ public CelPolicy.Rule parseRule( } hasAggregate = true; ruleBuilder - .addMatches(parseMatches(ctx, policyBuilder, value, true)) + .addMatches(parseMatches(ctx, policyBuilder, value)) .setSemantic(EvaluationSemantic.AGGREGATE); break; @@ -320,10 +320,7 @@ public CelPolicy.Rule parseRule( } private ImmutableSet parseMatches( - PolicyParserContext ctx, - CelPolicy.Builder policyBuilder, - Node node, - boolean isAggregate) { + PolicyParserContext ctx, CelPolicy.Builder policyBuilder, Node node) { long valueId = ctx.collectMetadata(node); ImmutableSet.Builder matchesBuilder = ImmutableSet.builder(); if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) { @@ -332,7 +329,7 @@ private ImmutableSet parseMatches( SequenceNode matchListNode = (SequenceNode) node; for (Node elementNode : matchListNode.getValue()) { - matchesBuilder.add(parseMatchInternal(ctx, policyBuilder, elementNode, isAggregate)); + matchesBuilder.add(parseMatch(ctx, policyBuilder, elementNode)); } return matchesBuilder.build(); @@ -341,14 +338,6 @@ private ImmutableSet parseMatches( @Override public CelPolicy.Match parseMatch( PolicyParserContext ctx, CelPolicy.Builder policyBuilder, Node node) { - return parseMatchInternal(ctx, policyBuilder, node, false); - } - - private CelPolicy.Match parseMatchInternal( - PolicyParserContext ctx, - CelPolicy.Builder policyBuilder, - Node node, - boolean isAggregate) { long nodeId = ctx.collectMetadata(node); if (!assertYamlType(ctx, nodeId, node, YamlNodeType.MAP)) { return ERROR_MATCH; @@ -369,20 +358,6 @@ private CelPolicy.Match parseMatchInternal( matchBuilder.setCondition(ctx.newSourceString(value)); break; case "output": - if (isAggregate) { - ctx.reportError(tagId, "Rule aggregate requires 'emit' tag instead of 'output'"); - } - matchBuilder - .result() - .filter(result -> result.kind().equals(Match.Result.Kind.RULE)) - .ifPresent( - result -> ctx.reportError(tagId, "Only the rule or the output may be set")); - matchBuilder.setResult(Match.Result.ofOutput(ctx.newSourceString(value))); - break; - case "emit": - if (!isAggregate) { - ctx.reportError(tagId, "Rule match requires 'output' tag instead of 'emit'"); - } matchBuilder .result() .filter(result -> result.kind().equals(Match.Result.Kind.RULE)) diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java index 2e3274912..5f697e0b9 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java @@ -146,9 +146,9 @@ public void evalYamlPolicy_aggregate() throws Exception { + "rule:\n" + " aggregate:\n" + " - condition: 'true'\n" - + " emit: '\"PII\"'\n" + + " output: '\"PII\"'\n" + " - condition: 'true'\n" - + " emit: '\"CONFIDENTIAL\"'\n"; + + " output: '\"CONFIDENTIAL\"'\n"; Cel cel = newCel(); CelPolicy policy = POLICY_PARSER.parse(policySource); @@ -166,11 +166,11 @@ public void evaluateYamlPolicy_aggregate_cseApplied() throws Exception { + "rule:\n" + " aggregate:\n" + " - condition: \"size(resource.payload) > 5\"\n" - + " emit: '\"CSE1\"'\n" + + " output: '\"CSE1\"'\n" + " - condition: \"size(resource.payload) > 5\"\n" - + " emit: '\"CSE2\"'\n" + + " output: '\"CSE2\"'\n" + " - condition: 'true'\n" - + " emit: '\"ALWAYS\"'\n"; + + " output: '\"ALWAYS\"'\n"; Cel cel = newCel() .toCelBuilder() @@ -213,7 +213,7 @@ public void compileYamlPolicy_aggregate_macrosPreserved() throws Exception { + " - condition: \"true\"\n" + " output: \"payload.filter(x, x > 10).exists(y, y % 2 == 0)\"\n" + " - condition: \"true\"\n" - + " emit: \"payload.all(x, x > 0)\"\n"; + + " output: \"payload.all(x, x > 0)\"\n"; Cel cel = newCel() .toCelBuilder() @@ -243,7 +243,7 @@ public void compileYamlPolicy_nestedAggregate_throws() throws Exception { + " rule:\n" + " aggregate:\n" + " - condition: 'true'\n" - + " emit: \"'foo'\"\n"; + + " output: \"'foo'\"\n"; CelPolicy policy = POLICY_PARSER.parse(policySource); CelPolicyValidationException e = @@ -269,7 +269,7 @@ public void compileYamlPolicy_nestedAggregate_withInterveningMatch_throws() thro + " rule:\n" + " aggregate:\n" + " - condition: 'true'\n" - + " emit: \"'foo'\"\n"; + + " output: \"'foo'\"\n"; CelPolicy policy = POLICY_PARSER.parse(policySource); CelPolicyValidationException e = @@ -292,7 +292,7 @@ public void compileYamlPolicy_aggregateUnderMatch_success() throws Exception { + " rule:\n" + " aggregate:\n" + " - condition: 'true'\n" - + " emit: \"'foo'\"\n"; + + " output: \"'foo'\"\n"; CelPolicy policy = POLICY_PARSER.parse(policySource); CelAbstractSyntaxTree ast = diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java index dfb483981..a881afb03 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java @@ -334,7 +334,7 @@ private enum PolicyParseErrorTestCase { + " match:\n" + " - output: 'true'\n" + " aggregate:\n" - + " - emit: 'true'\n", + + " - output: 'true'\n", "ERROR: :5:3: Only one of 'match' or 'aggregate' may be set in a rule\n" + " | aggregate:\n" + " | ..^"), @@ -342,22 +342,12 @@ private enum PolicyParseErrorTestCase { "name: test\n" + "rule:\n" + " aggregate:\n" - + " - emit: 'true'\n" + + " - output: 'true'\n" + " match:\n" + " - output: 'true'\n", "ERROR: :5:3: Only one of 'match' or 'aggregate' may be set in a rule\n" + " | match:\n" + " | ..^"), - AGGREGATE_RULE_USES_OUTPUT( - "name: test\n" + "rule:\n" + " aggregate:\n" + " - output: 'true'\n", - "ERROR: :4:7: Rule aggregate requires 'emit' tag instead of 'output'\n" - + " | - output: 'true'\n" - + " | ......^"), - MATCH_RULE_USES_EMIT( - "name: test\n" + "rule:\n" + " match:\n" + " - emit: 'true'\n", - "ERROR: :4:7: Rule match requires 'output' tag instead of 'emit'\n" - + " | - emit: 'true'\n" - + " | ......^"), ILLEGAL_YAML_TYPE_ON_RULE_VALUE( "rule: illegal", "ERROR: :1:7: Got yaml node type tag:yaml.org,2002:str, wanted type(s)" From 30f8e6db9acdeaf260cfc34eea297612b38caa26 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Wed, 12 Aug 2026 13:28:25 -0700 Subject: [PATCH 173/204] Add a validation pass for ID uniqueness in optimizers PiperOrigin-RevId: 963628515 --- .../main/java/dev/cel/optimizer/BUILD.bazel | 4 + .../cel/optimizer/CelOptimizerFactory.java | 32 ++- .../dev/cel/optimizer/CelOptimizerImpl.java | 80 +++++++- .../cel/optimizer/CelOptimizerOptions.java | 51 +++++ .../optimizer/CelOptimizerFactoryTest.java | 36 ++++ .../cel/optimizer/CelOptimizerImplTest.java | 188 +++++++++++++++++- 6 files changed, 383 insertions(+), 8 deletions(-) create mode 100644 optimizer/src/main/java/dev/cel/optimizer/CelOptimizerOptions.java diff --git a/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel b/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel index e9e8994a2..22dab14f4 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel +++ b/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel @@ -32,12 +32,14 @@ java_library( srcs = [ "CelOptimizer.java", "CelOptimizerBuilder.java", + "CelOptimizerOptions.java", ], tags = [ ], deps = [ ":ast_optimizer", ":optimization_exception", + "//:auto_value", "//common:cel_ast", "@maven//:com_google_errorprone_error_prone_annotations", ], @@ -57,6 +59,8 @@ java_library( "//bundle:cel", "//common:cel_ast", "//common:compiler_common", + "//common/ast", + "//common/navigation", "@maven//:com_google_guava_guava", ], ) diff --git a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerFactory.java b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerFactory.java index 1ebfd293e..d82403825 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerFactory.java +++ b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerFactory.java @@ -25,22 +25,48 @@ /** Factory class for constructing an {@link CelOptimizer} instance. */ public final class CelOptimizerFactory { + private static final CelOptimizerOptions DEFAULT_OPTIMIZER_OPTIONS = + CelOptimizerOptions.newBuilder().build(); + /** Create a new builder for constructing a {@link CelOptimizer} instance. */ public static CelOptimizerBuilder standardCelOptimizerBuilder(Cel cel) { - return CelOptimizerImpl.newBuilder(cel); + return standardCelOptimizerBuilder(cel, DEFAULT_OPTIMIZER_OPTIONS); + } + + /** Create a new builder for constructing a {@link CelOptimizer} instance with custom options. */ + public static CelOptimizerBuilder standardCelOptimizerBuilder( + Cel cel, CelOptimizerOptions optimizerOptions) { + return CelOptimizerImpl.newBuilder(cel, optimizerOptions); } /** Create a new builder for constructing a {@link CelOptimizer} instance. */ public static CelOptimizerBuilder standardCelOptimizerBuilder( CelCompiler celCompiler, CelRuntime celRuntime) { - return standardCelOptimizerBuilder(CelFactory.combine(celCompiler, celRuntime)); + return standardCelOptimizerBuilder(celCompiler, celRuntime, DEFAULT_OPTIMIZER_OPTIONS); + } + + /** Create a new builder for constructing a {@link CelOptimizer} instance with custom options. */ + public static CelOptimizerBuilder standardCelOptimizerBuilder( + CelCompiler celCompiler, CelRuntime celRuntime, CelOptimizerOptions optimizerOptions) { + return standardCelOptimizerBuilder( + CelFactory.combine(celCompiler, celRuntime), optimizerOptions); } /** Create a new builder for constructing a {@link CelOptimizer} instance. */ public static CelOptimizerBuilder standardCelOptimizerBuilder( CelParser celParser, CelChecker celChecker, CelRuntime celRuntime) { return standardCelOptimizerBuilder( - CelCompilerFactory.combine(celParser, celChecker), celRuntime); + celParser, celChecker, celRuntime, DEFAULT_OPTIMIZER_OPTIONS); + } + + /** Create a new builder for constructing a {@link CelOptimizer} instance with custom options. */ + public static CelOptimizerBuilder standardCelOptimizerBuilder( + CelParser celParser, + CelChecker celChecker, + CelRuntime celRuntime, + CelOptimizerOptions optimizerOptions) { + return standardCelOptimizerBuilder( + CelCompilerFactory.combine(celParser, celChecker), celRuntime, optimizerOptions); } private CelOptimizerFactory() {} diff --git a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java index 4ac8764f1..2911d3d4a 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java +++ b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java @@ -20,16 +20,25 @@ import dev.cel.bundle.Cel; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelValidationException; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import dev.cel.common.navigation.CelNavigableAst; +import dev.cel.common.navigation.CelNavigableExpr; import dev.cel.optimizer.CelAstOptimizer.OptimizationResult; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; final class CelOptimizerImpl implements CelOptimizer { private final Cel cel; private final ImmutableSet astOptimizers; + private final CelOptimizerOptions optimizerOptions; - CelOptimizerImpl(Cel cel, ImmutableSet astOptimizers) { + CelOptimizerImpl( + Cel cel, ImmutableSet astOptimizers, CelOptimizerOptions optimizerOptions) { this.cel = cel; this.astOptimizers = astOptimizers; + this.optimizerOptions = optimizerOptions; } @Override @@ -52,6 +61,9 @@ public CelAbstractSyntaxTree optimize(CelAbstractSyntaxTree ast) throws CelOptim .build(); } optimizedAst = celOptimizerEnv.check(result.optimizedAst()).getAst(); + if (optimizerOptions.enableAstValidation()) { + assertAstIdCorrectness(optimizedAst); + } } } catch (CelValidationException e) { throw new CelOptimizationException( @@ -63,18 +75,78 @@ public CelAbstractSyntaxTree optimize(CelAbstractSyntaxTree ast) throws CelOptim return optimizedAst; } + private static void assertAstIdCorrectness(CelAbstractSyntaxTree ast) { + Map allExprs = new HashMap<>(); + CelNavigableAst.fromAst(ast) + .getRoot() + .allNodes() + .forEach( + navExpr -> { + CelExpr expr = navExpr.expr(); + CelExpr existing = allExprs.put(expr.id(), expr); + if (existing != null) { + throw new IllegalStateException( + String.format("Duplicate expr ID %d detected in the AST.", expr.id())); + } + }); + + for (CelExpr macroCall : ast.getSource().getMacroCalls().values()) { + if (macroCall.id() != 0) { + throw new IllegalStateException( + String.format("Expected macro call root ID to be 0, but was %d.", macroCall.id())); + } + CelNavigableExpr.fromExpr(macroCall) + .descendants() + .forEach( + navExpr -> { + CelExpr macroExpr = navExpr.expr(); + CelExpr astExpr = allExprs.get(macroExpr.id()); + // A node may not exist in the AST if it is a synthetic macro node or was eliminated + // during optimization passes. + if (astExpr == null) { + return; + } + + if (astExpr.exprKind().getKind().equals(Kind.COMPREHENSION)) { + if (!macroExpr.exprKind().getKind().equals(Kind.NOT_SET)) { + throw new IllegalStateException( + String.format( + "Expected macro call node %d to be NOT_SET for comprehension, but" + + " was %s.", + macroExpr.id(), macroExpr.exprKind().getKind())); + } + } else if (!macroExpr.exprKind().getKind().equals(astExpr.exprKind().getKind())) { + throw new IllegalStateException( + String.format( + "Macro call node %d kind mismatch: expected %s (from AST), but was %s" + + " (in macro call).", + macroExpr.id(), + astExpr.exprKind().getKind(), + macroExpr.exprKind().getKind())); + } + }); + } + } + /** Create a new builder for constructing a {@link CelOptimizer} instance. */ static CelOptimizerImpl.Builder newBuilder(Cel cel) { - return new CelOptimizerImpl.Builder(cel); + return newBuilder(cel, CelOptimizerOptions.newBuilder().build()); + } + + /** Create a new builder for constructing a {@link CelOptimizer} instance with custom options. */ + static CelOptimizerImpl.Builder newBuilder(Cel cel, CelOptimizerOptions optimizerOptions) { + return new CelOptimizerImpl.Builder(cel, optimizerOptions); } /** Builder class for {@link CelOptimizerImpl}. */ static final class Builder implements CelOptimizerBuilder { private final Cel cel; + private final CelOptimizerOptions optimizerOptions; private final ImmutableSet.Builder astOptimizers; - private Builder(Cel cel) { + private Builder(Cel cel, CelOptimizerOptions optimizerOptions) { this.cel = cel; + this.optimizerOptions = checkNotNull(optimizerOptions); this.astOptimizers = ImmutableSet.builder(); } @@ -93,7 +165,7 @@ public CelOptimizerBuilder addAstOptimizers(Iterable astOptimiz @Override public CelOptimizer build() { - return new CelOptimizerImpl(cel, astOptimizers.build()); + return new CelOptimizerImpl(cel, astOptimizers.build(), optimizerOptions); } } } diff --git a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerOptions.java b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerOptions.java new file mode 100644 index 000000000..888683298 --- /dev/null +++ b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerOptions.java @@ -0,0 +1,51 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.optimizer; + +import com.google.auto.value.AutoValue; + +/** Options to configure how {@link CelOptimizer} behaves. */ +@AutoValue +public abstract class CelOptimizerOptions { + + /** + * Returns true if AST validation is enabled. When enabled, each optimizer pass verifies AST + * invariants (such as expression ID uniqueness and macro source consistency) after type-checking. + */ + public abstract boolean enableAstValidation(); + + /** Builder for configuring the {@link CelOptimizerOptions}. */ + @AutoValue.Builder + public abstract static class Builder { + + /** + * Enables or disables post-pass AST validation. When enabled, each optimizer pass verifies that + * expression IDs are unique and macro calls in the AST source are consistent with the + * expression nodes. + */ + public abstract Builder enableAstValidation(boolean value); + + public abstract CelOptimizerOptions build(); + + Builder() {} + } + + /** Returns a new options builder with recommended defaults pre-configured. */ + public static Builder newBuilder() { + return new AutoValue_CelOptimizerOptions.Builder().enableAstValidation(false); + } + + CelOptimizerOptions() {} +} diff --git a/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerFactoryTest.java b/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerFactoryTest.java index 41c7ecd74..146102995 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerFactoryTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerFactoryTest.java @@ -39,6 +39,19 @@ public void standardCelOptimizerBuilder_withParserCheckerAndRuntime() { assertThat(builder.build()).isNotNull(); } + @Test + public void standardCelOptimizerBuilder_withParserCheckerRuntimeAndOptions() { + CelOptimizerBuilder builder = + CelOptimizerFactory.standardCelOptimizerBuilder( + CelParserFactory.standardCelParserBuilder().build(), + CelCompilerFactory.standardCelCheckerBuilder().build(), + CelRuntimeFactory.standardCelRuntimeBuilder().build(), + CelOptimizerOptions.newBuilder().enableAstValidation(true).build()); + + assertThat(builder).isNotNull(); + assertThat(builder.build()).isNotNull(); + } + @Test public void standardCelOptimizerBuilder_withCompilerAndRuntime() { CelOptimizerBuilder builder = @@ -50,6 +63,18 @@ public void standardCelOptimizerBuilder_withCompilerAndRuntime() { assertThat(builder.build()).isNotNull(); } + @Test + public void standardCelOptimizerBuilder_withCompilerRuntimeAndOptions() { + CelOptimizerBuilder builder = + CelOptimizerFactory.standardCelOptimizerBuilder( + CelCompilerFactory.standardCelCompilerBuilder().build(), + CelRuntimeFactory.standardCelRuntimeBuilder().build(), + CelOptimizerOptions.newBuilder().enableAstValidation(true).build()); + + assertThat(builder).isNotNull(); + assertThat(builder.build()).isNotNull(); + } + @Test public void standardCelOptimizerBuilder_withCel() { CelOptimizerBuilder builder = @@ -58,4 +83,15 @@ public void standardCelOptimizerBuilder_withCel() { assertThat(builder).isNotNull(); assertThat(builder.build()).isNotNull(); } + + @Test + public void standardCelOptimizerBuilder_withCelAndOptions() { + CelOptimizerBuilder builder = + CelOptimizerFactory.standardCelOptimizerBuilder( + CelFactory.standardCelBuilder().build(), + CelOptimizerOptions.newBuilder().enableAstValidation(true).build()); + + assertThat(builder).isNotNull(); + assertThat(builder.build()).isNotNull(); + } } diff --git a/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerImplTest.java b/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerImplTest.java index cb0bff6c6..4373e7fe4 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerImplTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerImplTest.java @@ -17,15 +17,20 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import com.google.common.collect.ImmutableList; import dev.cel.bundle.Cel; import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelOptions; import dev.cel.common.CelSource; import dev.cel.common.CelValidationException; +import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; import dev.cel.optimizer.CelAstOptimizer.OptimizationResult; +import dev.cel.parser.CelStandardMacro; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -33,7 +38,11 @@ @RunWith(JUnit4.class) public class CelOptimizerImplTest { - private static final Cel CEL = CelFactory.standardCelBuilder().build(); + private static final Cel CEL = + CelFactory.standardCelBuilder() + .setOptions(CelOptions.current().populateMacroCalls(true).build()) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .build(); @Test public void constructCelOptimizer_success() { @@ -131,4 +140,181 @@ public void optimizedAst_failsToTypeCheck_throwsException() { + " 'undeclared_ident' (in container '')"); assertThat(e).hasCauseThat().isInstanceOf(CelValidationException.class); } + + @Test + public void optimize_duplicateExprId_throwsException() { + CelOptimizer celOptimizer = + CelOptimizerImpl.newBuilder( + CEL, CelOptimizerOptions.newBuilder().enableAstValidation(true).build()) + .addAstOptimizers( + (navigableAst, cel) -> + OptimizationResult.create( + CelAbstractSyntaxTree.newParsedAst( + CelExpr.ofCall( + 1, + Optional.empty(), + "_+_", + ImmutableList.of( + CelExpr.ofConstant(1, CelConstant.ofValue(1L)), + CelExpr.ofConstant(2, CelConstant.ofValue(2L)))), + CelSource.newBuilder().build()))) + .build(); + + CelOptimizationException e = + assertThrows( + CelOptimizationException.class, + () -> celOptimizer.optimize(CEL.compile("1 + 2").getAst())); + + assertThat(e) + .hasMessageThat() + .isEqualTo("Optimization failure: Duplicate expr ID 1 detected in the AST."); + assertThat(e).hasCauseThat().isInstanceOf(IllegalStateException.class); + } + + @Test + public void optimize_macroCallRootIdNonZero_throwsException() { + CelOptimizer celOptimizer = + CelOptimizerImpl.newBuilder( + CEL, CelOptimizerOptions.newBuilder().enableAstValidation(true).build()) + .addAstOptimizers( + (navigableAst, cel) -> + OptimizationResult.create( + CelAbstractSyntaxTree.newParsedAst( + CelExpr.ofConstant(1, CelConstant.ofValue(1L)), + CelSource.newBuilder() + .addMacroCalls( + 1L, + CelExpr.ofCall( + 10L, + Optional.empty(), + "has", + ImmutableList.of( + CelExpr.ofConstant(1L, CelConstant.ofValue(1L))))) + .build()))) + .build(); + + CelOptimizationException e = + assertThrows( + CelOptimizationException.class, () -> celOptimizer.optimize(CEL.compile("1").getAst())); + + assertThat(e) + .hasMessageThat() + .isEqualTo("Optimization failure: Expected macro call root ID to be 0, but was 10."); + assertThat(e).hasCauseThat().isInstanceOf(IllegalStateException.class); + } + + @Test + public void optimize_macroCallKindMismatch_throwsException() { + CelOptimizer celOptimizer = + CelOptimizerImpl.newBuilder( + CEL, CelOptimizerOptions.newBuilder().enableAstValidation(true).build()) + .addAstOptimizers( + (navigableAst, cel) -> + OptimizationResult.create( + CelAbstractSyntaxTree.newParsedAst( + CelExpr.ofConstant(1, CelConstant.ofValue(1L)), + CelSource.newBuilder() + .addMacroCalls( + 1L, + CelExpr.ofCall( + 0L, + Optional.empty(), + "has", + ImmutableList.of(CelExpr.ofIdent(1L, "x")))) + .build()))) + .build(); + + CelOptimizationException e = + assertThrows( + CelOptimizationException.class, () -> celOptimizer.optimize(CEL.compile("1").getAst())); + + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Optimization failure: Macro call node 1 kind mismatch: expected CONSTANT (from AST)," + + " but was IDENT (in macro call)."); + assertThat(e).hasCauseThat().isInstanceOf(IllegalStateException.class); + } + + @Test + public void optimize_macroCallComprehensionKindNotSetMismatch_throwsException() throws Exception { + CelAbstractSyntaxTree astWithComprehension = CEL.compile("[1].all(x, x > 0)").getAst(); + long compId = astWithComprehension.getExpr().id(); + + CelOptimizer celOptimizer = + CelOptimizerImpl.newBuilder( + CEL, CelOptimizerOptions.newBuilder().enableAstValidation(true).build()) + .addAstOptimizers( + (navigableAst, cel) -> + OptimizationResult.create( + CelAbstractSyntaxTree.newParsedAst( + astWithComprehension.getExpr(), + CelSource.newBuilder() + .addMacroCalls( + compId, + CelExpr.ofCall( + 0L, + Optional.empty(), + "all", + ImmutableList.of( + CelExpr.ofIdent(compId, "not_set_expected")))) + .build()))) + .build(); + + CelOptimizationException e = + assertThrows( + CelOptimizationException.class, () -> celOptimizer.optimize(astWithComprehension)); + + assertThat(e) + .hasMessageThat() + .isEqualTo( + String.format( + "Optimization failure: Expected macro call node %d to be NOT_SET for comprehension," + + " but was IDENT.", + compId)); + assertThat(e).hasCauseThat().isInstanceOf(IllegalStateException.class); + } + + @Test + public void optimize_macroCallComprehensionKindNotSet_success() throws Exception { + CelAbstractSyntaxTree astWithComprehension = CEL.compile("[1].all(x, x > 0)").getAst(); + long compId = astWithComprehension.getExpr().id(); + + CelOptimizer celOptimizer = + CelOptimizerImpl.newBuilder(CEL) + .addAstOptimizers( + (navigableAst, cel) -> + OptimizationResult.create( + CelAbstractSyntaxTree.newParsedAst( + astWithComprehension.getExpr(), + CelSource.newBuilder() + .addMacroCalls( + compId, + CelExpr.ofCall( + 0L, + Optional.empty(), + "all", + ImmutableList.of(CelExpr.ofNotSet(compId)))) + .build()))) + .build(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(astWithComprehension); + + assertThat(optimizedAst).isNotNull(); + } + + @Test + public void optimize_validMacroCalls_success() throws Exception { + CelAbstractSyntaxTree ast = CEL.compile("[1, 2, 3].all(x, x > 0)").getAst(); + + CelOptimizer celOptimizer = + CelOptimizerImpl.newBuilder(CEL) + .addAstOptimizers((navigableAst, cel) -> OptimizationResult.create(navigableAst)) + .build(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(optimizedAst).isNotNull(); + assertThat(optimizedAst.getSource().getMacroCalls()).hasSize(1); + } } From 1ba7cc76889f9e2dbd3ee68e29c6a2b3f78ea95c Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Thu, 13 Aug 2026 11:04:58 -0700 Subject: [PATCH 174/204] Default enable AST validation in optimizers PiperOrigin-RevId: 964184247 --- .../src/main/java/dev/cel/optimizer/CelOptimizerOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerOptions.java b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerOptions.java index 888683298..292f54224 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerOptions.java +++ b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerOptions.java @@ -44,7 +44,7 @@ public abstract static class Builder { /** Returns a new options builder with recommended defaults pre-configured. */ public static Builder newBuilder() { - return new AutoValue_CelOptimizerOptions.Builder().enableAstValidation(false); + return new AutoValue_CelOptimizerOptions.Builder().enableAstValidation(true); } CelOptimizerOptions() {} From 496434ef316f29b1a8031c768d30ea4160121e52 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Thu, 13 Aug 2026 13:19:36 -0700 Subject: [PATCH 175/204] Handle custom functions returning unknowns in planner PiperOrigin-RevId: 964256969 --- .../extensions/CelOptionalLibraryTest.java | 10 +- .../dev/cel/runtime/CallArgumentChecker.java | 2 +- .../dev/cel/runtime/DefaultInterpreter.java | 2 +- .../java/dev/cel/runtime/InterpreterUtil.java | 10 +- .../java/dev/cel/runtime/planner/BUILD.bazel | 6 + .../dev/cel/runtime/planner/EvalHelpers.java | 19 ++- .../runtime/planner/NamespacedAttribute.java | 3 + .../java/dev/cel/runtime/CelRuntimeTest.java | 157 +++++++++++++++++- .../runtime/planner/ProgramPlannerTest.java | 117 +++++++++++++ 9 files changed, 306 insertions(+), 20 deletions(-) diff --git a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java index fab444750..650c01526 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java @@ -54,7 +54,7 @@ import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelRuntime; -import dev.cel.runtime.InterpreterUtil; +import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.PartialVars; import java.time.Duration; import java.time.Instant; @@ -937,7 +937,7 @@ public void optionalIndex_onMapWithUnknownInput_returnsUnknownResult(String sour cel.createProgram(ast) .eval(PartialVars.of(CelAttributePattern.fromQualifiedIdentifier("x"))); - assertThat(InterpreterUtil.isUnknown(result)).isTrue(); + assertThat(result).isInstanceOf(CelUnknownSet.class); } @Test @@ -1029,7 +1029,7 @@ public void optionalIndex_onListWithUnknownInput_returnsUnknownResult() throws E cel.createProgram(ast) .eval(PartialVars.of(CelAttributePattern.fromQualifiedIdentifier("x"))); - assertThat(InterpreterUtil.isUnknown(result)).isTrue(); + assertThat(result).isInstanceOf(CelUnknownSet.class); } @Test @@ -1066,7 +1066,7 @@ public void optionalFieldSelect_fieldMarkedUnknown_returnsUnknownSet() throws Ex ImmutableMap.of("msg", TestAllTypes.newBuilder().setSingleInt32(42).build()), CelAttributePattern.fromQualifiedIdentifier("msg.single_int32"))); - assertThat(InterpreterUtil.isUnknown(result)).isTrue(); + assertThat(result).isInstanceOf(CelUnknownSet.class); } @Test @@ -1089,7 +1089,7 @@ public void optionalChainedFunctions_lhsIsUnknown_returnsUnknown(String expressi cel.createProgram(ast) .eval(PartialVars.of(CelAttributePattern.fromQualifiedIdentifier("optx"))); - assertThat(InterpreterUtil.isUnknown(result)).isTrue(); + assertThat(result).isInstanceOf(CelUnknownSet.class); } @Test diff --git a/runtime/src/main/java/dev/cel/runtime/CallArgumentChecker.java b/runtime/src/main/java/dev/cel/runtime/CallArgumentChecker.java index 76a942927..7ce8fb006 100644 --- a/runtime/src/main/java/dev/cel/runtime/CallArgumentChecker.java +++ b/runtime/src/main/java/dev/cel/runtime/CallArgumentChecker.java @@ -73,7 +73,7 @@ void checkArg(DefaultInterpreter.IntermediateResult arg) { unknowns = mergeOptionalUnknowns(unknowns, argUnknowns); // support for ExprValue unknowns. - if (InterpreterUtil.isAccumulatedUnknowns(arg.value())) { + if (arg.value() instanceof AccumulatedUnknowns) { AccumulatedUnknowns unknownSet = (AccumulatedUnknowns) arg.value(); exprIds.addAll(unknownSet.exprIds()); } diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java index fdab71c3d..fa22b1d00 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java @@ -282,7 +282,7 @@ private IntermediateResult evalInternal(ExecutionFrame frame, CelExpr expr) } private static boolean isUnknownValue(Object value) { - return InterpreterUtil.isAccumulatedUnknowns(value); + return value instanceof AccumulatedUnknowns; } private static boolean isUnknownOrError(Object value) { diff --git a/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java b/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java index 73607cefd..8c817d055 100644 --- a/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java +++ b/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java @@ -16,6 +16,7 @@ import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CheckReturnValue; +import com.google.errorprone.annotations.InlineMe; import dev.cel.common.annotations.Internal; import org.jspecify.annotations.Nullable; @@ -51,15 +52,14 @@ public static Object strict(Object valueOrThrowable) throws CelEvaluationExcepti * * @param obj Object to check. * @return boolean value if object is unknown. + * @deprecated Perform {@code obj instanceof CelUnknownSet} directly instead. */ + @Deprecated + @InlineMe(replacement = "obj instanceof CelUnknownSet", imports = "dev.cel.runtime.CelUnknownSet") public static boolean isUnknown(Object obj) { return obj instanceof CelUnknownSet; } - public static boolean isAccumulatedUnknowns(Object obj) { - return obj instanceof AccumulatedUnknowns; - } - /** If the argument is {@link CelUnknownSet}, adapts it into {@link AccumulatedUnknowns} */ public static Object maybeAdaptToAccumulatedUnknowns(Object val) { if (!(val instanceof CelUnknownSet)) { @@ -102,7 +102,7 @@ public static Object enforceStrictness(Object left, Object right) throws CelEval public static Object valueOrUnknown(@Nullable Object valueOrThrowable, Long id) { // Handle the unknown value case. - if (isAccumulatedUnknowns(valueOrThrowable)) { + if (valueOrThrowable instanceof AccumulatedUnknowns) { return AccumulatedUnknowns.create(id); } // Handle the null value case. diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index bdac6c95a..e05fca9b4 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -342,9 +342,12 @@ java_library( "//common:error_codes", "//common/exceptions:runtime_exception", "//common/values", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", + "//runtime:interpreter_util", "//runtime:resolved_overload", + "//runtime:unknown_attributes", "@maven//:com_google_guava_guava", ], ) @@ -851,9 +854,12 @@ cel_android_library( "//common:error_codes", "//common/exceptions:runtime_exception", "//common/values:values_android", + "//runtime:accumulated_unknowns_android", "//runtime:evaluation_exception", "//runtime:interpretable_android", + "//runtime:interpreter_util_android", "//runtime:resolved_overload_android", + "//runtime:unknown_attributes_android", "@maven_android//:com_google_guava_guava", ], ) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java index f9812793e..1b8d61234 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java @@ -19,9 +19,12 @@ import dev.cel.common.exceptions.CelRuntimeException; import dev.cel.common.values.CelValueConverter; import dev.cel.common.values.ErrorValue; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelResolvedOverload; +import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.InterpreterUtil; final class EvalHelpers { @@ -63,7 +66,7 @@ static Object dispatch( throws CelEvaluationException { try { Object result = overload.invoke(args); - return valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result)); + return convertAndAdaptResult(valueConverter, result); } catch (RuntimeException e) { throw handleDispatchException(e, overload, args); } @@ -77,7 +80,7 @@ static Object dispatch( throws CelEvaluationException { try { Object result = overload.invoke(arg); - return valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result)); + return convertAndAdaptResult(valueConverter, result); } catch (RuntimeException e) { throw handleDispatchException(e, overload, arg); } @@ -92,12 +95,22 @@ static Object dispatch( throws CelEvaluationException { try { Object result = overload.invoke(arg1, arg2); - return valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result)); + return convertAndAdaptResult(valueConverter, result); } catch (RuntimeException e) { throw handleDispatchException(e, overload, arg1, arg2); } } + /** + * Converts the raw invocation result into a CEL runtime value, unwraps it if necessary, and + * adapts any public {@link CelUnknownSet} instances into internal {@link AccumulatedUnknowns} for + * AST evaluation. + */ + private static Object convertAndAdaptResult(CelValueConverter valueConverter, Object result) { + return InterpreterUtil.maybeAdaptToAccumulatedUnknowns( + valueConverter.maybeUnwrap(valueConverter.toRuntimeValue(result))); + } + private static RuntimeException handleDispatchException( RuntimeException e, CelResolvedOverload overload, Object... args) { if (e instanceof CelRuntimeException) { diff --git a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java index 95a4489fd..01673923d 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java @@ -184,6 +184,9 @@ public NamespacedAttribute addQualifier(Qualifier qualifier) { private static Object applyQualifiers( Object value, CelValueConverter celValueConverter, ImmutableList qualifiers) { + if (value instanceof AccumulatedUnknowns) { + return value; + } Object obj = celValueConverter.toRuntimeValue(value); // Avoid enhanced for loop to prevent UnmodifiableIterator from being allocated diff --git a/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java b/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java index 13d5dd550..d7247f8a1 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java @@ -522,7 +522,7 @@ public void trace_shortCircuitingDisabledWithUnknownsAndedToFalse_returnsFalse(S (expr, res) -> { if (expr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE) || expr.identOrDefault().name().equals("x")) { - if (InterpreterUtil.isUnknown(res)) { + if (res instanceof CelUnknownSet) { branchResults.add("x"); // Swap unknown result with a sentinel value for testing } else { branchResults.add(res); @@ -577,7 +577,7 @@ public void trace_shortCircuitingDisabledWithUnknownAndedToTrue_returnsUnknown(S PartialVars partialVars = PartialVars.of(CelAttributePattern.create("x")); Object unknownResult = cel.createProgram(ast).trace(partialVars, listener); - assertThat(InterpreterUtil.isUnknown(unknownResult)).isTrue(); + assertThat(unknownResult).isInstanceOf(CelUnknownSet.class); assertThat(branchResults.build()).containsExactly(true, true, unknownResult); } @@ -653,7 +653,7 @@ public void trace_shortCircuitingDisabledWithUnknownsOredToFalse_returnsUnknown( PartialVars partialVars = PartialVars.of(CelAttributePattern.create("x")); Object unknownResult = cel.createProgram(ast).trace(partialVars, listener); - assertThat(InterpreterUtil.isUnknown(unknownResult)).isTrue(); + assertThat(unknownResult).isInstanceOf(CelUnknownSet.class); assertThat(branchResults.build()).containsExactly(false, false, unknownResult); } @@ -668,7 +668,7 @@ public void trace_shortCircuitingDisabledWithUnknownOredToTrue_returnsTrue(Strin (expr, res) -> { if (expr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE) || expr.identOrDefault().name().equals("x")) { - if (InterpreterUtil.isUnknown(res)) { + if (res instanceof CelUnknownSet) { branchResults.add("x"); // Swap unknown result with a sentinel value for testing } else { branchResults.add(res); @@ -748,7 +748,7 @@ public void trace_shortCircuitingDisabled_ternaryWithUnknowns(String source) thr PartialVars partialVars = PartialVars.of(CelAttributePattern.create("x")); Object unknownResult = cel.createProgram(ast).trace(partialVars, listener); - assertThat(InterpreterUtil.isUnknown(unknownResult)).isTrue(); + assertThat(unknownResult).isInstanceOf(CelUnknownSet.class); assertThat(branchResults.build()).containsExactly(false, unknownResult, true); } @@ -944,4 +944,151 @@ public void trace_shortCircuitingDisabled_logicalOrPrefersFirstError() throws Ex CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> program.eval()); assertThat(e).hasCauseThat().hasMessageThat().contains("error 1"); } + + @Test + public void evaluate_customFunctionReturningCelUnknownSet_propagatesUnknown( + @TestParameter({ + // Field selection + "getMsg().single_int32", + "getMsg().single_nested_message.bb", + // Binary & unary operators + "getMsg().single_int32 == 100", + "getMsg().single_int32 + 5 == 10", + "-getMsg().single_int32 == -10", + // Boolean operators & ternary + "true && (getMsg().single_int32 == 100)", + "false || (getMsg().single_int32 == 100)", + "(getMsg().single_int32 == 100) ? 'match' : 'no-match'", + // Comprehensions + "[1, 2, 3].exists(x, x == getMsg().single_int32)", + "[1, 2, 3].all(x, x > 0 && getMsg().single_int32 > 0)", + "[1, 2, 3].map(x, x + getMsg().single_int32)", + "[1, 2, 3].filter(x, x == getMsg().single_int32)", + }) + String expression) + throws Exception { + Cel cel = + runtimeFlavor + .builder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addMessageTypes(TestAllTypes.getDescriptor()) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "getMsg", + CelOverloadDecl.newGlobalOverload( + "getMsg_overload", + StructTypeReference.create(TestAllTypes.getDescriptor().getFullName()), + ImmutableList.of()))) + .addFunctionBindings( + CelFunctionBinding.from( + "getMsg_overload", + ImmutableList.of(), + args -> CelUnknownSet.create(CelAttribute.create("custom_msg")))) + .build(); + + Object result = cel.createProgram(cel.compile(expression).getAst()).eval(); + + assertThat(result).isInstanceOf(CelUnknownSet.class); + } + + @Test + // Short-circuited boolean operators + @TestParameters("{expression: 'false && (getMsg().single_int32 == 100)', expected: false}") + @TestParameters("{expression: 'true || (getMsg().single_int32 == 100)', expected: true}") + // Short-circuited comprehensions + @TestParameters( + "{expression: '[1, 2, 3].exists(x, x == 1 || x == getMsg().single_int32)', expected: true}") + @TestParameters( + "{expression: '[1, 2, 3].all(x, x == 0 && getMsg().single_int32 > 0)', expected: false}") + public void evaluate_customFunctionReturningCelUnknownSet_shortCircuits( + String expression, boolean expected) throws Exception { + Cel cel = + runtimeFlavor + .builder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addMessageTypes(TestAllTypes.getDescriptor()) + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "getMsg", + CelOverloadDecl.newGlobalOverload( + "getMsg_overload", + StructTypeReference.create(TestAllTypes.getDescriptor().getFullName()), + ImmutableList.of()))) + .addFunctionBindings( + CelFunctionBinding.from( + "getMsg_overload", + ImmutableList.of(), + args -> CelUnknownSet.create(CelAttribute.create("custom_msg")))) + .build(); + + Object result = cel.createProgram(cel.compile(expression).getAst()).eval(); + + assertThat(result).isEqualTo(expected); + } + + @Test + public void evaluate_customFunctionReturningCelUnknownSet_differentArities() throws Exception { + Cel cel = + runtimeFlavor + .builder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "unkZero", + CelOverloadDecl.newGlobalOverload( + "unk_zero", SimpleType.INT, ImmutableList.of())), + CelFunctionDecl.newFunctionDeclaration( + "unkUnary", + CelOverloadDecl.newGlobalOverload("unk_unary", SimpleType.INT, SimpleType.INT)), + CelFunctionDecl.newFunctionDeclaration( + "unkBinary", + CelOverloadDecl.newGlobalOverload( + "unk_binary", SimpleType.INT, SimpleType.INT, SimpleType.INT)), + CelFunctionDecl.newFunctionDeclaration( + "unkMember", + CelOverloadDecl.newMemberOverload( + "unk_member", SimpleType.INT, SimpleType.STRING, SimpleType.INT)), + CelFunctionDecl.newFunctionDeclaration( + "unkVarargs", + CelOverloadDecl.newGlobalOverload( + "unk_varargs", + SimpleType.INT, + SimpleType.INT, + SimpleType.INT, + SimpleType.INT))) + .addFunctionBindings( + CelFunctionBinding.from( + "unk_zero", + ImmutableList.of(), + args -> CelUnknownSet.create(CelAttribute.create("attr_zero"))), + CelFunctionBinding.from( + "unk_unary", + Long.class, + arg -> CelUnknownSet.create(CelAttribute.create("attr_unary"))), + CelFunctionBinding.from( + "unk_binary", + Long.class, + Long.class, + (a, b) -> CelUnknownSet.create(CelAttribute.create("attr_binary"))), + CelFunctionBinding.from( + "unk_member", + String.class, + Long.class, + (target, arg) -> CelUnknownSet.create(CelAttribute.create("attr_member"))), + CelFunctionBinding.from( + "unk_varargs", + ImmutableList.of(Long.class, Long.class, Long.class), + args -> CelUnknownSet.create(CelAttribute.create("attr_varargs")))) + .build(); + + assertThat(cel.createProgram(cel.compile("unkZero() + 1").getAst()).eval()) + .isEqualTo(CelUnknownSet.create(CelAttribute.create("attr_zero"))); + assertThat(cel.createProgram(cel.compile("unkUnary(1) + 1").getAst()).eval()) + .isEqualTo(CelUnknownSet.create(CelAttribute.create("attr_unary"))); + assertThat(cel.createProgram(cel.compile("unkBinary(1, 2) + 1").getAst()).eval()) + .isEqualTo(CelUnknownSet.create(CelAttribute.create("attr_binary"))); + assertThat(cel.createProgram(cel.compile("'target'.unkMember(1) + 1").getAst()).eval()) + .isEqualTo(CelUnknownSet.create(CelAttribute.create("attr_member"))); + assertThat(cel.createProgram(cel.compile("unkVarargs(1, 2, 3) + 1").getAst()).eval()) + .isEqualTo(CelUnknownSet.create(CelAttribute.create("attr_varargs"))); + } } diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index 57fa72162..34e7831a6 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -1253,6 +1253,123 @@ public void localDoubleShadowIdentifier_withGlobalDisambiguation() throws Except assertThat(result).isTrue(); } + @Test + public void plan_customFunctionReturningUnknown_fieldSelection() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .addFunctionDeclarations( + newFunctionDeclaration( + "getMsg", + newGlobalOverload( + "getMsg_overload", + StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())))) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "getMsg().single_int32"); + + DefaultDispatcher.Builder dispatcherBuilder = DefaultDispatcher.newBuilder(); + addBindingsToDispatcher( + dispatcherBuilder, + CelFunctionBinding.fromOverloads( + "getMsg", + CelFunctionBinding.from( + "getMsg_overload", + ImmutableList.of(), + (unused) -> CelUnknownSet.create(CelAttribute.create("custom_msg"))))); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + VALUE_PROVIDER, + dispatcherBuilder.build(), + CEL_VALUE_CONVERTER, + CEL_CONTAINER, + CEL_OPTIONS, + ImmutableSet.of()); + + Program program = planner.plan(ast); + + Object result = program.eval(); + assertThat(((CelUnknownSet) result).attributes()) + .containsExactly(CelAttribute.create("custom_msg")); + } + + @Test + public void plan_customFunctionReturningUnknown_binaryOperation() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addFunctionDeclarations( + newFunctionDeclaration( + "getUnknownInt", newGlobalOverload("getUnknownInt_overload", SimpleType.INT))) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "getUnknownInt() == 100"); + + DefaultDispatcher.Builder dispatcherBuilder = DefaultDispatcher.newBuilder(); + CelStandardFunctions stdFunctions = + CelStandardFunctions.newBuilder().includeFunctions(StandardFunction.EQUALS).build(); + addBindingsToDispatcher( + dispatcherBuilder, stdFunctions.newFunctionBindings(RUNTIME_EQUALITY, CEL_OPTIONS)); + addBindingsToDispatcher( + dispatcherBuilder, + CelFunctionBinding.fromOverloads( + "getUnknownInt", + CelFunctionBinding.from( + "getUnknownInt_overload", + ImmutableList.of(), + (unused) -> CelUnknownSet.create(CelAttribute.create("custom_int"))))); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + VALUE_PROVIDER, + dispatcherBuilder.build(), + CEL_VALUE_CONVERTER, + CEL_CONTAINER, + CEL_OPTIONS, + ImmutableSet.of()); + + Program program = planner.plan(ast); + + Object result = program.eval(); + assertThat(((CelUnknownSet) result).attributes()) + .containsExactly(CelAttribute.create("custom_int")); + } + + @Test + public void plan_variableAsCelUnknownSet_propagatesUnknown() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .addVar("x", SimpleType.INT) + .build(); + CelAbstractSyntaxTree ast1 = compile(compiler, "x + 1"); + CelAbstractSyntaxTree ast2 = compile(compiler, "msg.single_int32"); + + DefaultDispatcher.Builder dispatcherBuilder = DefaultDispatcher.newBuilder(); + CelStandardFunctions stdFunctions = + CelStandardFunctions.newBuilder().includeFunctions(StandardFunction.ADD).build(); + addBindingsToDispatcher( + dispatcherBuilder, stdFunctions.newFunctionBindings(RUNTIME_EQUALITY, CEL_OPTIONS)); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + VALUE_PROVIDER, + dispatcherBuilder.build(), + CEL_VALUE_CONVERTER, + CEL_CONTAINER, + CEL_OPTIONS, + ImmutableSet.of()); + + ImmutableMap vars = + ImmutableMap.of( + "msg", CelUnknownSet.create(CelAttribute.create("custom_msg")), + "x", CelUnknownSet.create(CelAttribute.create("custom_x"))); + + assertThat(planner.plan(ast1).eval(vars)) + .isEqualTo(CelUnknownSet.create(CelAttribute.create("custom_x"))); + assertThat(planner.plan(ast2).eval(vars)) + .isEqualTo(CelUnknownSet.create(CelAttribute.create("custom_msg"))); + } + private CelAbstractSyntaxTree compile(String expression) throws Exception { return compile(CEL_COMPILER, expression); } From e5c146649da5b01202325f23b141fd83bd2d62f4 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Thu, 13 Aug 2026 13:30:52 -0700 Subject: [PATCH 176/204] Remove CelOptimizerOptions PiperOrigin-RevId: 964262762 --- .../main/java/dev/cel/optimizer/BUILD.bazel | 2 - .../cel/optimizer/CelOptimizerFactory.java | 32 ++---------- .../dev/cel/optimizer/CelOptimizerImpl.java | 22 ++------ .../cel/optimizer/CelOptimizerOptions.java | 51 ------------------- .../optimizer/CelOptimizerFactoryTest.java | 36 ------------- .../cel/optimizer/CelOptimizerImplTest.java | 12 ++--- 6 files changed, 12 insertions(+), 143 deletions(-) delete mode 100644 optimizer/src/main/java/dev/cel/optimizer/CelOptimizerOptions.java diff --git a/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel b/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel index 22dab14f4..31e410f6a 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel +++ b/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel @@ -32,14 +32,12 @@ java_library( srcs = [ "CelOptimizer.java", "CelOptimizerBuilder.java", - "CelOptimizerOptions.java", ], tags = [ ], deps = [ ":ast_optimizer", ":optimization_exception", - "//:auto_value", "//common:cel_ast", "@maven//:com_google_errorprone_error_prone_annotations", ], diff --git a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerFactory.java b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerFactory.java index d82403825..1ebfd293e 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerFactory.java +++ b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerFactory.java @@ -25,48 +25,22 @@ /** Factory class for constructing an {@link CelOptimizer} instance. */ public final class CelOptimizerFactory { - private static final CelOptimizerOptions DEFAULT_OPTIMIZER_OPTIONS = - CelOptimizerOptions.newBuilder().build(); - /** Create a new builder for constructing a {@link CelOptimizer} instance. */ public static CelOptimizerBuilder standardCelOptimizerBuilder(Cel cel) { - return standardCelOptimizerBuilder(cel, DEFAULT_OPTIMIZER_OPTIONS); - } - - /** Create a new builder for constructing a {@link CelOptimizer} instance with custom options. */ - public static CelOptimizerBuilder standardCelOptimizerBuilder( - Cel cel, CelOptimizerOptions optimizerOptions) { - return CelOptimizerImpl.newBuilder(cel, optimizerOptions); + return CelOptimizerImpl.newBuilder(cel); } /** Create a new builder for constructing a {@link CelOptimizer} instance. */ public static CelOptimizerBuilder standardCelOptimizerBuilder( CelCompiler celCompiler, CelRuntime celRuntime) { - return standardCelOptimizerBuilder(celCompiler, celRuntime, DEFAULT_OPTIMIZER_OPTIONS); - } - - /** Create a new builder for constructing a {@link CelOptimizer} instance with custom options. */ - public static CelOptimizerBuilder standardCelOptimizerBuilder( - CelCompiler celCompiler, CelRuntime celRuntime, CelOptimizerOptions optimizerOptions) { - return standardCelOptimizerBuilder( - CelFactory.combine(celCompiler, celRuntime), optimizerOptions); + return standardCelOptimizerBuilder(CelFactory.combine(celCompiler, celRuntime)); } /** Create a new builder for constructing a {@link CelOptimizer} instance. */ public static CelOptimizerBuilder standardCelOptimizerBuilder( CelParser celParser, CelChecker celChecker, CelRuntime celRuntime) { return standardCelOptimizerBuilder( - celParser, celChecker, celRuntime, DEFAULT_OPTIMIZER_OPTIONS); - } - - /** Create a new builder for constructing a {@link CelOptimizer} instance with custom options. */ - public static CelOptimizerBuilder standardCelOptimizerBuilder( - CelParser celParser, - CelChecker celChecker, - CelRuntime celRuntime, - CelOptimizerOptions optimizerOptions) { - return standardCelOptimizerBuilder( - CelCompilerFactory.combine(celParser, celChecker), celRuntime, optimizerOptions); + CelCompilerFactory.combine(celParser, celChecker), celRuntime); } private CelOptimizerFactory() {} diff --git a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java index 2911d3d4a..f5e30093a 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java +++ b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java @@ -32,13 +32,10 @@ final class CelOptimizerImpl implements CelOptimizer { private final Cel cel; private final ImmutableSet astOptimizers; - private final CelOptimizerOptions optimizerOptions; - CelOptimizerImpl( - Cel cel, ImmutableSet astOptimizers, CelOptimizerOptions optimizerOptions) { + CelOptimizerImpl(Cel cel, ImmutableSet astOptimizers) { this.cel = cel; this.astOptimizers = astOptimizers; - this.optimizerOptions = optimizerOptions; } @Override @@ -61,9 +58,7 @@ public CelAbstractSyntaxTree optimize(CelAbstractSyntaxTree ast) throws CelOptim .build(); } optimizedAst = celOptimizerEnv.check(result.optimizedAst()).getAst(); - if (optimizerOptions.enableAstValidation()) { - assertAstIdCorrectness(optimizedAst); - } + assertAstIdCorrectness(optimizedAst); } } catch (CelValidationException e) { throw new CelOptimizationException( @@ -130,23 +125,16 @@ private static void assertAstIdCorrectness(CelAbstractSyntaxTree ast) { /** Create a new builder for constructing a {@link CelOptimizer} instance. */ static CelOptimizerImpl.Builder newBuilder(Cel cel) { - return newBuilder(cel, CelOptimizerOptions.newBuilder().build()); - } - - /** Create a new builder for constructing a {@link CelOptimizer} instance with custom options. */ - static CelOptimizerImpl.Builder newBuilder(Cel cel, CelOptimizerOptions optimizerOptions) { - return new CelOptimizerImpl.Builder(cel, optimizerOptions); + return new CelOptimizerImpl.Builder(cel); } /** Builder class for {@link CelOptimizerImpl}. */ static final class Builder implements CelOptimizerBuilder { private final Cel cel; - private final CelOptimizerOptions optimizerOptions; private final ImmutableSet.Builder astOptimizers; - private Builder(Cel cel, CelOptimizerOptions optimizerOptions) { + private Builder(Cel cel) { this.cel = cel; - this.optimizerOptions = checkNotNull(optimizerOptions); this.astOptimizers = ImmutableSet.builder(); } @@ -165,7 +153,7 @@ public CelOptimizerBuilder addAstOptimizers(Iterable astOptimiz @Override public CelOptimizer build() { - return new CelOptimizerImpl(cel, astOptimizers.build(), optimizerOptions); + return new CelOptimizerImpl(cel, astOptimizers.build()); } } } diff --git a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerOptions.java b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerOptions.java deleted file mode 100644 index 292f54224..000000000 --- a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerOptions.java +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dev.cel.optimizer; - -import com.google.auto.value.AutoValue; - -/** Options to configure how {@link CelOptimizer} behaves. */ -@AutoValue -public abstract class CelOptimizerOptions { - - /** - * Returns true if AST validation is enabled. When enabled, each optimizer pass verifies AST - * invariants (such as expression ID uniqueness and macro source consistency) after type-checking. - */ - public abstract boolean enableAstValidation(); - - /** Builder for configuring the {@link CelOptimizerOptions}. */ - @AutoValue.Builder - public abstract static class Builder { - - /** - * Enables or disables post-pass AST validation. When enabled, each optimizer pass verifies that - * expression IDs are unique and macro calls in the AST source are consistent with the - * expression nodes. - */ - public abstract Builder enableAstValidation(boolean value); - - public abstract CelOptimizerOptions build(); - - Builder() {} - } - - /** Returns a new options builder with recommended defaults pre-configured. */ - public static Builder newBuilder() { - return new AutoValue_CelOptimizerOptions.Builder().enableAstValidation(true); - } - - CelOptimizerOptions() {} -} diff --git a/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerFactoryTest.java b/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerFactoryTest.java index 146102995..41c7ecd74 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerFactoryTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerFactoryTest.java @@ -39,19 +39,6 @@ public void standardCelOptimizerBuilder_withParserCheckerAndRuntime() { assertThat(builder.build()).isNotNull(); } - @Test - public void standardCelOptimizerBuilder_withParserCheckerRuntimeAndOptions() { - CelOptimizerBuilder builder = - CelOptimizerFactory.standardCelOptimizerBuilder( - CelParserFactory.standardCelParserBuilder().build(), - CelCompilerFactory.standardCelCheckerBuilder().build(), - CelRuntimeFactory.standardCelRuntimeBuilder().build(), - CelOptimizerOptions.newBuilder().enableAstValidation(true).build()); - - assertThat(builder).isNotNull(); - assertThat(builder.build()).isNotNull(); - } - @Test public void standardCelOptimizerBuilder_withCompilerAndRuntime() { CelOptimizerBuilder builder = @@ -63,18 +50,6 @@ public void standardCelOptimizerBuilder_withCompilerAndRuntime() { assertThat(builder.build()).isNotNull(); } - @Test - public void standardCelOptimizerBuilder_withCompilerRuntimeAndOptions() { - CelOptimizerBuilder builder = - CelOptimizerFactory.standardCelOptimizerBuilder( - CelCompilerFactory.standardCelCompilerBuilder().build(), - CelRuntimeFactory.standardCelRuntimeBuilder().build(), - CelOptimizerOptions.newBuilder().enableAstValidation(true).build()); - - assertThat(builder).isNotNull(); - assertThat(builder.build()).isNotNull(); - } - @Test public void standardCelOptimizerBuilder_withCel() { CelOptimizerBuilder builder = @@ -83,15 +58,4 @@ public void standardCelOptimizerBuilder_withCel() { assertThat(builder).isNotNull(); assertThat(builder.build()).isNotNull(); } - - @Test - public void standardCelOptimizerBuilder_withCelAndOptions() { - CelOptimizerBuilder builder = - CelOptimizerFactory.standardCelOptimizerBuilder( - CelFactory.standardCelBuilder().build(), - CelOptimizerOptions.newBuilder().enableAstValidation(true).build()); - - assertThat(builder).isNotNull(); - assertThat(builder.build()).isNotNull(); - } } diff --git a/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerImplTest.java b/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerImplTest.java index 4373e7fe4..9e92814f2 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerImplTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerImplTest.java @@ -144,8 +144,7 @@ public void optimizedAst_failsToTypeCheck_throwsException() { @Test public void optimize_duplicateExprId_throwsException() { CelOptimizer celOptimizer = - CelOptimizerImpl.newBuilder( - CEL, CelOptimizerOptions.newBuilder().enableAstValidation(true).build()) + CelOptimizerImpl.newBuilder(CEL) .addAstOptimizers( (navigableAst, cel) -> OptimizationResult.create( @@ -174,8 +173,7 @@ public void optimize_duplicateExprId_throwsException() { @Test public void optimize_macroCallRootIdNonZero_throwsException() { CelOptimizer celOptimizer = - CelOptimizerImpl.newBuilder( - CEL, CelOptimizerOptions.newBuilder().enableAstValidation(true).build()) + CelOptimizerImpl.newBuilder(CEL) .addAstOptimizers( (navigableAst, cel) -> OptimizationResult.create( @@ -206,8 +204,7 @@ public void optimize_macroCallRootIdNonZero_throwsException() { @Test public void optimize_macroCallKindMismatch_throwsException() { CelOptimizer celOptimizer = - CelOptimizerImpl.newBuilder( - CEL, CelOptimizerOptions.newBuilder().enableAstValidation(true).build()) + CelOptimizerImpl.newBuilder(CEL) .addAstOptimizers( (navigableAst, cel) -> OptimizationResult.create( @@ -242,8 +239,7 @@ public void optimize_macroCallComprehensionKindNotSetMismatch_throwsException() long compId = astWithComprehension.getExpr().id(); CelOptimizer celOptimizer = - CelOptimizerImpl.newBuilder( - CEL, CelOptimizerOptions.newBuilder().enableAstValidation(true).build()) + CelOptimizerImpl.newBuilder(CEL) .addAstOptimizers( (navigableAst, cel) -> OptimizationResult.create( From e13bec5fa83e5797232712116475bcbefac154c6 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Thu, 13 Aug 2026 18:15:33 -0700 Subject: [PATCH 177/204] Add block_ext to conformance test suite, refactor to consolidate cel.block overload declaration PiperOrigin-RevId: 964402265 --- .../test/java/dev/cel/conformance/BUILD.bazel | 5 + .../dev/cel/conformance/ConformanceTest.java | 119 +++++++++++++++++- extensions/BUILD.bazel | 6 + .../main/java/dev/cel/extensions/BUILD.bazel | 1 + .../cel/extensions/CelBindingsExtensions.java | 19 +-- .../dev/cel/optimizer/optimizers/BUILD.bazel | 1 + .../optimizers/SubexpressionOptimizer.java | 18 +-- 7 files changed, 146 insertions(+), 23 deletions(-) diff --git a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel index c5364b146..4abc705c3 100644 --- a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -20,10 +20,14 @@ java_library( "//common:compiler_common", "//common:container", "//common:options", + "//common/ast", + "//common/ast:cel_block", + "//common/types", "//common/types:cel_proto_types", "//compiler", "//compiler:compiler_builder", "//extensions", + "//extensions:bindings", "//extensions:optional_library", "//parser:macro", "//parser:parser_builder", @@ -75,6 +79,7 @@ java_library( _ALL_TESTS = [ "@cel_spec//tests/simple:testdata/basic.textproto", "@cel_spec//tests/simple:testdata/bindings_ext.textproto", + "@cel_spec//tests/simple:testdata/block_ext.textproto", "@cel_spec//tests/simple:testdata/comparisons.textproto", "@cel_spec//tests/simple:testdata/conversions.textproto", "@cel_spec//tests/simple:testdata/dynamic.textproto", diff --git a/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java index db57ccb79..82b4cf812 100644 --- a/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java +++ b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java @@ -30,16 +30,28 @@ import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.TypeRegistry; import dev.cel.checker.CelChecker; +import dev.cel.checker.CelCheckerBuilder; import dev.cel.common.CelContainer; +import dev.cel.common.CelIssue; import dev.cel.common.CelOptions; import dev.cel.common.CelValidationResult; +import dev.cel.common.CelVarDecl; +import dev.cel.common.ast.CelBlock; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr; import dev.cel.common.types.CelProtoTypes; +import dev.cel.common.types.SimpleType; import dev.cel.compiler.CelCompilerFactory; import dev.cel.compiler.CelCompilerLibrary; import dev.cel.expr.conformance.test.SimpleTest; +import dev.cel.extensions.CelBindingsExtensions; import dev.cel.extensions.CelExtensions; import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.parser.CelMacro; +import dev.cel.parser.CelMacroExpander; +import dev.cel.parser.CelMacroExprFactory; import dev.cel.parser.CelParser; +import dev.cel.parser.CelParserBuilder; import dev.cel.parser.CelParserFactory; import dev.cel.parser.CelStandardMacro; import dev.cel.runtime.CelEvaluationException; @@ -50,6 +62,7 @@ import dev.cel.runtime.CelRuntimeImpl; import dev.cel.runtime.CelRuntimeLibrary; import java.util.Map; +import java.util.Optional; import org.junit.runners.model.Statement; // Qualifying proto2/proto3 TestAllTypes makes it less clear. @@ -73,7 +86,8 @@ public final class ConformanceTest extends Statement { CelExtensions.protos(), CelExtensions.sets(OPTIONS), CelExtensions.strings(), - CelOptionalLibrary.INSTANCE); + CelOptionalLibrary.INSTANCE, + new ConformanceBlockLibrary()); private static final ImmutableList CANONICAL_RUNTIME_EXTENSIONS = ImmutableList.of( @@ -206,11 +220,11 @@ public boolean shouldSkip() { @Override public void evaluate() throws Throwable { CelValidationResult response = getParser(test).parse(test.getExpr(), test.getName()); - assertThat(response.hasError()).isFalse(); + assertThat(response.getErrors()).isEmpty(); if (!test.getDisableCheck()) { response = getChecker(test).check(response.getAst()); } - assertThat(response.hasError()).isFalse(); + assertThat(response.getErrors()).isEmpty(); Type resultType = CelProtoTypes.celTypeToType(response.getAst().getResultType()); if (test.getCheckOnly()) { @@ -262,4 +276,103 @@ public void evaluate() throws Throwable { String.format("Unexpected matcher kind: %s", test.getResultMatcherCase())); } } + + /** + * Conformance-only library providing macros for the {@code block_ext} test suite. + * + *

These macros ({@code cel.block}, {@code cel.index}, {@code cel.iterVar}, and {@code + * cel.accuVar}) are strictly used for conformance testing to represent block expressions in text + * form. In production, AST optimization passes (such as common subexpression elimination) + * directly generate the {@code cel.@block} call and {@code @index} / {@code @it} / {@code @ac} + * variable nodes without going through these macros. + */ + private static final class ConformanceBlockLibrary implements CelCompilerLibrary { + private static final int MAX_INDICES = 30; + + @Override + public void setParserOptions(CelParserBuilder parserBuilder) { + parserBuilder.addMacros( + CelMacro.newReceiverMacro("block", 2, ConformanceBlockLibrary::expandBlock), + CelMacro.newReceiverMacro("index", 1, ConformanceBlockLibrary::expandIndex), + CelMacro.newReceiverMacro("iterVar", 2, expandCompreVar("cel.iterVar", "@it")), + CelMacro.newReceiverMacro("accuVar", 2, expandCompreVar("cel.accuVar", "@ac"))); + } + + @Override + public void setCheckerOptions(CelCheckerBuilder checkerBuilder) { + checkerBuilder.addFunctionDeclarations(CelBindingsExtensions.CEL_BLOCK_FUNCTION_DECL); + for (int i = 0; i < MAX_INDICES; i++) { + checkerBuilder.addVarDeclarations( + CelVarDecl.newVarDeclaration(CelBlock.INDEX_PREFIX + i, SimpleType.DYN)); + } + } + + private static Optional expandBlock( + CelMacroExprFactory exprFactory, CelExpr target, ImmutableList args) { + if (!isCelNamespace(target)) { + return Optional.empty(); + } + CelExpr bindings = args.get(0); + if (!bindings.exprKind().getKind().equals(CelExpr.ExprKind.Kind.LIST)) { + return Optional.of( + exprFactory.reportError( + CelIssue.formatError( + exprFactory.getSourceLocation(bindings), + "cel.block requires the first arg to be a list literal"))); + } + return Optional.of(exprFactory.newGlobalCall(CelBlock.FUNCTION_NAME, args)); + } + + private static Optional expandIndex( + CelMacroExprFactory exprFactory, CelExpr target, ImmutableList args) { + if (!isCelNamespace(target)) { + return Optional.empty(); + } + CelExpr index = args.get(0); + if (!isNonNegativeInt(index)) { + return Optional.of( + exprFactory.reportError( + CelIssue.formatError( + exprFactory.getSourceLocation(index), + "cel.index requires a single non-negative int constant arg"))); + } + return Optional.of( + exprFactory.newIdentifier(CelBlock.INDEX_PREFIX + index.constant().int64Value())); + } + + private static CelMacroExpander expandCompreVar(String macroName, String prefix) { + return (exprFactory, target, args) -> { + if (!isCelNamespace(target)) { + return Optional.empty(); + } + for (CelExpr arg : args) { + if (!isNonNegativeInt(arg)) { + return Optional.of( + exprFactory.reportError( + CelIssue.formatError( + exprFactory.getSourceLocation(arg), + macroName + " requires two non-negative int constant args"))); + } + } + return Optional.of( + exprFactory.newIdentifier( + String.format( + "%s:%d:%d", + prefix, + args.get(0).constant().int64Value(), + args.get(1).constant().int64Value()))); + }; + } + + private static boolean isNonNegativeInt(CelExpr expr) { + return expr.exprKind().getKind().equals(CelExpr.ExprKind.Kind.CONSTANT) + && expr.constant().getKind().equals(CelConstant.Kind.INT64_VALUE) + && expr.constant().int64Value() >= 0; + } + + private static boolean isCelNamespace(CelExpr target) { + return target.exprKind().getKind().equals(CelExpr.ExprKind.Kind.IDENT) + && target.ident().name().equals("cel"); + } + } } diff --git a/extensions/BUILD.bazel b/extensions/BUILD.bazel index dea4cd760..f9c2aee45 100644 --- a/extensions/BUILD.bazel +++ b/extensions/BUILD.bazel @@ -61,3 +61,9 @@ java_library( name = "native", exports = ["//extensions/src/main/java/dev/cel/extensions:native"], ) + +java_library( + name = "bindings", + visibility = ["//:internal"], + exports = ["//extensions/src/main/java/dev/cel/extensions:bindings"], +) diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel index ba57a07c3..8b7991cc0 100644 --- a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel @@ -145,6 +145,7 @@ java_library( deps = [ "//common:compiler_common", "//common/ast", + "//common/ast:cel_block", "//common/types", "//compiler:compiler_builder", "//extensions:extension_library", diff --git a/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java index 0e6537334..9fea7f481 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java @@ -23,6 +23,7 @@ import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelIssue; import dev.cel.common.CelOverloadDecl; +import dev.cel.common.ast.CelBlock; import dev.cel.common.ast.CelExpr; import dev.cel.common.types.ListType; import dev.cel.common.types.SimpleType; @@ -59,6 +60,15 @@ static CelExtensionLibrary library() { return LIBRARY; } + public static final CelFunctionDecl CEL_BLOCK_FUNCTION_DECL = + CelFunctionDecl.newFunctionDeclaration( + CelBlock.FUNCTION_NAME, + CelOverloadDecl.newGlobalOverload( + "cel_block_list", + TypeParamType.create("T"), + ListType.create(SimpleType.DYN), + TypeParamType.create("T"))); + @Override public int version() { return 0; @@ -67,14 +77,7 @@ public int version() { @Override public ImmutableSet functions() { // TODO: Add bindings for block once decorator support is available. - return ImmutableSet.of( - CelFunctionDecl.newFunctionDeclaration( - "cel.@block", - CelOverloadDecl.newGlobalOverload( - "cel_block_list", - TypeParamType.create("T"), - ListType.create(SimpleType.DYN), - TypeParamType.create("T")))); + return ImmutableSet.of(CEL_BLOCK_FUNCTION_DECL); } @Override diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel index 1012b19c2..0e6509c44 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -73,6 +73,7 @@ java_library( "//common/navigation:mutable_navigation", "//common/types", "//common/types:type_providers", + "//extensions:bindings", "//optimizer:ast_optimizer", "//optimizer:mutable_ast", "@maven//:com_google_errorprone_error_prone_annotations", diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java index 6a9860750..6d671b162 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java @@ -34,7 +34,6 @@ import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelMutableAst; import dev.cel.common.CelMutableSource; -import dev.cel.common.CelOverloadDecl; import dev.cel.common.CelSource; import dev.cel.common.CelSource.Extension; import dev.cel.common.CelSource.Extension.Component; @@ -55,8 +54,8 @@ import dev.cel.common.navigation.CelNavigableMutableExpr; import dev.cel.common.navigation.TraversalOrder; import dev.cel.common.types.CelType; -import dev.cel.common.types.ListType; import dev.cel.common.types.SimpleType; +import dev.cel.extensions.CelBindingsExtensions; import dev.cel.optimizer.AstMutator; import dev.cel.optimizer.AstMutator.MangledComprehensionAst; import dev.cel.optimizer.CelAstOptimizer; @@ -98,8 +97,6 @@ public final class SubexpressionOptimizer implements CelAstOptimizer { private static final SubexpressionOptimizer INSTANCE = new SubexpressionOptimizer(SubexpressionOptimizerOptions.newBuilder().build()); private static final String BIND_IDENTIFIER_PREFIX = "@r"; - private static final String CEL_BLOCK_FUNCTION = "cel.@block"; - private static final String BLOCK_INDEX_PREFIX = "@index"; private static final Extension CEL_BLOCK_AST_EXTENSION_TAG = Extension.create("cel_block", Version.of(1L, 1L), Component.COMPONENT_RUNTIME); @@ -165,7 +162,7 @@ private OptimizationResult optimizeUsingCelBlock(CelAbstractSyntaxTree ast, Cel CelMutableExpr targetCseShape = normalizeForEquality(cseCandidates.get(0)); subexpressions.add(cseCandidates.get(0)); - String blockIdentifier = BLOCK_INDEX_PREFIX + blockIdentifierIndex++; + String blockIdentifier = CelBlock.INDEX_PREFIX + blockIdentifierIndex++; // Replace all CSE candidates with new block index identifier astToModify = @@ -217,7 +214,7 @@ private OptimizationResult optimizeUsingCelBlock(CelAbstractSyntaxTree ast, Cel // Wrap the optimized expression in cel.block astToModify = - astMutator.wrapAstWithNewCelBlock(CEL_BLOCK_FUNCTION, astToModify, subexpressions); + astMutator.wrapAstWithNewCelBlock(CelBlock.FUNCTION_NAME, astToModify, subexpressions); astToModify = astMutator.renumberIdsConsecutively(astToModify); // Tag the AST with cel.block designated as an extension @@ -226,7 +223,7 @@ private OptimizationResult optimizeUsingCelBlock(CelAbstractSyntaxTree ast, Cel return OptimizationResult.create( optimizedAst, newVarDecls.build(), - ImmutableList.of(newCelBlockFunctionDecl(ast.getResultType()))); + ImmutableList.of(CelBindingsExtensions.CEL_BLOCK_FUNCTION_DECL)); } /** @@ -595,11 +592,8 @@ private CelMutableExpr normalizeForEquality(CelMutableExpr mutableExpr) { } @VisibleForTesting - static CelFunctionDecl newCelBlockFunctionDecl(CelType resultType) { - return CelFunctionDecl.newFunctionDeclaration( - CEL_BLOCK_FUNCTION, - CelOverloadDecl.newGlobalOverload( - "cel_block_list", resultType, ListType.create(SimpleType.DYN), resultType)); + static CelFunctionDecl newCelBlockFunctionDecl(CelType unusedResultType) { + return CelBindingsExtensions.CEL_BLOCK_FUNCTION_DECL; } /** Options to configure how Common Subexpression Elimination behave. */ From 05bf69c51488ed65ed21aa8ea8eca309366c518f Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Fri, 14 Aug 2026 15:59:06 -0700 Subject: [PATCH 178/204] Add shorthand type specifier syntax for policy configs PiperOrigin-RevId: 964936007 --- .../src/main/java/dev/cel/bundle/BUILD.bazel | 2 + .../java/dev/cel/bundle/CelEnvironment.java | 13 + .../cel/bundle/CelEnvironmentYamlParser.java | 33 +- .../dev/cel/bundle/TypeSpecifierParser.java | 200 ++++++++++++ .../bundle/CelEnvironmentYamlParserTest.java | 296 +++++++++++++++++- .../cel/bundle/TypeSpecifierParserTest.java | 250 +++++++++++++++ .../cel/compiler/tools/CelCompilerTool.java | 5 +- .../cel/policy/CelPolicyCompilerImplTest.java | 40 ++- .../resources/environment/extended_env.yaml | 27 ++ 9 files changed, 844 insertions(+), 22 deletions(-) create mode 100644 bundle/src/main/java/dev/cel/bundle/TypeSpecifierParser.java create mode 100644 bundle/src/test/java/dev/cel/bundle/TypeSpecifierParserTest.java diff --git a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel index 1c11bb34e..be4fade3d 100644 --- a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel +++ b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel @@ -97,6 +97,7 @@ java_library( name = "environment", srcs = [ "CelEnvironment.java", + "TypeSpecifierParser.java", ], tags = [ ], @@ -111,6 +112,7 @@ java_library( "//common:container", "//common:options", "//common:source", + "//common/formats:parser_context", "//common/types", "//common/types:type_providers", "//compiler:compiler_builder", diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java index 6b4684b27..f26d4e3fd 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java @@ -692,6 +692,19 @@ public static TypeDecl create(String name) { return newBuilder().setName(name).build(); } + /** + * Parses a type specifier shorthand string (e.g. {@code "list"}, {@code "map"}, {@code "list<~T>"}) into a {@link TypeDecl}. + */ + static TypeDecl parse(String typeSpecifier) { + return TypeSpecifierParser.parse(typeSpecifier); + } + + /** Creates a new {@link TypeDecl} representing a type parameter with the provided name. */ + static TypeDecl ofTypeParam(String typeParamName) { + return newBuilder().setName(typeParamName).setIsTypeParam(true).build(); + } + public static TypeDecl.Builder newBuilder() { return new AutoValue_CelEnvironment_TypeDecl.Builder().setIsTypeParam(false); } diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java index 14f1c93d8..821ca6586 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java @@ -22,7 +22,6 @@ import static dev.cel.common.formats.YamlHelper.newString; import static dev.cel.common.formats.YamlHelper.parseYamlSource; import static dev.cel.common.formats.YamlHelper.validateYamlType; -import static java.util.Collections.singletonList; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -60,7 +59,7 @@ */ public final class CelEnvironmentYamlParser { // Sentinel values to be returned for various declarations when parsing failure is encountered. - private static final TypeDecl ERROR_TYPE_DECL = TypeDecl.create(ERROR); + private static final TypeDecl ERROR_TYPE_DECL = TypeSpecifierParser.ERROR_TYPE_DECL; private static final VariableDecl ERROR_VARIABLE_DECL = VariableDecl.create(ERROR, ERROR_TYPE_DECL); private static final FunctionDecl ERROR_FUNCTION_DECL = @@ -335,6 +334,7 @@ private ContextVariable parseContextVariable(ParserContext ctx, Node node) Node valueNode = nodeTuple.getValueNode(); String keyName = ((ScalarNode) keyNode).getValue(); switch (keyName) { + case "type": case "type_name": typeName = newString(ctx, valueNode); break; @@ -478,7 +478,7 @@ private FunctionDecl parseFunction(ParserContext ctx, Node node) { return builder.build(); } - private static ImmutableSet parseOverloads(ParserContext ctx, Node node) { + private ImmutableSet parseOverloads(ParserContext ctx, Node node) { long listId = ctx.collectMetadata(node); ImmutableSet.Builder overloadSetBuilder = ImmutableSet.builder(); if (!assertYamlType(ctx, listId, node, YamlNodeType.LIST)) { @@ -553,8 +553,7 @@ private static ImmutableList parseOverloadExamples(ParserContext c return builder.build(); } - private static ImmutableList parseOverloadArguments( - ParserContext ctx, Node node) { + private ImmutableList parseOverloadArguments(ParserContext ctx, Node node) { long listValueId = ctx.collectMetadata(node); if (!assertYamlType(ctx, listValueId, node, YamlNodeType.LIST)) { return ImmutableList.of(); @@ -791,7 +790,7 @@ private static ImmutableSet parseFunctionOverloadsSelector( } @CanIgnoreReturnValue - private static TypeDecl.Builder parseInlinedTypeDecl( + private TypeDecl.Builder parseInlinedTypeDecl( ParserContext ctx, long keyId, Node keyNode, Node valueNode, TypeDecl.Builder builder) { if (!assertYamlType(ctx, keyId, keyNode, YamlNodeType.STRING, YamlNodeType.TEXT)) { return builder; @@ -800,24 +799,28 @@ private static TypeDecl.Builder parseInlinedTypeDecl( // Create a synthetic node to make this behave as if a `type: ` parent node actually exists. MappingNode mapNode = new MappingNode( - Tag.MAP, /* value= */ singletonList(new NodeTuple(keyNode, valueNode)), FlowStyle.AUTO); + Tag.MAP, + /* value= */ ImmutableList.of(new NodeTuple(keyNode, valueNode)), + FlowStyle.AUTO); return parseTypeDeclFields(ctx, mapNode, builder); } - private static TypeDecl parseTypeDecl(ParserContext ctx, Node node) { - TypeDecl.Builder builder = TypeDecl.newBuilder(); + private TypeDecl parseTypeDecl(ParserContext ctx, Node node) { long id = ctx.collectMetadata(node); - if (!assertYamlType(ctx, id, node, YamlNodeType.MAP)) { - return ERROR_TYPE_DECL; + if (validateYamlType(node, YamlNodeType.STRING, YamlNodeType.TEXT)) { + return TypeSpecifierParser.parse(ctx, id, newString(ctx, node)); } - - MappingNode mapNode = (MappingNode) node; - return parseTypeDeclFields(ctx, mapNode, builder).build(); + if (validateYamlType(node, YamlNodeType.MAP)) { + TypeDecl.Builder builder = TypeDecl.newBuilder(); + return parseTypeDeclFields(ctx, (MappingNode) node, builder).build(); + } + assertYamlType(ctx, id, node, YamlNodeType.STRING, YamlNodeType.TEXT, YamlNodeType.MAP); + return ERROR_TYPE_DECL; } @CanIgnoreReturnValue - private static TypeDecl.Builder parseTypeDeclFields( + private TypeDecl.Builder parseTypeDeclFields( ParserContext ctx, MappingNode mapNode, TypeDecl.Builder builder) { for (NodeTuple nodeTuple : mapNode.getValue()) { Node keyNode = nodeTuple.getKeyNode(); diff --git a/bundle/src/main/java/dev/cel/bundle/TypeSpecifierParser.java b/bundle/src/main/java/dev/cel/bundle/TypeSpecifierParser.java new file mode 100644 index 000000000..ca96a955f --- /dev/null +++ b/bundle/src/main/java/dev/cel/bundle/TypeSpecifierParser.java @@ -0,0 +1,200 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.bundle; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import dev.cel.bundle.CelEnvironment.TypeDecl; +import dev.cel.common.formats.ParserContext; + +/** + * Parses a type specifier shorthand string (e.g. {@code "map"}, {@code "list<~T>"}, + * {@code "int"}) into a {@link TypeDecl}. + */ +final class TypeSpecifierParser { + private static final int MAX_RECURSION_DEPTH = 64; + static final TypeDecl ERROR_TYPE_DECL = TypeDecl.create("*error*"); + + private final String text; + private final int length; + private int pos; + + static TypeDecl parse(String text) { + checkNotNull(text); + TypeSpecifierParser parser = new TypeSpecifierParser(text); + return parser.parse(); + } + + static TypeDecl parse(ParserContext ctx, long nodeId, String text) { + checkNotNull(ctx); + checkNotNull(text); + try { + return parse(text); + } catch (IllegalArgumentException e) { + ctx.reportError(nodeId, e.getMessage()); + return ERROR_TYPE_DECL; + } + } + + private TypeDecl parse() { + TypeDecl res = parseTypeElem(0); + skipWhitespace(); + if (pos < length) { + throw new IllegalArgumentException( + String.format( + "unexpected character '%c' at position %d in %s", + text.charAt(pos), pos, formatQuoted(text))); + } + return res; + } + + private TypeSpecifierParser(String text) { + this.text = text; + this.length = text.length(); + this.pos = 0; + } + + private TypeDecl parseTypeElem(int depth) { + if (depth > MAX_RECURSION_DEPTH) { + throw new IllegalArgumentException( + String.format("exceeded maximum type specifier recursion depth at position %d", pos)); + } + skipWhitespace(); + if (pos < length && text.charAt(pos) == '~') { + pos++; // consume '~' + String id = parseTypeParamIdent(); + return TypeDecl.ofTypeParam(id); + } + return parseConcreteType(depth); + } + + private TypeDecl parseConcreteType(int depth) { + String id = parseNamespaceIdentifier(); + skipWhitespace(); + if (pos < length && text.charAt(pos) == '<') { + pos++; // consume '<' + ImmutableList.Builder params = ImmutableList.builder(); + while (true) { + TypeDecl param = parseTypeElem(depth + 1); + params.add(param); + skipWhitespace(); + if (pos < length && text.charAt(pos) == ',') { + pos++; // consume ',' + continue; + } + if (pos < length && text.charAt(pos) == '>') { + pos++; // consume '>' + break; + } + throw new IllegalArgumentException( + String.format("expected ',' or '>' at position %d", pos)); + } + return TypeDecl.newBuilder().setName(id).addParams(params.build()).build(); + } + return TypeDecl.create(id); + } + + private String parseNamespaceIdentifier() { + StringBuilder id = new StringBuilder(); + while (pos < length && text.charAt(pos) != '<') { + char c = text.charAt(pos); + if (c == '.') { + id.append('.'); + pos++; // consume '.' + } + String ident = parseIdentifier(); + id.append(ident); + if (pos < length && text.charAt(pos) != '.') { + break; + } + } + String identifier = id.toString(); + if (identifier.isEmpty()) { + throw new IllegalArgumentException(String.format("missing identifier at position %d", pos)); + } + return identifier; + } + + private String parseIdentifier() { + if (pos >= length) { + throw new IllegalArgumentException("unexpected end of input"); + } + int start = pos; + while (pos < length) { + char c = text.charAt(pos); + boolean isValid = (pos == start) ? (isAlpha(c) || c == '_') : (isAlphaNumeric(c) || c == '_'); + if (isValid) { + pos++; + continue; + } + if (pos == start) { + throw new IllegalArgumentException( + String.format("identifier is expected, but '%c' was found at position %d", c, pos)); + } + break; + } + return text.substring(start, pos); + } + + private String parseTypeParamIdent() { + if (pos >= length) { + throw new IllegalArgumentException("unexpected end of input"); + } + char c = text.charAt(pos); + if (c < 'A' || c > 'Z') { + throw new IllegalArgumentException( + String.format( + "invalid type parameter identifier '%c' at position %d, must be a single character" + + " from A-Z", + c, pos)); + } + pos++; + if (pos < length) { + char next = text.charAt(pos); + if (isAlphaNumeric(next) || next == '_') { + throw new IllegalArgumentException( + String.format( + "invalid type parameter identifier '%c' at position %d, must be a single character" + + " from A-Z", + next, pos)); + } + } + return String.valueOf(c); + } + + private void skipWhitespace() { + while (pos < length) { + char c = text.charAt(pos); + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { + pos++; + } else { + break; + } + } + } + + private static boolean isAlpha(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + } + + private static boolean isAlphaNumeric(char c) { + return isAlpha(c) || (c >= '0' && c <= '9'); + } + + private static String formatQuoted(String s) { + return "\"" + s + "\""; + } +} diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java index 043664e8e..9a07a854d 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java @@ -19,12 +19,14 @@ import static org.junit.Assert.assertThrows; import com.google.common.base.Ascii; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; import com.google.rpc.context.AttributeContext; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.bundle.CelEnvironment.ContextVariable; import dev.cel.bundle.CelEnvironment.ExtensionConfig; import dev.cel.bundle.CelEnvironment.FunctionDecl; import dev.cel.bundle.CelEnvironment.LibrarySubset; @@ -378,6 +380,256 @@ public void environment_setMessageVariable() throws Exception { assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull(); } + @Test + public void environment_setListVariable_shorthand() throws Exception { + String yamlConfig = + "variables:\n" // + + "- name: 'request'\n" // + + " type: 'list'"; + + CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig); + + assertThat(environment) + .isEqualTo( + CelEnvironment.newBuilder() + .setSource(environment.source().get()) + .setVariables( + ImmutableSet.of( + VariableDecl.create( + "request", + TypeDecl.newBuilder() + .setName("list") + .addParams(TypeDecl.create("string")) + .build()))) + .build()); + assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull(); + } + + @Test + public void environment_setMapVariable_shorthand() throws Exception { + String yamlConfig = + "variables:\n" // + + "- name: 'request'\n" // + + " type: 'map'"; + + CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig); + + assertThat(environment) + .isEqualTo( + CelEnvironment.newBuilder() + .setSource(environment.source().get()) + .setVariables( + ImmutableSet.of( + VariableDecl.create( + "request", + TypeDecl.newBuilder() + .setName("map") + .addParams(TypeDecl.create("string"), TypeDecl.create("dyn")) + .build()))) + .build()); + assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull(); + } + + @Test + public void environment_withTypeSpecifiersEnabled_handlesStructuredMapTypeDecl() + throws Exception { + String yamlConfig = + "variables:\n" // + + "- name: 'request'\n" // + + " type:\n" // + + " type_name: 'map'\n" // + + " params:\n" // + + " - type_name: 'string'\n" // + + " - type_name: 'dyn'"; + + CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig); + + assertThat(environment) + .isEqualTo( + CelEnvironment.newBuilder() + .setSource(environment.source().get()) + .setVariables( + ImmutableSet.of( + VariableDecl.create( + "request", + TypeDecl.newBuilder() + .setName("map") + .addParams(TypeDecl.create("string"), TypeDecl.create("dyn")) + .build()))) + .build()); + assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull(); + } + + @Test + public void environment_withTypeSpecifiersEnabled_handlesBlockScalarTextTypeDecl() + throws Exception { + String yamlConfig = + "variables:\n" // + + "- name: 'request'\n" // + + " type: >-\n" // + + " list"; + + CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig); + + assertThat(environment) + .isEqualTo( + CelEnvironment.newBuilder() + .setSource(environment.source().get()) + .setVariables( + ImmutableSet.of( + VariableDecl.create( + "request", + TypeDecl.newBuilder() + .setName("list") + .addParams(TypeDecl.create("string")) + .build()))) + .build()); + assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull(); + } + + @Test + public void environment_setMessageVariable_shorthand() throws Exception { + String yamlConfig = + "variables:\n" // + + "- name: 'request'\n" // + + " type: 'google.rpc.context.AttributeContext.Request'"; + + CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig); + + assertThat(environment) + .isEqualTo( + CelEnvironment.newBuilder() + .setSource(environment.source().get()) + .setVariables( + ImmutableSet.of( + VariableDecl.create( + "request", + TypeDecl.create("google.rpc.context.AttributeContext.Request")))) + .build()); + assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull(); + } + + @Test + public void environment_setContextVariable_type() throws Exception { + String yamlConfig = + "context_variable:\n" // + + " type: 'google.rpc.context.AttributeContext.Request'"; + + CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig); + + assertThat(environment) + .isEqualTo( + CelEnvironment.newBuilder() + .setSource(environment.source().get()) + .setContextVariable( + ContextVariable.create("google.rpc.context.AttributeContext.Request")) + .build()); + assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull(); + } + + @Test + public void environment_setFunctions_shorthand() throws Exception { + String yamlConfig = + "functions:\n" // + + "- name: 'isEmpty'\n" // + + " overloads:\n" // + + " - id: 'list_isEmpty'\n" // + + " target: 'list<~T>'\n" // + + " return: 'bool'\n" // + + "- name: 'getOrDefault'\n" // + + " overloads:\n" // + + " - id: 'map_getOrDefault'\n" // + + " target: 'map<~K, ~V>'\n" // + + " args:\n" // + + " - '~K'\n" // + + " - '~V'\n" // + + " return: '~V'"; + + CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig); + + assertThat(environment) + .isEqualTo( + CelEnvironment.newBuilder() + .setSource(environment.source().get()) + .setFunctions( + ImmutableSet.of( + FunctionDecl.create( + "isEmpty", + ImmutableSet.of( + OverloadDecl.newBuilder() + .setId("list_isEmpty") + .setTarget( + TypeDecl.newBuilder() + .setName("list") + .addParams(TypeDecl.ofTypeParam("T")) + .build()) + .setReturnType(TypeDecl.create("bool")) + .build())), + FunctionDecl.create( + "getOrDefault", + ImmutableSet.of( + OverloadDecl.newBuilder() + .setId("map_getOrDefault") + .setTarget( + TypeDecl.newBuilder() + .setName("map") + .addParams( + TypeDecl.ofTypeParam("K"), + TypeDecl.ofTypeParam("V")) + .build()) + .addArguments( + TypeDecl.ofTypeParam("K"), TypeDecl.ofTypeParam("V")) + .setReturnType(TypeDecl.ofTypeParam("V")) + .build())))) + .build()); + assertThat(environment.extend(CEL_WITH_MESSAGE_TYPES, CelOptions.DEFAULT)).isNotNull(); + } + + @Test + public void environment_withTypeSpecifier_invalidSyntaxError() { + String yamlConfig = + "variables:\n" // + + "- name: 'request'\n" // + + " type: 'list<'"; + + CelEnvironmentException e = + assertThrows(CelEnvironmentException.class, () -> ENVIRONMENT_PARSER.parse(yamlConfig)); + assertThat(e).hasMessageThat().contains("missing identifier at position 5"); + } + + @Test + public void environment_withTypeSpecifier_invalidYamlNodeError() { + String yamlConfig = + "variables:\n" // + + "- name: 'request'\n" // + + " type: 1"; + + CelEnvironmentException e = + assertThrows(CelEnvironmentException.class, () -> ENVIRONMENT_PARSER.parse(yamlConfig)); + assertThat(e) + .hasMessageThat() + .contains("wanted type(s) [tag:yaml.org,2002:str !txt tag:yaml.org,2002:map]"); + } + + @Test + public void environment_evaluatesShorthandVariable() throws Exception { + String yamlConfig = + "variables:\n" // + + "- name: 'values'\n" // + + " type: 'list'"; + + CelEnvironment environment = ENVIRONMENT_PARSER.parse(yamlConfig); + Cel cel = environment.extend(CelFactory.standardCelBuilder().build(), CelOptions.DEFAULT); + + CelAbstractSyntaxTree ast = cel.compile("values.size() == 2 && values[0] == 'hello'").getAst(); + boolean result = + (boolean) + cel.createProgram(ast) + .eval( + ImmutableMap.of("values", ImmutableList.of("hello", "world"))); + assertThat(result).isTrue(); + } + @Test public void environment_setContainer() throws Exception { String yamlConfig = @@ -577,7 +829,7 @@ private enum EnvironmentParseErrorTestcase { + " - name: foo\n" // + " type: 1", "ERROR: :3:10: Got yaml node type tag:yaml.org,2002:int, wanted type(s)" - + " [tag:yaml.org,2002:map]\n" + + " [tag:yaml.org,2002:str !txt tag:yaml.org,2002:map]\n" + " | type: 1\n" + " | .........^"), ILLEGAL_YAML_TYPE_TYPE_VALUE( @@ -890,6 +1142,48 @@ private enum EnvironmentYamlResourceTestCase { .build()) .setReturnType(TypeDecl.create("bool")) .build())) + .build(), + FunctionDecl.newBuilder() + .setName("isEmptyAlt") + .setDescription( + "determines whether a list is empty,\nor a string has no characters") + .setOverloads( + ImmutableSet.of( + OverloadDecl.newBuilder() + .setId("wrapper_string_isEmpty") + .setTarget(TypeDecl.create("google.protobuf.StringValue")) + .addExamples("''.isEmptyAlt() // true") + .setReturnType(TypeDecl.create("bool")) + .build(), + OverloadDecl.newBuilder() + .setId("list_isEmpty") + .addExamples("[].isEmptyAlt() // true") + .addExamples("[1].isEmptyAlt() // false") + .setTarget( + TypeDecl.newBuilder() + .setName("list") + .addParams(TypeDecl.ofTypeParam("T")) + .build()) + .setReturnType(TypeDecl.create("bool")) + .build())) + .build(), + FunctionDecl.newBuilder() + .setName("getOrDefault") + .setDescription( + "Returns the value of a key in a map or the provided\ndefault value.") + .setOverloads( + ImmutableSet.of( + OverloadDecl.newBuilder() + .setId("map_getOrDefault") + .setTarget( + TypeDecl.newBuilder() + .setName("map") + .addParams( + TypeDecl.ofTypeParam("K"), TypeDecl.ofTypeParam("V")) + .build()) + .addArguments(TypeDecl.ofTypeParam("K"), TypeDecl.ofTypeParam("V")) + .setReturnType(TypeDecl.ofTypeParam("V")) + .build())) .build()) .setFeatures(CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true)) .setLimits( diff --git a/bundle/src/test/java/dev/cel/bundle/TypeSpecifierParserTest.java b/bundle/src/test/java/dev/cel/bundle/TypeSpecifierParserTest.java new file mode 100644 index 000000000..ee3dab9c6 --- /dev/null +++ b/bundle/src/test/java/dev/cel/bundle/TypeSpecifierParserTest.java @@ -0,0 +1,250 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.bundle; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.bundle.CelEnvironment.TypeDecl; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class TypeSpecifierParserTest { + + @Test + public void parse_concreteSimpleType() { + assertThat(TypeDecl.parse("int")).isEqualTo(TypeDecl.create("int")); + assertThat(TypeDecl.parse("string")).isEqualTo(TypeDecl.create("string")); + assertThat(TypeDecl.parse("bool")).isEqualTo(TypeDecl.create("bool")); + assertThat(TypeDecl.parse("double")).isEqualTo(TypeDecl.create("double")); + assertThat(TypeDecl.parse("uint")).isEqualTo(TypeDecl.create("uint")); + assertThat(TypeDecl.parse("bytes")).isEqualTo(TypeDecl.create("bytes")); + assertThat(TypeDecl.parse("duration")).isEqualTo(TypeDecl.create("duration")); + assertThat(TypeDecl.parse("timestamp")).isEqualTo(TypeDecl.create("timestamp")); + assertThat(TypeDecl.parse("dyn")).isEqualTo(TypeDecl.create("dyn")); + assertThat(TypeDecl.parse("any")).isEqualTo(TypeDecl.create("any")); + assertThat(TypeDecl.parse("null_type")).isEqualTo(TypeDecl.create("null_type")); + } + + @Test + public void parse_qualifiedMessageType() { + assertThat(TypeDecl.parse("google.protobuf.StringValue")) + .isEqualTo(TypeDecl.create("google.protobuf.StringValue")); + assertThat(TypeDecl.parse("google.rpc.context.AttributeContext.Request")) + .isEqualTo(TypeDecl.create("google.rpc.context.AttributeContext.Request")); + assertThat(TypeDecl.parse(".com.example.Message")) + .isEqualTo(TypeDecl.create(".com.example.Message")); + } + + @Test + public void parse_parameterizedTypes() { + assertThat(TypeDecl.parse("list")) + .isEqualTo(TypeDecl.newBuilder().setName("list").addParams(TypeDecl.create("int")).build()); + assertThat(TypeDecl.parse("map")) + .isEqualTo( + TypeDecl.newBuilder() + .setName("map") + .addParams(TypeDecl.create("string"), TypeDecl.create("dyn")) + .build()); + assertThat(TypeDecl.parse("optional_type")) + .isEqualTo( + TypeDecl.newBuilder() + .setName("optional_type") + .addParams(TypeDecl.create("string")) + .build()); + assertThat(TypeDecl.parse("type")) + .isEqualTo(TypeDecl.newBuilder().setName("type").addParams(TypeDecl.create("int")).build()); + } + + @Test + public void parse_nestedParameterizedTypes() { + assertThat(TypeDecl.parse("map>")) + .isEqualTo( + TypeDecl.newBuilder() + .setName("map") + .addParams( + TypeDecl.create("int"), + TypeDecl.newBuilder() + .setName("list") + .addParams(TypeDecl.create("string")) + .build()) + .build()); + + assertThat(TypeDecl.parse("list>>")) + .isEqualTo( + TypeDecl.newBuilder() + .setName("list") + .addParams( + TypeDecl.newBuilder() + .setName("map") + .addParams( + TypeDecl.create("string"), + TypeDecl.newBuilder() + .setName("optional_type") + .addParams(TypeDecl.create("int")) + .build()) + .build()) + .build()); + } + + @Test + public void parse_whitespaceTolerance() { + assertThat(TypeDecl.parse(" list < int > ")) + .isEqualTo(TypeDecl.newBuilder().setName("list").addParams(TypeDecl.create("int")).build()); + assertThat(TypeDecl.parse(" map < string , list < int > > ")) + .isEqualTo( + TypeDecl.newBuilder() + .setName("map") + .addParams( + TypeDecl.create("string"), + TypeDecl.newBuilder().setName("list").addParams(TypeDecl.create("int")).build()) + .build()); + } + + @Test + public void parse_whitespaceWithTabsAndNewlines() { + assertThat(TypeDecl.parse("list<\tstring\n>")) + .isEqualTo( + TypeDecl.newBuilder().setName("list").addParams(TypeDecl.create("string")).build()); + assertThat(TypeDecl.parse(" map < string ,\t int > ")) + .isEqualTo( + TypeDecl.newBuilder() + .setName("map") + .addParams(TypeDecl.create("string"), TypeDecl.create("int")) + .build()); + assertThat(TypeDecl.parse("map\t<\nint\r,\tstring\n>\r")) + .isEqualTo( + TypeDecl.newBuilder() + .setName("map") + .addParams(TypeDecl.create("int"), TypeDecl.create("string")) + .build()); + assertThat(TypeDecl.parse("\tlist\n<\r~T\t>\n")) + .isEqualTo( + TypeDecl.newBuilder().setName("list").addParams(TypeDecl.ofTypeParam("T")).build()); + } + + @Test + public void parse_typeParameters() { + assertThat(TypeDecl.parse("~T")).isEqualTo(TypeDecl.ofTypeParam("T")); + assertThat(TypeDecl.parse(" ~T ")).isEqualTo(TypeDecl.ofTypeParam("T")); + assertThat(TypeDecl.parse("list<~T>")) + .isEqualTo( + TypeDecl.newBuilder().setName("list").addParams(TypeDecl.ofTypeParam("T")).build()); + assertThat(TypeDecl.parse("list< ~T >")) + .isEqualTo( + TypeDecl.newBuilder().setName("list").addParams(TypeDecl.ofTypeParam("T")).build()); + assertThat(TypeDecl.parse("map<~K, ~V>")) + .isEqualTo( + TypeDecl.newBuilder() + .setName("map") + .addParams(TypeDecl.ofTypeParam("K"), TypeDecl.ofTypeParam("V")) + .build()); + assertThat(TypeDecl.parse("map< ~K , ~V >")) + .isEqualTo( + TypeDecl.newBuilder() + .setName("map") + .addParams(TypeDecl.ofTypeParam("K"), TypeDecl.ofTypeParam("V")) + .build()); + } + + @Test + public void parse_maxRecursionDepth_succeedsAtBoundary() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 64; i++) { + sb.append("list<"); + } + sb.append("int"); + for (int i = 0; i < 64; i++) { + sb.append(">"); + } + String input = sb.toString(); + TypeDecl result = TypeDecl.parse(input); + assertThat(result).isNotNull(); + } + + @Test + public void parse_exceedsMaxRecursionDepth_throws() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 65; i++) { + sb.append("list<"); + } + sb.append("int"); + for (int i = 0; i < 65; i++) { + sb.append(">"); + } + String input = sb.toString(); + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> TypeDecl.parse(input)); + assertThat(e).hasMessageThat().contains("exceeded maximum type specifier recursion depth"); + } + + @Test + public void parse_errors(@TestParameter ParseErrorTestCase testCase) { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> TypeDecl.parse(testCase.input)); + assertThat(e).hasMessageThat().contains(testCase.expectedMessageSubstring); + } + + private enum ParseErrorTestCase { + EMPTY("", "missing identifier at position 0"), + TRAILING_CHARACTERS("int int", "unexpected character 'i' at position 4 in \"int int\""), + UNEXPECTED_CLOSING_BRACKET("int>", "unexpected character '>' at position 3 in \"int>\""), + TRAILING_DOT(".foo.", "unexpected end of input"), + CONSECUTIVE_DOTS("..foo", "identifier is expected, but '.' was found at position 1"), + EMPTY_TYPE_PARAM("~", "unexpected end of input"), + DIGIT_TYPE_PARAM( + "~1", + "invalid type parameter identifier '1' at position 1, must be a single character from A-Z"), + LOWERCASE_TYPE_PARAM( + "~t", + "invalid type parameter identifier 't' at position 1, must be a single character from A-Z"), + TYPE_PARAM_FOLLOWED_BY_NUMERIC( + "~T1", + "invalid type parameter identifier '1' at position 2, must be a single character from A-Z"), + TYPE_PARAM_FOLLOWED_BY_UNDERSCORE( + "~T_", + "invalid type parameter identifier '_' at position 2, must be a single character from A-Z"), + TYPE_PARAM_FOLLOWED_BY_LOWERCASE( + "~Telem", + "invalid type parameter identifier 'e' at position 2, must be a single character from A-Z"), + MULTI_CHAR_TYPE_PARAM( + "~elem", + "invalid type parameter identifier 'e' at position 1, must be a single character from A-Z"), + WHITESPACE_IN_IDENTIFIER( + "google. protobuf.StringValue", "identifier is expected, but ' ' was found at position 7"), + WHITESPACE_BEFORE_DOT( + "google .protobuf.StringValue", + "unexpected character '.' at position 7 in \"google .protobuf.StringValue\""), + EXTRA_CLOSING_BRACKET("list>", "unexpected character '>' at position 9 in \"list>\""), + CONSECUTIVE_OPENING_BRACKETS("list<", "missing identifier at position 5"), + TRAILING_COMMA("map", "identifier is expected, but '>' was found at position 11"), + EMPTY_GENERIC_PARAM("map<, int>", "identifier is expected, but ',' was found at position 4"), + UNFINISHED_GENERIC("list<", "missing identifier at position 5"), + TRAILING_COMMA_GENERIC("map", "identifier is expected, but '>' was found at position 9"), + UNCLOSED_GENERIC("map' at position 15"), + ; + + private final String input; + private final String expectedMessageSubstring; + + ParseErrorTestCase(String input, String expectedMessageSubstring) { + this.input = input; + this.expectedMessageSubstring = expectedMessageSubstring; + } + } +} diff --git a/compiler/src/main/java/dev/cel/compiler/tools/CelCompilerTool.java b/compiler/src/main/java/dev/cel/compiler/tools/CelCompilerTool.java index abe780c4a..f1d2d4f4b 100644 --- a/compiler/src/main/java/dev/cel/compiler/tools/CelCompilerTool.java +++ b/compiler/src/main/java/dev/cel/compiler/tools/CelCompilerTool.java @@ -14,6 +14,8 @@ package dev.cel.compiler.tools; +import static java.nio.charset.StandardCharsets.UTF_8; + import dev.cel.expr.CheckedExpr; import com.google.common.collect.ImmutableSet; import com.google.common.io.Files; @@ -34,7 +36,6 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Locale; @@ -95,7 +96,7 @@ private static CelCompiler prepareCompiler( } CelEnvironmentYamlParser environmentYamlParser = CelEnvironmentYamlParser.newInstance(); - String yamlContent = new String(readFileBytes(celEnvironmentPath), StandardCharsets.UTF_8); + String yamlContent = new String(readFileBytes(celEnvironmentPath), UTF_8); CelEnvironment environment = environmentYamlParser.parse(yamlContent); return environment.extend(celCompilerBuilder.build(), CEL_OPTIONS); diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java index 5f697e0b9..3fbc8720c 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java @@ -55,6 +55,7 @@ import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; import java.io.IOException; import java.net.URL; +import java.util.EnumSet; import java.util.Map; import java.util.Optional; import org.junit.Test; @@ -201,6 +202,37 @@ public void evaluateYamlPolicy_aggregate_cseApplied() throws Exception { assertThat(evalResultFalse).isEqualTo(ImmutableList.of("ALWAYS")); } + @Test + public void evaluateYamlPolicy_withShorthandTypeSpecifiersInEnvironment() throws Exception { + String configSource = + "variables:\n" // + + "- name: 'user_scores'\n" // + + " type: 'map'\n" // + + "- name: 'allowed_users'\n" // + + " type: 'list'\n"; + CelEnvironment celEnvironment = CelEnvironmentYamlParser.newInstance().parse(configSource); + Cel cel = celEnvironment.extend(newCel(), CEL_OPTIONS); + + String policySource = + "name: 'user_access_policy'\n" // + + "rule:\n" // + + " match:\n" // + + " - condition: \"user_scores['alice'] > 50 && 'alice' in allowed_users\"\n" // + + " output: 'true'\n"; + CelPolicy policy = POLICY_PARSER.parse(policySource); + + CelAbstractSyntaxTree ast = + CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy); + + Object evalResult = + cel.createProgram(ast) + .eval( + ImmutableMap.of( + "user_scores", ImmutableMap.of("alice", 95L), + "allowed_users", ImmutableList.of("alice", "bob"))); + assertThat(evalResult).isEqualTo(Optional.of(true)); + } + @Test public void compileYamlPolicy_aggregate_macrosPreserved() throws Exception { String policySource = @@ -398,7 +430,7 @@ public void compileYamlPolicy_astDepthLimitCheckDisabled_doesNotThrow() throws E } @Test - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") // Test only public void evaluateYamlPolicy_withCanonicalTestData( @TestParameter(valuesProvider = EvaluablePolicyTestDataProvider.class) EvaluablePolicyTestData testData) @@ -470,7 +502,7 @@ public void evaluateYamlPolicy_withCanonicalTestData( } @Test - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") // Test only public void evaluateYamlPolicy_nestedRuleProducesOptionalOutput() throws Exception { Cel cel = newCel(); String policySource = @@ -527,7 +559,7 @@ public void evaluateYamlPolicy_lateBoundFunction() throws Exception { String evalResult = (String) cel.createProgram(compiledPolicyAst) - .eval((unused) -> Optional.empty(), lateFunctionBindings); + .eval(unused -> Optional.empty(), lateFunctionBindings); assertThat(evalResult).isEqualTo("foo" + exampleValue); } @@ -587,7 +619,7 @@ private static final class EvaluablePolicyTestDataProvider extends TestParameter @Override protected ImmutableList provideValues(Context context) throws Exception { ImmutableList.Builder builder = ImmutableList.builder(); - for (TestYamlPolicy yamlPolicy : TestYamlPolicy.values()) { + for (TestYamlPolicy yamlPolicy : EnumSet.allOf(TestYamlPolicy.class)) { PolicyTestSuite testSuite = yamlPolicy.readTestYamlContent(); for (PolicyTestSection testSection : testSuite.getSection()) { for (PolicyTestCase testCase : testSection.getTests()) { diff --git a/testing/src/test/resources/environment/extended_env.yaml b/testing/src/test/resources/environment/extended_env.yaml index 9fc2d511d..f380f4ed2 100644 --- a/testing/src/test/resources/environment/extended_env.yaml +++ b/testing/src/test/resources/environment/extended_env.yaml @@ -49,6 +49,33 @@ functions: is_type_param: true return: type_name: "bool" +- name: "isEmptyAlt" + description: |- + determines whether a list is empty, + or a string has no characters + overloads: + - id: "wrapper_string_isEmpty" + examples: + - "''.isEmptyAlt() // true" + target: "google.protobuf.StringValue" + return: "bool" + - id: "list_isEmpty" + examples: + - "[].isEmptyAlt() // true" + - "[1].isEmptyAlt() // false" + target: "list<~T>" + return: "bool" +- name: "getOrDefault" + description: |- + Returns the value of a key in a map or the provided + default value. + overloads: + - id: "map_getOrDefault" + target: "map<~K, ~V>" + return: "~V" + args: + - "~K" + - "~V" features: - name: cel.feature.macro_call_tracking enabled: true From 849cb3e99093836e329d86182d8c67021e7172b4 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Mon, 17 Aug 2026 15:17:01 -0700 Subject: [PATCH 179/204] Avoid concatenating superfluous empty list for aggregate semantics Add more aggregate policy conformance test cases PiperOrigin-RevId: 966209180 --- .../java/dev/cel/policy/RuleComposer.java | 12 +++- .../cel/policy/CelPolicyCompilerImplTest.java | 58 ++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/policy/src/main/java/dev/cel/policy/RuleComposer.java b/policy/src/main/java/dev/cel/policy/RuleComposer.java index bf667cb93..f98152c62 100644 --- a/policy/src/main/java/dev/cel/policy/RuleComposer.java +++ b/policy/src/main/java/dev/cel/policy/RuleComposer.java @@ -148,8 +148,12 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule, boolean asList) private @Nullable Step createBaseStep(boolean returnList, boolean hasOptionalOutput) { if (returnList) { - // If the rule is evaluated as a list (AGGREGATE), the base case is an empty list. - return Step.newUnconditionalNonOptionalStep(newTrueLiteral(), newList()); + if (hasOptionalOutput) { + // If a nested rule inside an aggregate context has an optional output, the last result in + // the ternary should return an empty list to allow concatenation with other branches. + return Step.newUnconditionalNonOptionalStep(newTrueLiteral(), newList()); + } + return null; } if (hasOptionalOutput) { @@ -280,6 +284,10 @@ private Step combineAggregate(AstMutator astMutator, Step currentStep, Step accu conditionalListPart = currentListPart; } + if (accumulatedStep == null) { + return Step.newUnconditionalNonOptionalStep(trueCondition, conditionalListPart); + } + CelMutableAst concatenated = astMutator.newGlobalCall( Operator.ADD.getFunction(), conditionalListPart, accumulatedStep.expr); diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java index 3fbc8720c..b5894c6df 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java @@ -261,8 +261,62 @@ public void compileYamlPolicy_aggregate_macrosPreserved() throws Exception { String unparsed = CelUnparserFactory.newUnparser().unparse(ast); assertThat(unparsed) .isEqualTo( - "(cond ? [payload.filter(x, x > 10, x).exists(y, y % 2 == 0)] : []) " - + "+ ([payload.all(x, x > 0)] + [])"); + "(cond ? [payload.filter(x, x > 10).exists(y, y % 2 == 0)] : []) " + + "+ [payload.all(x, x > 0)]"); + } + + @Test + public void compileYamlPolicy_aggregateSingleMatch_noSuperfluousConcatenation() throws Exception { + String policySource = + "name: aggregate_single_match\n" + + "rule:\n" + + " aggregate:\n" + + " - condition: \"cond\"\n" + + " output: \"payload.filter(x, x > 10).exists(y, y % 2 == 0)\"\n"; + Cel cel = + newCel() + .toCelBuilder() + .addVar("cond", SimpleType.BOOL) + .addVar("payload", ListType.create(SimpleType.INT)) + .build(); + + CelPolicy policy = POLICY_PARSER.parse(policySource); + + CelAbstractSyntaxTree ast = + CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy); + + String unparsed = CelUnparserFactory.newUnparser().unparse(ast); + assertThat(unparsed).isEqualTo("cond ? [payload.filter(x, x > 10).exists(y, y % 2 == 0)] : []"); + } + + @Test + public void compileYamlPolicy_aggregateMultipleConditionalMatches_noSuperfluousConcatenation() + throws Exception { + String policySource = + "name: aggregate_multiple_conditional\n" + + "rule:\n" + + " aggregate:\n" + + " - condition: \"cond1\"\n" + + " output: \"payload.all(x, x > 0)\"\n" + + " - condition: \"cond2\"\n" + + " output: \"payload.exists(x, x == 0)\"\n"; + Cel cel = + newCel() + .toCelBuilder() + .addVar("cond1", SimpleType.BOOL) + .addVar("cond2", SimpleType.BOOL) + .addVar("payload", ListType.create(SimpleType.INT)) + .build(); + + CelPolicy policy = POLICY_PARSER.parse(policySource); + + CelAbstractSyntaxTree ast = + CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy); + + String unparsed = CelUnparserFactory.newUnparser().unparse(ast); + assertThat(unparsed) + .isEqualTo( + "(cond1 ? [payload.all(x, x > 0)] : []) + (cond2 ? [payload.exists(x, x == 0)] : [])"); } @Test From 5cf3ab3c9f0a2ccb4ac449527d41689ed6aa6879 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Mon, 17 Aug 2026 16:25:46 -0700 Subject: [PATCH 180/204] Prepare 0.14.0 release PiperOrigin-RevId: 966243304 --- BUILD.bazel | 1 + MODULE.bazel | 55 +++++++++++-------- README.md | 4 +- .../src/test/java/dev/cel/maven/BUILD.bazel | 2 + .../cel/policy/CelPolicyCompilerImplTest.java | 10 ++-- publish/BUILD.bazel | 22 +++++++- publish/cel_version.bzl | 2 +- repositories.bzl | 8 +-- verifier/tools/README.md | 31 ++++++++--- 9 files changed, 89 insertions(+), 46 deletions(-) diff --git a/BUILD.bazel b/BUILD.bazel index d2bf2124b..3ff862d3a 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -81,6 +81,7 @@ java_library( neverlink = 1, exports = [ "@maven//:com_google_auto_value_auto_value_annotations", + "@maven//:org_jspecify_jspecify", ], ) diff --git a/MODULE.bazel b/MODULE.bazel index 3dcf8b0e5..bec7543d6 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -16,23 +16,23 @@ module( name = "cel_java", ) -bazel_dep(name = "bazel_skylib", version = "1.9.0") -bazel_dep(name = "rules_jvm_external", version = "6.10") -bazel_dep(name = "protobuf", version = "33.4", repo_name = "com_google_protobuf") # see https://github.com/bazelbuild/rules_android/issues/373 -bazel_dep(name = "googleapis", version = "0.0.0-20260223-edfe7983", repo_name = "com_google_googleapis") +bazel_dep(name = "bazel_skylib", version = "1.9.2") +bazel_dep(name = "rules_jvm_external", version = "7.1") +bazel_dep(name = "protobuf", version = "35.1", repo_name = "com_google_protobuf") # see https://github.com/bazelbuild/rules_android/issues/373 +bazel_dep(name = "googleapis", version = "0.0.0-20260728-b8486a2f", repo_name = "com_google_googleapis") bazel_dep(name = "rules_pkg", version = "1.2.0") bazel_dep(name = "rules_license", version = "1.0.0") bazel_dep(name = "rules_proto", version = "7.1.0") -bazel_dep(name = "rules_java", version = "9.3.0") -bazel_dep(name = "rules_android", version = "0.7.1") -bazel_dep(name = "rules_shell", version = "0.6.1") -bazel_dep(name = "googleapis-java", version = "1.0.0") -bazel_dep(name = "cel-spec", version = "0.25.1", repo_name = "cel_spec") -bazel_dep(name = "rules_go", version = "0.50.1") +bazel_dep(name = "rules_java", version = "9.7.0") +bazel_dep(name = "rules_android", version = "0.7.3") +bazel_dep(name = "rules_shell", version = "0.8.0") +bazel_dep(name = "googleapis-java", version = "1.1.5") +bazel_dep(name = "cel-spec", version = "0.25.2", repo_name = "cel_spec") +bazel_dep(name = "rules_go", version = "0.62.0") # Required by cel-spec to satisfy gazelle transitive dependency go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk") -go_sdk.download(version = "1.23.0") +go_sdk.download(version = "1.26.5") switched_rules = use_extension("@com_google_googleapis//:extensions.bzl", "switched_rules") switched_rules.use_languages(java = True) @@ -40,13 +40,17 @@ use_repo(switched_rules, "com_google_googleapis_imports") maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") -GUAVA_VERSION = "33.5.0" +AUTO_VALUE_VERSION = "1.11.1" -TRUTH_VERSION = "1.4.4" +GUAVA_VERSION = "33.6.0" -PROTOBUF_JAVA_VERSION = "4.33.5" +JLINE_VERSION = "3.30.16" -CEL_VERSION = "0.13.1" +TRUTH_VERSION = "1.4.5" + +PROTOBUF_JAVA_VERSION = "4.35.1" + +CEL_VERSION = "0.14.0" # Compile only artifacts [ @@ -58,7 +62,7 @@ CEL_VERSION = "0.13.1" ) for group, artifact, version in [coord.split(":") for coord in [ "com.google.code.findbugs:annotations:3.0.1", - "com.google.errorprone:error_prone_annotations:2.42.0", + "com.google.errorprone:error_prone_annotations:2.50.0", ]] ] @@ -72,8 +76,8 @@ CEL_VERSION = "0.13.1" ) for group, artifact, version in [coord.split(":") for coord in [ "org.mockito:mockito-core:4.11.0", - "io.github.classgraph:classgraph:4.8.179", - "com.google.testparameterinjector:test-parameter-injector:1.18", + "io.github.classgraph:classgraph:4.8.186", + "com.google.testparameterinjector:test-parameter-injector:1.22", "com.google.guava:guava-testlib:" + GUAVA_VERSION + "-jre", "com.google.truth.extensions:truth-java8-extension:" + TRUTH_VERSION, "com.google.truth.extensions:truth-proto-extension:" + TRUTH_VERSION, @@ -86,8 +90,8 @@ maven.install( name = "maven", # keep sorted artifacts = [ - "com.google.auto.value:auto-value:1.11.0", - "com.google.auto.value:auto-value-annotations:1.11.0", + "com.google.auto.value:auto-value:" + AUTO_VALUE_VERSION, + "com.google.auto.value:auto-value-annotations:" + AUTO_VALUE_VERSION, "com.google.guava:guava:" + GUAVA_VERSION + "-jre", "com.google.protobuf:protobuf-java:" + PROTOBUF_JAVA_VERSION, "com.google.protobuf:protobuf-java-util:" + PROTOBUF_JAVA_VERSION, @@ -95,13 +99,14 @@ maven.install( "info.picocli:picocli:4.7.7", "org.antlr:antlr4-runtime:4.13.2", "org.freemarker:freemarker:2.3.34", - "org.jline:jline-reader:3.26.1", - "org.jline:jline-terminal:3.26.1", + "org.jline:jline-reader:" + JLINE_VERSION, + "org.jline:jline-terminal:" + JLINE_VERSION, "org.jspecify:jspecify:1.0.0", - "org.threeten:threeten-extra:1.8.0", - "org.yaml:snakeyaml:2.5", + "org.threeten:threeten-extra:1.10.0", + "org.yaml:snakeyaml:2.6", "tools.aqua:z3-turnkey:4.14.1", ], + known_contributing_modules = ["protobuf"], repositories = [ "https://maven.google.com", "https://repo1.maven.org/maven2", @@ -129,10 +134,12 @@ maven.install( "dev.cel:compiler:" + CEL_VERSION, "dev.cel:runtime:" + CEL_VERSION, ], + fail_on_missing_checksum = not CEL_VERSION.endswith("-SNAPSHOT"), repositories = [ "https://maven.google.com", "https://repo1.maven.org/maven2", "https://central.sonatype.com/repository/maven-snapshots/", + "m2local", ], ) use_repo(maven, "maven", "maven_android", "maven_conformance") diff --git a/README.md b/README.md index dbcceb7d8..bf81ad6dc 100644 --- a/README.md +++ b/README.md @@ -55,14 +55,14 @@ CEL-Java is available in Maven Central Repository. [Download the JARs here][8] o dev.cel cel - 0.13.1 + 0.14.0 ``` **Gradle** ```gradle -implementation 'dev.cel:cel:0.13.1' +implementation 'dev.cel:cel:0.14.0' ``` Then run this example: diff --git a/conformance/src/test/java/dev/cel/maven/BUILD.bazel b/conformance/src/test/java/dev/cel/maven/BUILD.bazel index 895339521..e06f78c0f 100644 --- a/conformance/src/test/java/dev/cel/maven/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/maven/BUILD.bazel @@ -20,6 +20,7 @@ MAVEN_RUNTIME_JAR_DEPS = [ java_test( name = "compiler_artifact_test", srcs = ["CompilerArtifactTest.java"], + tags = ["conformance_maven"], test_class = "dev.cel.maven.CompilerArtifactTest", deps = MAVEN_COMPILER_JAR_DEPS + [ @@ -33,6 +34,7 @@ java_test( java_test( name = "runtime_artifact_test", srcs = ["RuntimeArtifactTest.java"], + tags = ["conformance_maven"], test_class = "dev.cel.maven.RuntimeArtifactTest", deps = MAVEN_RUNTIME_JAR_DEPS + [ diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java index b5894c6df..db1a36dcf 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java @@ -798,12 +798,10 @@ private enum TestErrorYamlPolicy { DUPLICATE_VARIABLE("duplicate_variable"), IMPORT("import"), INCOMPATIBLE_OUTPUTS("incompatible_outputs"), - UNDECLARED_REFERENCE("undeclared_reference"); - // TODO: Re-enable once cel-policy OSS dependency is updated with aggregate - // testdata. - // AGGREGATE_ERRORS("aggregate_errors"), - // AGGREGATE_LIST_ERRORS("aggregate_list_errors"), - // AGGREGATE_NESTED_MIXED_SEMANTICS("aggregate_nested_mixed_semantics"); + UNDECLARED_REFERENCE("undeclared_reference"), + AGGREGATE_ERRORS("aggregate_errors"), + AGGREGATE_LIST_ERRORS("aggregate_list_errors"), + AGGREGATE_NESTED_MIXED_SEMANTICS("aggregate_nested_mixed_semantics"); private final String name; private final String policyFilePath; diff --git a/publish/BUILD.bazel b/publish/BUILD.bazel index 69766290e..7fd15a769 100644 --- a/publish/BUILD.bazel +++ b/publish/BUILD.bazel @@ -335,7 +335,7 @@ pom_file( "CEL_VERSION": CEL_VERSION, "CEL_ARTIFACT_ID": "verifier", "PACKAGE_NAME": "CEL Java Verifier", - "PACKAGE_DESC": "Formal verification tools for Common Expression Language for Java.", + "PACKAGE_DESC": "Formal verification library for Common Expression Language for Java.", }, targets = VERIFIER_TARGETS, template_file = "pom_template.xml", @@ -362,8 +362,28 @@ pom_file( template_file = "pom_template.xml", ) +genrule( + name = "empty_sources_jar", + outs = ["empty_sources.jar"], + cmd = "$(location @bazel_tools//tools/zip:zipper) c $@ META-INF/MANIFEST.MF=/dev/null", + tools = ["@bazel_tools//tools/zip:zipper"], +) + +genrule( + name = "empty_javadoc_jar", + outs = ["empty_javadoc.jar"], + cmd = "$(location @bazel_tools//tools/zip:zipper) c $@ META-INF/MANIFEST.MF=/dev/null", + tools = ["@bazel_tools//tools/zip:zipper"], +) + maven_export( name = "cel_verifier_cli", + classifier_artifacts = { + # Maven Central deployment requires source and javadoc JARs. + # Since this doesn't apply to CLI (uber-jar), we generate an empty one. + "sources": ":empty_sources_jar", + "javadoc": ":empty_javadoc_jar", + }, maven_coordinates = "dev.cel:verifier-cli:%s" % CEL_VERSION, pom_template = ":cel_verifier_cli_pom", target = "//verifier/src/main/java/dev/cel/verifier/tools:cel_verifier_tool_deploy.jar", diff --git a/publish/cel_version.bzl b/publish/cel_version.bzl index 4ceb4bfa4..a98746092 100644 --- a/publish/cel_version.bzl +++ b/publish/cel_version.bzl @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. """Maven artifact version for CEL.""" -CEL_VERSION = "0.13.1" +CEL_VERSION = "0.14.0" diff --git a/repositories.bzl b/repositories.bzl index cbb7b3832..9fa3d3edb 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -24,8 +24,8 @@ def antlr4_jar_dependency(): ) def bazel_common_dependency(): - bazel_common_tag = "aaa4d801588f7744c6f4428e4f133f26b8518f42" - bazel_common_sha = "1f85abb0043f3589b9bf13a80319dc48a5f01a052c68bab3c08015a56d92ab7f" + bazel_common_tag = "768dbe0b3247e2e5def0b9ac6c4cde95e214f18a" + bazel_common_sha = "b3f1fe7e26ade37712b00b82a0ab3760bb340e9307d57166872dacc679b78da1" http_archive( name = "bazel_common", sha256 = bazel_common_sha, @@ -34,8 +34,8 @@ def bazel_common_dependency(): ) def cel_policy_dependency(): - cel_policy_tag = "e4c38defbbf34dfff2dc448dc58e93a9733ae8b1" - cel_policy_sha = "46378e0d17a16465899f9fefc94c3d44e1f40aedd8a31c9c0b2b6198048eabd6" + cel_policy_tag = "01bcc1c3f7c9c5e442fa940013cd6d029af2baf7" + cel_policy_sha = "8e3ddc74e918c2a5910794387354a236da601694dbf7b6921f8a7babf7b78181" http_archive( name = "cel_policy", sha256 = cel_policy_sha, diff --git a/verifier/tools/README.md b/verifier/tools/README.md index 09b83ad70..71570ad39 100644 --- a/verifier/tools/README.md +++ b/verifier/tools/README.md @@ -13,17 +13,35 @@ directly from Maven Central and invoke it with `java -jar`: ```bash -# Download the latest CLI JAR -curl -LO https://repo1.maven.org/maven2/dev/cel/verifier-cli/0.13.1/verifier-cli-0.13.1.jar +# Download the latest CLI JAR (Note: this is an uber-JAR) +curl -LO https://repo1.maven.org/maven2/dev/cel/verifier-cli/0.14.0/verifier-cli-0.14.0.jar -# Run the verifier CLI / REPL -java -jar verifier-cli-0.13.1.jar --help +# Launch interactive REPL shell +java -jar verifier-cli-0.14.0.jar repl + +# Run a one-shot verification command +java -jar verifier-cli-0.14.0.jar check-sat \ + --expr "role == 'editor' && port > 1024" \ + --var "role:string" \ + --var "port:int" + +# Run with JSON output format for CI/CD integrations +java -jar verifier-cli-0.14.0.jar check-sat \ + --expr "role == 'editor'" \ + --var "role:string" \ + --output_format=json + +# Display help and available commands +java -jar verifier-cli-0.14.0.jar --help ``` ### Running via Bazel ```bash -# Run CLI verification commands +# Launch interactive REPL shell +bazel run //verifier/tools:cel_verifier_tool -- repl + +# Run a one-shot verification command bazel run //verifier/tools:cel_verifier_tool -- \ check-sat \ --expr "role == 'editor' && port > 1024" \ @@ -36,9 +54,6 @@ bazel run //verifier/tools:cel_verifier_tool -- \ --expr "role == 'editor'" \ --var "role:string" \ --output_format=json - -# Launch interactive REPL shell -bazel run //verifier/tools:cel_verifier_tool -- repl ``` ## CLI Commands From 426fa24ddf3c66be7615eef3f7385a8ca510daab Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Tue, 18 Aug 2026 13:13:36 -0700 Subject: [PATCH 181/204] Add agent_tool_execution_governance aggregate policy to conformance test PiperOrigin-RevId: 966765679 --- .../policy/PolicyConformanceTest.java | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java index d7851bb72..5727eb5ee 100644 --- a/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java +++ b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java @@ -32,6 +32,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Locale; +import java.util.Map; import org.junit.runners.model.Statement; /** Statement representing a single CEL policy conformance test case. */ @@ -40,21 +41,30 @@ public final class PolicyConformanceTest extends Statement { private static final Cel CEL = CelFactory.standardCelBuilder() .addFunctionBindings( - CelFunctionBinding.fromOverloads( - "locationCode", - CelFunctionBinding.from( - "locationCode_string", - String.class, - (ip) -> { - switch (ip) { - case "10.0.0.1": - return "us"; - case "10.0.0.2": - return "de"; - default: - return "ir"; - } - }))) + CelFunctionBinding.from( + "locationCode_string", + String.class, + (ip) -> { + switch (ip) { + case "10.0.0.1": + return "us"; + case "10.0.0.2": + return "de"; + default: + return "ir"; + } + }), + CelFunctionBinding.from( + "hasCreditCard", + Object.class, + (arg) -> arg instanceof Map && ((Map) arg).containsKey("cc")), + CelFunctionBinding.from( + "hasEmailOrPhone", + Object.class, + (arg) -> + arg instanceof Map + && (((Map) arg).containsKey("email") + || ((Map) arg).containsKey("phone")))) .build(); private final String name; From 9333702e07368e774532dc0f839811b3d1accf65 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Wed, 19 Aug 2026 14:21:01 -0700 Subject: [PATCH 182/204] Deprecate setStandardEnvironmentEnabled flag in favor of subsetting PiperOrigin-RevId: 967407286 --- .../main/java/dev/cel/bundle/CelBuilder.java | 13 +++- .../src/main/java/dev/cel/bundle/CelImpl.java | 1 + .../src/test/java/dev/cel/bundle/BUILD.bazel | 2 + .../test/java/dev/cel/bundle/CelImplTest.java | 25 +++++++ .../dev/cel/checker/CelCheckerBuilder.java | 12 ++- .../dev/cel/checker/CelCheckerLegacyImpl.java | 13 +--- .../cel/checker/CelStandardDeclarations.java | 4 + .../checker/CelStandardDeclarationsTest.java | 73 ++++++++++++++----- .../dev/cel/compiler/CelCompilerBuilder.java | 12 ++- .../dev/cel/compiler/CelCompilerImpl.java | 1 + .../dev/cel/runtime/CelRuntimeBuilder.java | 9 ++- .../java/dev/cel/runtime/CelRuntimeImpl.java | 1 + .../dev/cel/runtime/CelRuntimeLegacyImpl.java | 1 + .../dev/cel/runtime/CelStandardFunctions.java | 4 + .../cel/runtime/CelStandardFunctionsTest.java | 5 ++ 15 files changed, 140 insertions(+), 36 deletions(-) diff --git a/bundle/src/main/java/dev/cel/bundle/CelBuilder.java b/bundle/src/main/java/dev/cel/bundle/CelBuilder.java index a45f846e4..53eb0126b 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelBuilder.java +++ b/bundle/src/main/java/dev/cel/bundle/CelBuilder.java @@ -292,7 +292,15 @@ public interface CelBuilder { @CanIgnoreReturnValue CelBuilder addFileTypes(FileDescriptorSet fileDescriptorSet); - /** Enable or disable the standard CEL library functions and variables */ + /** + * Enable or disable the standard CEL library functions and variables. + * + * @deprecated Use {@link #setStandardDeclarations(CelStandardDeclarations)} and/or {@link + * #setStandardFunctions(CelStandardFunctions)} to configure or subset the standard + * environment. Use {@link CelStandardDeclarations#EMPTY} and {@link + * CelStandardFunctions#EMPTY} to disable all standard declarations and functions. + */ + @Deprecated @CanIgnoreReturnValue CelBuilder setStandardEnvironmentEnabled(boolean value); @@ -314,8 +322,7 @@ public interface CelBuilder { /** * Override the standard declarations for the type-checker. This can be used to subset the - * standard environment to only expose the desired declarations to the type-checker. {@link - * #setStandardEnvironmentEnabled(boolean)} must be set to false for this to take effect. + * standard environment to only expose the desired declarations to the type-checker. */ @CanIgnoreReturnValue CelBuilder setStandardDeclarations(CelStandardDeclarations standardDeclarations); diff --git a/bundle/src/main/java/dev/cel/bundle/CelImpl.java b/bundle/src/main/java/dev/cel/bundle/CelImpl.java index 999f1573a..b8c7c36e9 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelImpl.java +++ b/bundle/src/main/java/dev/cel/bundle/CelImpl.java @@ -379,6 +379,7 @@ public CelBuilder addFileTypes(FileDescriptorSet fileDescriptorSet) { } @Override + @Deprecated public CelBuilder setStandardEnvironmentEnabled(boolean value) { compilerBuilder.setStandardEnvironmentEnabled(value); runtimeBuilder.setStandardEnvironmentEnabled(value); diff --git a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel index 548b4483d..ddd2e7285 100644 --- a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel +++ b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel @@ -27,6 +27,7 @@ java_library( "//checker", "//checker:checker_legacy_environment", "//checker:proto_type_mask", + "//checker:standard_decl", "//common:cel_ast", "//common:cel_descriptor_util", "//common:cel_source", @@ -56,6 +57,7 @@ java_library( "//runtime:evaluation_exception_builder", "//runtime:evaluation_listener", "//runtime:function_binding", + "//runtime:standard_functions", "//runtime:unknown_attributes", "//testing:cel_runtime_flavor", "//testing/protos:single_file_extension_java_proto", diff --git a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java index fbacb242a..4f82411a3 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java @@ -60,6 +60,7 @@ import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.checker.CelCheckerLegacyImpl; +import dev.cel.checker.CelStandardDeclarations; import dev.cel.checker.DescriptorTypeProvider; import dev.cel.checker.ProtoTypeMask; import dev.cel.checker.TypeProvider; @@ -110,6 +111,7 @@ import dev.cel.runtime.CelRuntime.Program; import dev.cel.runtime.CelRuntimeFactory; import dev.cel.runtime.CelRuntimeLegacyImpl; +import dev.cel.runtime.CelStandardFunctions; import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.CelVariableResolver; import dev.cel.runtime.UnknownContext; @@ -2294,4 +2296,27 @@ private static Cel setupEnv(CelBuilder celBuilder) { .build()) .build(); } + + @Test + public void plannerCelBuilder_setStandardDeclarationsAndFunctions_subsetsEnvironment() + throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .setStandardDeclarations( + CelStandardDeclarations.newBuilder() + .includeFunctions(CelStandardDeclarations.StandardFunction.ADD) + .build()) + .setStandardFunctions( + CelStandardFunctions.newBuilder() + .includeFunctions(CelStandardFunctions.StandardFunction.ADD) + .build()) + .build(); + + CelAbstractSyntaxTree ast = cel.compile("1 + 1").getAst(); + assertThat(cel.createProgram(ast).eval()).isEqualTo(2L); + + CelValidationException validationException = + assertThrows(CelValidationException.class, () -> cel.compile("1 - 1").getAst()); + assertThat(validationException).hasMessageThat().contains("undeclared reference to '_-_'"); + } } diff --git a/checker/src/main/java/dev/cel/checker/CelCheckerBuilder.java b/checker/src/main/java/dev/cel/checker/CelCheckerBuilder.java index a7d531f88..b14782e27 100644 --- a/checker/src/main/java/dev/cel/checker/CelCheckerBuilder.java +++ b/checker/src/main/java/dev/cel/checker/CelCheckerBuilder.java @@ -155,14 +155,20 @@ public interface CelCheckerBuilder { @CanIgnoreReturnValue CelCheckerBuilder addFileTypes(FileDescriptorSet fileDescriptorSet); - /** Enable or disable the standard CEL library functions and variables */ + /** + * Enable or disable the standard CEL library functions and variables. + * + * @deprecated Use {@link #setStandardDeclarations(CelStandardDeclarations)} to configure or + * subset the standard environment. Use {@link CelStandardDeclarations#EMPTY} to disable all + * standard declarations. + */ + @Deprecated @CanIgnoreReturnValue CelCheckerBuilder setStandardEnvironmentEnabled(boolean value); /** * Override the standard declarations for the type-checker. This can be used to subset the - * standard environment to only expose the desired declarations to the type-checker. {@link - * #setStandardEnvironmentEnabled(boolean)} must be set to false for this to take effect. + * standard environment to only expose the desired declarations to the type-checker. */ @CanIgnoreReturnValue CelCheckerBuilder setStandardDeclarations(CelStandardDeclarations standardDeclarations); diff --git a/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java b/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java index ceab0fa93..329725e42 100644 --- a/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java +++ b/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java @@ -162,10 +162,10 @@ public void accept(EnvVisitor envVisitor) { private Env getEnv(Errors errors) { Env env; - if (standardEnvironmentEnabled) { - env = Env.standard(errors, typeProvider, celOptions); - } else if (overriddenStandardDeclarations != null) { + if (overriddenStandardDeclarations != null) { env = Env.standard(overriddenStandardDeclarations, errors, typeProvider, celOptions); + } else if (standardEnvironmentEnabled) { + env = Env.standard(errors, typeProvider, celOptions); } else { env = Env.unconfigured(errors, typeProvider, celOptions); } @@ -359,6 +359,7 @@ public CelCheckerBuilder addFileTypes(FileDescriptorSet fileDescriptorSet) { } @Override + @Deprecated public CelCheckerBuilder setStandardEnvironmentEnabled(boolean value) { this.standardEnvironmentEnabled = value; return this; @@ -434,12 +435,6 @@ CelTypeProvider celTypeProvider() { @Override @CheckReturnValue public CelCheckerLegacyImpl build() { - if (standardEnvironmentEnabled && standardDeclarations != null) { - throw new IllegalArgumentException( - "setStandardEnvironmentEnabled must be set to false to override standard" - + " declarations."); - } - // Add libraries, such as extensions ImmutableSet checkerLibraries = celCheckerLibraries.build(); checkerLibraries.forEach(celLibrary -> celLibrary.setCheckerOptions(this)); diff --git a/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java b/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java index 3d5175cb5..bd63c4279 100644 --- a/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java +++ b/checker/src/main/java/dev/cel/checker/CelStandardDeclarations.java @@ -51,6 +51,10 @@ public final class CelStandardDeclarations { private static final TypeParamType TYPE_PARAM_B = TypeParamType.create("B"); private static final MapType MAP_OF_AB = MapType.create(TYPE_PARAM_A, TYPE_PARAM_B); + /** An empty instance of {@link CelStandardDeclarations} with no functions or identifiers. */ + public static final CelStandardDeclarations EMPTY = + new CelStandardDeclarations(ImmutableSet.of(), ImmutableSet.of()); + private final ImmutableSet celFunctionDecls; private final ImmutableSet celIdentDecls; diff --git a/checker/src/test/java/dev/cel/checker/CelStandardDeclarationsTest.java b/checker/src/test/java/dev/cel/checker/CelStandardDeclarationsTest.java index 17a7212a1..f867728b0 100644 --- a/checker/src/test/java/dev/cel/checker/CelStandardDeclarationsTest.java +++ b/checker/src/test/java/dev/cel/checker/CelStandardDeclarationsTest.java @@ -86,24 +86,63 @@ public void standardDeclaration_moreThanOneIdentifierFilterSet_throws( } @Test - public void compiler_standardEnvironmentEnabled_throwsWhenOverridingDeclarations() { - IllegalArgumentException e = - assertThrows( - IllegalArgumentException.class, - () -> - CelCompilerFactory.standardCelCompilerBuilder() - .setStandardEnvironmentEnabled(true) - .setStandardDeclarations( - CelStandardDeclarations.newBuilder() - .includeFunctions(StandardFunction.ADD, StandardFunction.SUBTRACT) - .build()) - .build()); + public void compiler_setStandardDeclarations_overridesDefaultStandardEnvironment() + throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardDeclarations( + CelStandardDeclarations.newBuilder() + .includeFunctions(StandardFunction.ADD) + .build()) + .build(); - assertThat(e) - .hasMessageThat() - .contains( - "setStandardEnvironmentEnabled must be set to false to override standard" - + " declarations."); + assertThat(compiler.compile("1 + 1").hasError()).isFalse(); + assertThat(compiler.compile("1 - 1").hasError()).isTrue(); + } + + @Test + public void compiler_setStandardDeclarations_withStandardEnvironmentExplicitlyEnabled() + throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardEnvironmentEnabled(true) + .setStandardDeclarations( + CelStandardDeclarations.newBuilder() + .includeFunctions(StandardFunction.ADD) + .build()) + .build(); + + assertThat(compiler.compile("1 + 1").hasError()).isFalse(); + assertThat(compiler.compile("1 - 1").hasError()).isTrue(); + } + + @Test + public void compiler_setStandardDeclarations_withStandardEnvironmentExplicitlyDisabled() + throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardEnvironmentEnabled(false) + .setStandardDeclarations( + CelStandardDeclarations.newBuilder() + .includeFunctions(StandardFunction.ADD) + .build()) + .build(); + + assertThat(compiler.compile("1 + 1").hasError()).isFalse(); + assertThat(compiler.compile("1 - 1").hasError()).isTrue(); + } + + @Test + public void compiler_setStandardDeclarations_emptyDisablesAllStandardDeclarations() + throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardDeclarations(CelStandardDeclarations.EMPTY) + .build(); + + assertThat(compiler.compile("1 + 1").hasError()).isTrue(); + assertThat(compiler.compile("1 - 1").hasError()).isTrue(); + assertThat(compiler.compile("size([1])").hasError()).isTrue(); } @Test diff --git a/compiler/src/main/java/dev/cel/compiler/CelCompilerBuilder.java b/compiler/src/main/java/dev/cel/compiler/CelCompilerBuilder.java index 6dd2ee12e..08231657a 100644 --- a/compiler/src/main/java/dev/cel/compiler/CelCompilerBuilder.java +++ b/compiler/src/main/java/dev/cel/compiler/CelCompilerBuilder.java @@ -200,14 +200,20 @@ public interface CelCompilerBuilder { @CanIgnoreReturnValue CelCompilerBuilder addFileTypes(FileDescriptorSet fileDescriptorSet); - /** Enable or disable the standard CEL library functions and variables */ + /** + * Enable or disable the standard CEL library functions and variables. + * + * @deprecated Use {@link #setStandardDeclarations(CelStandardDeclarations)} to configure or + * subset the standard environment. Use {@link CelStandardDeclarations#EMPTY} to disable all + * standard declarations. + */ + @Deprecated @CanIgnoreReturnValue CelCompilerBuilder setStandardEnvironmentEnabled(boolean value); /** * Override the standard declarations for the type-checker. This can be used to subset the - * standard environment to only expose the desired declarations to the type-checker. {@link - * #setStandardEnvironmentEnabled(boolean)} must be set to false for this to take effect. + * standard environment to only expose the desired declarations to the type-checker. */ @CanIgnoreReturnValue CelCompilerBuilder setStandardDeclarations(CelStandardDeclarations standardDeclarations); diff --git a/compiler/src/main/java/dev/cel/compiler/CelCompilerImpl.java b/compiler/src/main/java/dev/cel/compiler/CelCompilerImpl.java index e8804f348..eb3e1549b 100644 --- a/compiler/src/main/java/dev/cel/compiler/CelCompilerImpl.java +++ b/compiler/src/main/java/dev/cel/compiler/CelCompilerImpl.java @@ -283,6 +283,7 @@ public CelCompilerBuilder addFileTypes(FileDescriptorSet fileDescriptorSet) { } @Override + @Deprecated public CelCompilerBuilder setStandardEnvironmentEnabled(boolean value) { checkerBuilder.setStandardEnvironmentEnabled(value); return this; diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java index e284b374c..00f6e3bf7 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java @@ -170,7 +170,14 @@ public interface CelRuntimeBuilder { /** Returns the configured {@link CelValueProvider}, or null if not set. */ CelValueProvider valueProvider(); - /** Enable or disable the standard CEL library functions and variables. */ + /** + * Enable or disable the standard CEL library functions and variables. + * + * @deprecated Use {@link #setStandardFunctions(CelStandardFunctions)} to configure or subset the + * standard environment. Use {@link CelStandardFunctions#EMPTY} to disable all standard + * functions. + */ + @Deprecated @CanIgnoreReturnValue CelRuntimeBuilder setStandardEnvironmentEnabled(boolean value); diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index f934108e0..5cda25800 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -393,6 +393,7 @@ public Builder setTypeFactory(Function typeFactory) { } @Override + @Deprecated public Builder setStandardEnvironmentEnabled(boolean value) { throw new UnsupportedOperationException( "Unsupported. Subset the environment using setStandardFunctions instead."); diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java index cad7e74f8..428c6dba5 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java @@ -219,6 +219,7 @@ public CelRuntimeBuilder setTypeFactory(Function typeFa } @Override + @Deprecated public CelRuntimeBuilder setStandardEnvironmentEnabled(boolean value) { standardEnvironmentEnabled = value; return this; diff --git a/runtime/src/main/java/dev/cel/runtime/CelStandardFunctions.java b/runtime/src/main/java/dev/cel/runtime/CelStandardFunctions.java index 39797e086..c9fd4a50e 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelStandardFunctions.java +++ b/runtime/src/main/java/dev/cel/runtime/CelStandardFunctions.java @@ -140,6 +140,10 @@ public final class CelStandardFunctions { GreaterEqualsOverload.GREATER_EQUALS_UINT64_DOUBLE, GreaterEqualsOverload.GREATER_EQUALS_DOUBLE_UINT64); + /** An empty instance of {@link CelStandardFunctions} with no functions. */ + public static final CelStandardFunctions EMPTY = + new CelStandardFunctions(ImmutableMultimap.of()); + private final ImmutableMultimap standardOverloads; public static final ImmutableSet ALL_STANDARD_FUNCTIONS = diff --git a/runtime/src/test/java/dev/cel/runtime/CelStandardFunctionsTest.java b/runtime/src/test/java/dev/cel/runtime/CelStandardFunctionsTest.java index c5f5572a7..f452b99dc 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelStandardFunctionsTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelStandardFunctionsTest.java @@ -136,6 +136,11 @@ public void standardFunctions_filterFunctions() { .containsExactly(AddOverload.ADD_INT64, SubtractOverload.SUBTRACT_INT64); } + @Test + public void standardFunctions_empty() { + assertThat(CelStandardFunctions.EMPTY.getOverloads()).isEmpty(); + } + @Test public void standardEnvironment_subsetEnvironment() throws Exception { CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); From 7f4a4e512c6ab54754c3f85fe3a978226162da2d Mon Sep 17 00:00:00 2001 From: Stephen Roberts Date: Thu, 27 Aug 2026 05:49:31 -0700 Subject: [PATCH 183/204] Add utility to find comprehension variables PiperOrigin-RevId: 971903461 --- .../navigation/CelNavigableExprUtil.java | 46 ++++++ .../navigation/CelNavigableExprUtilTest.java | 139 ++++++++++++++++++ 2 files changed, 185 insertions(+) diff --git a/common/src/main/java/dev/cel/common/navigation/CelNavigableExprUtil.java b/common/src/main/java/dev/cel/common/navigation/CelNavigableExprUtil.java index 788158b7c..c5a19ff9e 100644 --- a/common/src/main/java/dev/cel/common/navigation/CelNavigableExprUtil.java +++ b/common/src/main/java/dev/cel/common/navigation/CelNavigableExprUtil.java @@ -16,6 +16,7 @@ import static com.google.common.base.Preconditions.checkNotNull; +import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CheckReturnValue; import dev.cel.common.ast.CelExpr.ExprKind.Kind; import dev.cel.common.ast.Expression; @@ -79,6 +80,51 @@ Optional findDeclaringComprehension(T expr, String variableName) { return Optional.empty(); } + /** + * Returns a set of all variables declared by enclosing comprehensions that are in scope for + * {@code expr}. + * + *

A comprehension variable ({@code iterVar}, {@code iterVar2}, or {@code accuVar}) is in scope + * if {@code expr} resides within the branch where that variable is active: + * + *

    + *
  • In {@code loopCondition} and {@code loopStep}: {@code iterVar}, {@code iterVar2}, and + * {@code accuVar} are in scope. + *
  • In {@code result}: only {@code accuVar} is in scope. + *
  • In {@code iterRange} and {@code accuInit}: none of the comprehension variables are in + * scope. + *
+ */ + @SuppressWarnings("ReferenceEquality") // Disambiguates mutable child branches + public static ImmutableSet getEnclosingComprehensionVariables(BaseNavigableExpr expr) { + checkNotNull(expr); + ImmutableSet.Builder variables = ImmutableSet.builder(); + BaseNavigableExpr curr = expr; + Optional> maybeParent = curr.parent(); + while (maybeParent.isPresent()) { + BaseNavigableExpr parent = maybeParent.get(); + if (parent.getKind() == Kind.COMPREHENSION) { + Expression.Comprehension comp = parent.expr().comprehension(); + Expression currExpr = curr.expr(); + + if (currExpr != comp.iterRange() && currExpr != comp.accuInit()) { + if (currExpr == comp.result()) { + variables.add(comp.accuVar()); + } else { + variables.add(comp.iterVar()); + if (!comp.iterVar2().isEmpty()) { + variables.add(comp.iterVar2()); + } + variables.add(comp.accuVar()); + } + } + } + curr = parent; + maybeParent = parent.parent(); + } + return variables.build(); + } + /** * Returns true if {@code variableName} is in scope and shadowed by an enclosing comprehension * above {@code expr}. diff --git a/common/src/test/java/dev/cel/common/navigation/CelNavigableExprUtilTest.java b/common/src/test/java/dev/cel/common/navigation/CelNavigableExprUtilTest.java index 88eda90f2..9391881fa 100644 --- a/common/src/test/java/dev/cel/common/navigation/CelNavigableExprUtilTest.java +++ b/common/src/test/java/dev/cel/common/navigation/CelNavigableExprUtilTest.java @@ -475,4 +475,143 @@ public void isVariableShadowed_zeroedOutIds_scopedCorrectly() { assertThat(CelNavigableExprUtil.findDeclaringComprehension(identV, "v")).hasValue(innerComp); assertThat(CelNavigableExprUtil.findDeclaringComprehension(identX, "unknown")).isEmpty(); } + + @Test + public void getEnclosingComprehensionVariables_singleVar_loopStep() throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile("[1, 2].all(x, x > 0)").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + CelNavigableExpr identX = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.expr().identOrDefault().name().equals("x")) + .findFirst() + .get(); + + CelNavigableExpr comp = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.getKind() == Kind.COMPREHENSION) + .findFirst() + .get(); + String accuVar = comp.expr().comprehension().accuVar(); + + assertThat(CelNavigableExprUtil.getEnclosingComprehensionVariables(identX)) + .containsExactly("x", accuVar); + } + + @Test + public void getEnclosingComprehensionVariables_twoVar_loopStep() throws Exception { + CelAbstractSyntaxTree ast = + COMPILER.compile("{'k1': 1, 'k2': 2}.all(k, v, k != '' && v > 0)").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + CelNavigableExpr identK = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.expr().identOrDefault().name().equals("k")) + .findFirst() + .get(); + + CelNavigableExpr comp = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.getKind() == Kind.COMPREHENSION) + .findFirst() + .get(); + String accuVar = comp.expr().comprehension().accuVar(); + + assertThat(CelNavigableExprUtil.getEnclosingComprehensionVariables(identK)) + .containsExactly("k", "v", accuVar); + } + + @Test + public void getEnclosingComprehensionVariables_iterRange_empty() throws Exception { + CelAbstractSyntaxTree ast = COMPILER.compile("[a].all(x, x > 0)").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + CelNavigableExpr identA = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.expr().identOrDefault().name().equals("a")) + .findFirst() + .get(); + + assertThat(CelNavigableExprUtil.getEnclosingComprehensionVariables(identA)).isEmpty(); + } + + @Test + public void getEnclosingComprehensionVariables_nestedComprehension() throws Exception { + CelAbstractSyntaxTree ast = + COMPILER.compile("[1, 2].all(x, [3, 4].all(y, x > 0 && y > 0))").getAst(); + CelNavigableAst navigableAst = CelNavigableAst.fromAst(ast); + + CelNavigableExpr innerIdentY = + navigableAst + .getRoot() + .allNodes() + .filter(node -> node.expr().identOrDefault().name().equals("y")) + .findFirst() + .get(); + + CelNavigableExpr innerComp = + navigableAst + .getRoot() + .allNodes() + .filter( + node -> + node.getKind() == Kind.COMPREHENSION + && node.expr().comprehension().iterVar().equals("y")) + .findFirst() + .get(); + + String innerAccuVar = innerComp.expr().comprehension().accuVar(); + + assertThat(CelNavigableExprUtil.getEnclosingComprehensionVariables(innerIdentY)) + .containsExactly("y", innerAccuVar, "x"); + } + + @Test + public void getEnclosingComprehensionVariables_zeroedOutIds_scopedCorrectly() { + // Construct a mutable comprehension where ALL expression IDs are 0 (e.g. freshly minted AST) + CelMutableExpr iterRange = CelMutableExpr.ofList(0, CelMutableList.create()); + CelMutableExpr accuInit = CelMutableExpr.ofConstant(0, CelConstant.ofValue(true)); + CelMutableExpr loopCond = CelMutableExpr.ofConstant(0, CelConstant.ofValue(true)); + CelMutableExpr identX = CelMutableExpr.ofIdent(0, "x"); + CelMutableExpr loopStep = CelMutableExpr.ofCall(0, CelMutableCall.create("!_", identX)); + CelMutableExpr result = CelMutableExpr.ofIdent(0, "accu"); + + CelMutableExpr comp = + CelMutableExpr.ofComprehension( + 0, + CelMutableComprehension.create( + "x", iterRange, "accu", accuInit, loopCond, loopStep, result)); + + CelNavigableMutableExpr root = CelNavigableMutableExpr.fromExpr(comp); + + CelNavigableMutableExpr navIdentX = + root.allNodes() + .filter(node -> node.getKind() == Kind.IDENT && node.expr().ident().name().equals("x")) + .findFirst() + .get(); + CelNavigableMutableExpr navIterRange = + root.allNodes().filter(node -> node.getKind() == Kind.LIST).findFirst().get(); + CelNavigableMutableExpr navResult = + root.allNodes() + .filter( + node -> node.getKind() == Kind.IDENT && node.expr().ident().name().equals("accu")) + .findFirst() + .get(); + + assertThat(CelNavigableExprUtil.getEnclosingComprehensionVariables(navIdentX)) + .containsExactly("x", "accu"); + assertThat(CelNavigableExprUtil.getEnclosingComprehensionVariables(navIterRange)).isEmpty(); + assertThat(CelNavigableExprUtil.getEnclosingComprehensionVariables(navResult)) + .containsExactly("accu"); + } } From a2353b3e86063db4e1dd586efa990364c5584894 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Mon, 31 Aug 2026 09:45:26 -0700 Subject: [PATCH 184/204] Add EvalIndex to planner PiperOrigin-RevId: 973931384 --- .../java/dev/cel/runtime/CelAttribute.java | 2 + .../java/dev/cel/runtime/planner/BUILD.bazel | 36 ++++++- .../dev/cel/runtime/planner/EvalIndex.java | 81 ++++++++++++++++ .../cel/runtime/planner/ProgramPlanner.java | 11 ++- .../dev/cel/runtime/CelAttributeTest.java | 17 ++++ .../runtime/planner/ProgramPlannerTest.java | 93 +++++++++++++++++++ 6 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/EvalIndex.java diff --git a/runtime/src/main/java/dev/cel/runtime/CelAttribute.java b/runtime/src/main/java/dev/cel/runtime/CelAttribute.java index f04418e0c..8db377abc 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/CelAttribute.java @@ -104,6 +104,8 @@ public static Qualifier fromGeneric(Object value) { return ofUint((UnsignedLong) value); } else if (value instanceof Long) { return ofInt((Long) value); + } else if (value instanceof Integer) { + return ofInt(((Integer) value).longValue()); } else if (value instanceof Boolean) { return ofBool((boolean) value); } else if (value instanceof String) { diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index e05fca9b4..ca7665953 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -29,6 +29,7 @@ java_library( ":eval_exhaustive_conditional", ":eval_exhaustive_or", ":eval_fold", + ":eval_index", ":eval_late_bound_call", ":eval_optional_or", ":eval_optional_or_value", @@ -230,6 +231,22 @@ java_library( ], ) +java_library( + name = "eval_index", + srcs = ["EvalIndex.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "//runtime:resolved_overload", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + java_library( name = "eval_block", srcs = ["EvalBlock.java"], @@ -544,6 +561,7 @@ cel_android_library( ":eval_exhaustive_conditional_android", ":eval_exhaustive_or_android", ":eval_fold_android", + ":eval_index_android", ":eval_late_bound_call_android", ":eval_optional_or_android", ":eval_optional_or_value_android", @@ -735,10 +753,26 @@ cel_android_library( ":planned_interpretable_android", "//common/ast:ast_android", "//common/values:values_android", + "//runtime:accumulated_unknowns_android", + "//runtime:evaluation_exception", + "//runtime:interpretable_android", + "//runtime:resolved_overload_android", + ], +) + +cel_android_library( + name = "eval_index_android", + srcs = ["EvalIndex.java"], + deps = [ + ":eval_helpers_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/values:values_android", + "//runtime:accumulated_unknowns_android", "//runtime:evaluation_exception", "//runtime:interpretable_android", "//runtime:resolved_overload_android", - "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_android", + "@maven//:com_google_errorprone_error_prone_annotations", ], ) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalIndex.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalIndex.java new file mode 100644 index 000000000..027fd8339 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalIndex.java @@ -0,0 +1,81 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; +import static dev.cel.runtime.planner.EvalHelpers.evalStrictly; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.values.CelValueConverter; +import dev.cel.runtime.AccumulatedUnknowns; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelResolvedOverload; +import dev.cel.runtime.GlobalResolver; + +@Immutable +final class EvalIndex extends PlannedInterpretable { + + private final String functionName; + private final CelResolvedOverload resolvedOverload; + private final PlannedInterpretable target; + private final PlannedInterpretable index; + private final CelValueConverter celValueConverter; + + @Override + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + boolean isStrict = resolvedOverload.isStrict(); + Object targetVal = + isStrict ? evalStrictly(target, resolver, frame) : evalNonstrictly(target, resolver, frame); + Object indexVal = + isStrict ? evalStrictly(index, resolver, frame) : evalNonstrictly(index, resolver, frame); + + if (isStrict) { + AccumulatedUnknowns unknowns = AccumulatedUnknowns.maybeMerge(null, targetVal); + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, indexVal); + if (unknowns != null) { + return unknowns; + } + } + + return EvalHelpers.dispatch( + functionName, resolvedOverload, celValueConverter, targetVal, indexVal); + } + + static EvalIndex create( + CelExpr expr, + String functionName, + CelResolvedOverload resolvedOverload, + PlannedInterpretable target, + PlannedInterpretable index, + CelValueConverter celValueConverter) { + return new EvalIndex(expr, functionName, resolvedOverload, target, index, celValueConverter); + } + + private EvalIndex( + CelExpr expr, + String functionName, + CelResolvedOverload resolvedOverload, + PlannedInterpretable target, + PlannedInterpretable index, + CelValueConverter celValueConverter) { + super(expr); + this.functionName = functionName; + this.resolvedOverload = resolvedOverload; + this.target = target; + this.index = index; + this.celValueConverter = celValueConverter; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index 77f605efc..23a6e5dec 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -327,6 +327,15 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { return EvalUnary.create( expr, functionName, resolvedOverload, evaluatedArgs[0], celValueConverter); case 2: + if (functionName.equals(Operator.INDEX.getFunction())) { + return EvalIndex.create( + expr, + functionName, + resolvedOverload, + evaluatedArgs[0], + evaluatedArgs[1], + celValueConverter); + } return EvalBinary.create( expr, functionName, @@ -385,7 +394,7 @@ private Optional maybeInterceptOptionalCalls( break; } - if (Operator.OPTIONAL_SELECT.getFunction().equals(functionName)) { + if (functionName.equals(Operator.OPTIONAL_SELECT.getFunction())) { String field = expr.call().args().get(1).constant().stringValue(); InterpretableAttribute attribute; if (evaluatedArgs[0] instanceof EvalAttribute) { diff --git a/runtime/src/test/java/dev/cel/runtime/CelAttributeTest.java b/runtime/src/test/java/dev/cel/runtime/CelAttributeTest.java index fc6cb3442..dce3e376c 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelAttributeTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelAttributeTest.java @@ -143,16 +143,33 @@ public void fromQualifiedIdentifier_parseIdents() { @Test public void fromGeneric_supportedTypes() { + assertThat(Qualifier.fromGeneric(1)).isEqualTo(Qualifier.ofInt(1)); assertThat(Qualifier.fromGeneric(Long.valueOf(1))).isEqualTo(Qualifier.ofInt(1)); assertThat(Qualifier.fromGeneric(UnsignedLong.valueOf(1))).isEqualTo(Qualifier.ofUint(1)); assertThat(Qualifier.fromGeneric("abcd")).isEqualTo(Qualifier.ofString("abcd")); assertThat(Qualifier.fromGeneric(Boolean.valueOf(false))).isEqualTo(Qualifier.ofBool(false)); } + @Test + public void fromGeneric_integerBoundaryValues() { + assertThat(Qualifier.fromGeneric(Integer.MAX_VALUE)) + .isEqualTo(Qualifier.ofInt(Integer.MAX_VALUE)); + assertThat(Qualifier.fromGeneric(Integer.MIN_VALUE)) + .isEqualTo(Qualifier.ofInt(Integer.MIN_VALUE)); + assertThat(Qualifier.fromGeneric(0)).isEqualTo(Qualifier.ofInt(0)); + } + + @Test + public void fromGeneric_nullThrows() { + assertThrows(IllegalArgumentException.class, () -> Qualifier.fromGeneric(null)); + } + @Test public void fromGeneric_unsupportedTypeThrows() { assertThrows( IllegalArgumentException.class, () -> Qualifier.fromGeneric(new ArrayList())); + assertThrows(IllegalArgumentException.class, () -> Qualifier.fromGeneric(1.0)); + assertThrows(IllegalArgumentException.class, () -> Qualifier.fromGeneric(new byte[] {1, 2})); } @Test diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index 34e7831a6..a3b1e3596 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -573,6 +573,99 @@ public void plan_call_mapIndex() throws Exception { assertThat(result).isEqualTo(2L); } + @Test + public void plan_call_listIndex() throws Exception { + CelAbstractSyntaxTree ast = compile("[10, 20, 30][1]"); + Program program = PLANNER.plan(ast); + + Long result = (Long) program.eval(); + + assertThat(result).isEqualTo(20L); + } + + @Test + public void plan_call_listIndex_outOfBounds_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("[10, 20, 30][5]"); + Program program = PLANNER.plan(ast); + + assertThrows(CelEvaluationException.class, program::eval); + } + + @Test + public void plan_call_listIndex_negative_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("[10, 20, 30][-1]"); + Program program = PLANNER.plan(ast); + + assertThrows(CelEvaluationException.class, program::eval); + } + + @Test + public void plan_call_mapIndex_missingKey_throws() throws Exception { + CelAbstractSyntaxTree ast = compile("map_var['missing']"); + Program program = PLANNER.plan(ast); + + assertThrows( + CelEvaluationException.class, + () -> program.eval(ImmutableMap.of("map_var", ImmutableMap.of("key", 1L)))); + } + + @Test + public void plan_call_index_withUnknownTarget() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("unk_list", ListType.create(SimpleType.INT)) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "unk_list[0]"); + Program program = PLANNER.plan(ast); + + CelUnknownSet result = + (CelUnknownSet) program.eval(PartialVars.of(CelAttributePattern.create("unk_list"))); + + assertThat(result) + .isEqualTo( + CelUnknownSet.create( + ImmutableSet.of(CelAttribute.create("unk_list")), ImmutableSet.of(1L))); + } + + @Test + public void plan_call_index_withUnknownIndex() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder().addVar("unk_index", SimpleType.INT).build(); + CelAbstractSyntaxTree ast = compile(compiler, "[10, 20, 30][unk_index]"); + Program program = PLANNER.plan(ast); + + CelUnknownSet result = + (CelUnknownSet) program.eval(PartialVars.of(CelAttributePattern.create("unk_index"))); + + assertThat(result) + .isEqualTo( + CelUnknownSet.create( + ImmutableSet.of(CelAttribute.create("unk_index")), ImmutableSet.of(6L))); + } + + @Test + public void plan_call_index_withMultipleUnknowns_mergesUnknowns() throws Exception { + CelCompiler compiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("unk_map", MapType.create(SimpleType.STRING, SimpleType.INT)) + .addVar("unk_key", SimpleType.STRING) + .build(); + CelAbstractSyntaxTree ast = compile(compiler, "unk_map[unk_key]"); + Program program = PLANNER.plan(ast); + + CelUnknownSet result = + (CelUnknownSet) + program.eval( + PartialVars.of( + CelAttributePattern.create("unk_map"), CelAttributePattern.create("unk_key"))); + + assertThat(result) + .isEqualTo( + CelUnknownSet.create( + ImmutableSet.of(CelAttribute.create("unk_map"), CelAttribute.create("unk_key")), + ImmutableSet.of(1L, 3L))); + } + @Test public void plan_call_noMatchingOverload_throws() throws Exception { CelAbstractSyntaxTree ast = compile("concat(b'abc', dyn_var)"); From 9a97aec7b601a767af288208e8fb2eb27834bc76 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Thu, 3 Sep 2026 15:56:30 -0700 Subject: [PATCH 185/204] Add async function bindings, observer, and drain strategies PiperOrigin-RevId: 975968798 --- .bazelrc | 2 +- publish/BUILD.bazel | 3 + runtime/BUILD.bazel | 38 ++ .../src/main/java/dev/cel/runtime/BUILD.bazel | 86 +++++ .../java/dev/cel/runtime/CelAsyncCall.java | 34 ++ .../dev/cel/runtime/CelAsyncDrainAction.java | 57 +++ .../cel/runtime/CelAsyncDrainStrategy.java | 101 +++++ .../cel/runtime/CelAsyncFunctionOverload.java | 57 +++ .../dev/cel/runtime/CelAsyncObserver.java | 46 +++ .../dev/cel/runtime/CelFunctionBinding.java | 81 ++++ .../dev/cel/runtime/CelFunctionResolver.java | 18 +- .../src/test/java/dev/cel/runtime/BUILD.bazel | 3 +- .../runtime/CelAsyncDrainStrategyTest.java | 299 ++++++++++++++ .../cel/runtime/FunctionBindingImplTest.java | 365 ++++++++++++++++++ 14 files changed, 1187 insertions(+), 3 deletions(-) create mode 100644 runtime/src/main/java/dev/cel/runtime/CelAsyncCall.java create mode 100644 runtime/src/main/java/dev/cel/runtime/CelAsyncDrainAction.java create mode 100644 runtime/src/main/java/dev/cel/runtime/CelAsyncDrainStrategy.java create mode 100644 runtime/src/main/java/dev/cel/runtime/CelAsyncFunctionOverload.java create mode 100644 runtime/src/main/java/dev/cel/runtime/CelAsyncObserver.java create mode 100644 runtime/src/test/java/dev/cel/runtime/CelAsyncDrainStrategyTest.java create mode 100644 runtime/src/test/java/dev/cel/runtime/FunctionBindingImplTest.java diff --git a/.bazelrc b/.bazelrc index f6e2f39c0..34a59ec39 100644 --- a/.bazelrc +++ b/.bazelrc @@ -16,7 +16,7 @@ build --java_language_version=11 common --javacopt=-Xlint:-options # Remove flag once https://github.com/google/cel-spec/issues/508 and rules_jvm_external is fixed. -common --incompatible_autoload_externally=proto_library,cc_proto_library,java_proto_library,java_test +common --incompatible_autoload_externally=proto_library,cc_proto_library,java_proto_library,java_test,java_import # Limit repository cache size by not caching extracted repository contents build --repo_contents_cache= diff --git a/publish/BUILD.bazel b/publish/BUILD.bazel index 7fd15a769..17089eb82 100644 --- a/publish/BUILD.bazel +++ b/publish/BUILD.bazel @@ -29,6 +29,9 @@ COMMON_TARGETS = [ # keep sorted RUNTIME_TARGETS = [ "//runtime/src/main/java/dev/cel/runtime", + "//runtime/src/main/java/dev/cel/runtime:async_call", + "//runtime/src/main/java/dev/cel/runtime:async_drain_strategy", + "//runtime/src/main/java/dev/cel/runtime:async_observer", "//runtime/src/main/java/dev/cel/runtime:base", "//runtime/src/main/java/dev/cel/runtime:interpreter", "//runtime/src/main/java/dev/cel/runtime:late_function_binding", diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index c87fadca9..fbdbb1107 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -9,6 +9,9 @@ package( java_library( name = "runtime", exports = [ + ":async_call", + ":async_drain_strategy", + ":async_observer", ":descriptor_message_provider", ":evaluation_exception", ":function_overload", @@ -340,6 +343,11 @@ java_library( exports = ["//runtime/src/main/java/dev/cel/runtime:function_overload"], ) +cel_android_library( + name = "function_overload_android", + exports = ["//runtime/src/main/java/dev/cel/runtime:function_overload_android"], +) + java_library( name = "descriptor_message_provider", visibility = ["//:internal"], @@ -379,3 +387,33 @@ cel_android_library( name = "partial_vars_android", exports = ["//runtime/src/main/java/dev/cel/runtime:partial_vars_android"], ) + +java_library( + name = "async_call", + exports = ["//runtime/src/main/java/dev/cel/runtime:async_call"], +) + +cel_android_library( + name = "async_call_android", + exports = ["//runtime/src/main/java/dev/cel/runtime:async_call_android"], +) + +java_library( + name = "async_drain_strategy", + exports = ["//runtime/src/main/java/dev/cel/runtime:async_drain_strategy"], +) + +cel_android_library( + name = "async_drain_strategy_android", + exports = ["//runtime/src/main/java/dev/cel/runtime:async_drain_strategy_android"], +) + +java_library( + name = "async_observer", + exports = ["//runtime/src/main/java/dev/cel/runtime:async_observer"], +) + +cel_android_library( + name = "async_observer_android", + exports = ["//runtime/src/main/java/dev/cel/runtime:async_observer_android"], +) diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 489bb64d8..145341889 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -784,6 +784,7 @@ cel_android_library( java_library( name = "function_overload", srcs = [ + "CelAsyncFunctionOverload.java", "CelFunctionOverload.java", "OptimizedFunctionOverload.java", ], @@ -800,9 +801,12 @@ java_library( cel_android_library( name = "function_overload_android", srcs = [ + "CelAsyncFunctionOverload.java", "CelFunctionOverload.java", "OptimizedFunctionOverload.java", ], + tags = [ + ], deps = [ ":evaluation_exception", ":unknown_attributes_android", @@ -1277,6 +1281,88 @@ cel_android_library( ], ) +java_library( + name = "async_call", + srcs = ["CelAsyncCall.java"], + tags = [ + ], + deps = [ + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "async_call_android", + srcs = ["CelAsyncCall.java"], + tags = [ + ], + deps = [ + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "async_drain_strategy", + srcs = [ + "CelAsyncDrainAction.java", + "CelAsyncDrainStrategy.java", + ], + tags = [ + ], + deps = [ + ":async_call", + "//:auto_value", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "async_drain_strategy_android", + srcs = [ + "CelAsyncDrainAction.java", + "CelAsyncDrainStrategy.java", + ], + tags = [ + ], + deps = [ + ":async_call_android", + "//:auto_value", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "async_observer", + srcs = ["CelAsyncObserver.java"], + tags = [ + ], + deps = [ + ":async_call", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + +cel_android_library( + name = "async_observer_android", + srcs = ["CelAsyncObserver.java"], + tags = [ + ], + deps = [ + ":async_call_android", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "program", srcs = ["Program.java"], diff --git a/runtime/src/main/java/dev/cel/runtime/CelAsyncCall.java b/runtime/src/main/java/dev/cel/runtime/CelAsyncCall.java new file mode 100644 index 000000000..1582f12a1 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelAsyncCall.java @@ -0,0 +1,34 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import javax.annotation.concurrent.ThreadSafe; + +/** Describes a pending or completed asynchronous function call. */ +@ThreadSafe +public interface CelAsyncCall { + + /** Returns the unique incremental tracking ID assigned to this call. */ + long callId(); + + /** Returns the AST expression node ID where the call is located. */ + long exprId(); + + /** Returns the name of the function being invoked. */ + String functionName(); + + /** Returns the specific overload ID being invoked. */ + String overloadId(); +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelAsyncDrainAction.java b/runtime/src/main/java/dev/cel/runtime/CelAsyncDrainAction.java new file mode 100644 index 000000000..845db25a3 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelAsyncDrainAction.java @@ -0,0 +1,57 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.Immutable; +import java.time.Duration; + +/** Dictates what asynchronous evaluation should do after inspecting completions. */ +@AutoValue +@Immutable +public abstract class CelAsyncDrainAction { + + CelAsyncDrainAction() {} + + /** Indicates that the AST should be re-evaluated immediately. */ + public abstract boolean shouldReevaluate(); + + /** + * Indicates how long the evaluator should wait for additional completions before deciding to + * re-evaluate. A duration of ZERO with reevaluate=false means wait indefinitely for the next + * completion. + */ + public abstract Duration waitDuration(); + + public static CelAsyncDrainAction waitDuration(Duration duration) { + checkNotNull(duration); + checkArgument(!duration.isNegative(), "duration must not be negative"); + if (duration.isZero()) { + return reevaluate(); + } + return new AutoValue_CelAsyncDrainAction(false, duration); + } + + public static CelAsyncDrainAction reevaluate() { + return new AutoValue_CelAsyncDrainAction(true, Duration.ZERO); + } + + public static CelAsyncDrainAction waitForMore() { + return new AutoValue_CelAsyncDrainAction(false, Duration.ZERO); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelAsyncDrainStrategy.java b/runtime/src/main/java/dev/cel/runtime/CelAsyncDrainStrategy.java new file mode 100644 index 000000000..6d8349f8e --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelAsyncDrainStrategy.java @@ -0,0 +1,101 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.errorprone.annotations.Immutable; +import java.time.Duration; +import java.util.List; + +/** + * Controls when asynchronous evaluation re-evaluates the AST after async completions. + * + *

The evaluator consults the strategy each time completions are received. + */ +@Immutable +public interface CelAsyncDrainStrategy { + + /** + * Evaluates the current state of asynchronous evaluation and determines the next step. + * + * @param completedBatch The batch of async call completions accumulated so far in this drain + * cycle. + * @param activeCallsCount The number of async calls currently launched but unresolved. + */ + CelAsyncDrainAction nextAction(List completedBatch, int activeCallsCount); + + /** + * Re-evaluates after a debounce window after the first completion, batching completions that + * complete at roughly the same time. + */ + static CelAsyncDrainStrategy drainReady(Duration debounce) { + return new DrainReadyStrategy(debounce); + } + + /** Re-evaluates with the default debounce window of 100 microseconds. */ + static CelAsyncDrainStrategy drainReady() { + return drainReady(Duration.ofNanos(100_000)); + } + + /** Re-evaluates immediately as soon as any single call completes. */ + static CelAsyncDrainStrategy drainNone() { + return (completed, active) -> { + checkNotNull(completed, "completedBatch must not be null"); + checkArgument(active >= 0, "activeCallsCount must be non-negative: %s", active); + return active == 0 || !completed.isEmpty() + ? CelAsyncDrainAction.reevaluate() + : CelAsyncDrainAction.waitForMore(); + }; + } + + /** Waits for all currently pending calls to finish before re-evaluating. */ + static CelAsyncDrainStrategy drainAll() { + return (completed, active) -> { + checkNotNull(completed, "completedBatch must not be null"); + checkArgument(active >= 0, "activeCallsCount must be non-negative: %s", active); + return active == 0 ? CelAsyncDrainAction.reevaluate() : CelAsyncDrainAction.waitForMore(); + }; + } + + /** Internal implementation of the drain ready strategy with configurable debounce duration. */ + @Immutable + final class DrainReadyStrategy implements CelAsyncDrainStrategy { + private final Duration debounce; + + DrainReadyStrategy(Duration debounce) { + this.debounce = checkNotNull(debounce); + checkArgument(!debounce.isNegative(), "debounce duration must not be negative"); + } + + @Override + public CelAsyncDrainAction nextAction(List completedBatch, int activeCallsCount) { + checkNotNull(completedBatch, "completedBatch must not be null"); + checkArgument( + activeCallsCount >= 0, "activeCallsCount must be non-negative: %s", activeCallsCount); + if (activeCallsCount == 0) { + return CelAsyncDrainAction.reevaluate(); + } + if (completedBatch.isEmpty()) { + return CelAsyncDrainAction.waitForMore(); + } + if (debounce.isZero()) { + return CelAsyncDrainAction.reevaluate(); + } + return CelAsyncDrainAction.waitDuration(debounce); + } + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelAsyncFunctionOverload.java b/runtime/src/main/java/dev/cel/runtime/CelAsyncFunctionOverload.java new file mode 100644 index 000000000..685c81573 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelAsyncFunctionOverload.java @@ -0,0 +1,57 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.errorprone.annotations.Immutable; + +/** Represents a CEL custom function overload that executes asynchronously. */ +@Immutable +public interface CelAsyncFunctionOverload extends CelFunctionOverload { + + /** Invokes the overload asynchronously with evaluated arguments. */ + ListenableFuture applyAsync(Object[] args) throws CelEvaluationException; + + /** Optimized overload for single-argument async functions to avoid array allocation. */ + default ListenableFuture applyAsync(Object arg) throws CelEvaluationException { + return applyAsync(new Object[] {arg}); + } + + /** Optimized overload for two-argument async functions to avoid array allocation. */ + default ListenableFuture applyAsync(Object arg1, Object arg2) + throws CelEvaluationException { + return applyAsync(new Object[] {arg1, arg2}); + } + + @Override + default Object apply(Object[] args) throws CelEvaluationException { + throw new UnsupportedOperationException( + "Async overload cannot be evaluated synchronously. Use evalAsync instead."); + } + + /** Helper interface for describing unary async functions. */ + @Immutable + @FunctionalInterface + interface Unary { + ListenableFuture apply(T arg) throws CelEvaluationException; + } + + /** Helper interface for describing binary async functions. */ + @Immutable + @FunctionalInterface + interface Binary { + ListenableFuture apply(T1 arg1, T2 arg2) throws CelEvaluationException; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelAsyncObserver.java b/runtime/src/main/java/dev/cel/runtime/CelAsyncObserver.java new file mode 100644 index 000000000..31b001115 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelAsyncObserver.java @@ -0,0 +1,46 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import com.google.common.collect.ImmutableList; +import javax.annotation.concurrent.ThreadSafe; +import org.jspecify.annotations.Nullable; + +/** + * Provides callbacks for monitoring the lifecycle of asynchronous function calls. + * + *

Implementations must be thread-safe: {@code onCallStarted} is invoked from the thread + * dispatching the call, while {@code onCallFinished} is invoked from the call's completion thread. + */ +@ThreadSafe +public interface CelAsyncObserver { + + /** + * Invoked when an asynchronous function call is first dispatched. + * + * @param call The call description. + * @param args The evaluated arguments passed to the function call. + */ + void onCallStarted(CelAsyncCall call, ImmutableList args); + + /** + * Invoked when an asynchronous function call completes with either a result or an exception. + * + * @param call The call description. + * @param result The result of the call if successful, or null if failed. + * @param error The failure cause if the call failed, or null if successful. + */ + void onCallFinished(CelAsyncCall call, @Nullable Object result, @Nullable Throwable error); +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java index 98991d383..3b0084394 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java @@ -15,10 +15,12 @@ package dev.cel.runtime; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.Immutable; import java.util.Collection; @@ -100,6 +102,74 @@ static CelFunctionBinding from( overloadId, ImmutableList.copyOf(argTypes), impl, /* isStrict= */ true); } + /** + * Create an asynchronous unary function binding from the {@code overloadId}, {@code arg}, and + * {@code impl}. + */ + @SuppressWarnings("unchecked") // Safe from CelFunctionOverload.canHandle check before invocation + static CelFunctionBinding fromAsync( + String overloadId, Class arg, CelAsyncFunctionOverload.Unary impl) { + checkNotNull(overloadId); + checkNotNull(arg); + checkNotNull(impl); + return from( + overloadId, + ImmutableList.of(arg), + new CelAsyncFunctionOverload() { + @Override + public ListenableFuture applyAsync(Object[] args) throws CelEvaluationException { + return impl.apply((T) args[0]); + } + + @Override + public ListenableFuture applyAsync(Object arg1) throws CelEvaluationException { + return impl.apply((T) arg1); + } + }); + } + + /** + * Create an asynchronous binary function binding from the {@code overloadId}, {@code arg1}, + * {@code arg2}, and {@code impl}. + */ + @SuppressWarnings("unchecked") // Safe from CelFunctionOverload.canHandle check before invocation + static CelFunctionBinding fromAsync( + String overloadId, + Class arg1, + Class arg2, + CelAsyncFunctionOverload.Binary impl) { + checkNotNull(overloadId); + checkNotNull(arg1); + checkNotNull(arg2); + checkNotNull(impl); + return from( + overloadId, + ImmutableList.of(arg1, arg2), + new CelAsyncFunctionOverload() { + @Override + public ListenableFuture applyAsync(Object[] args) throws CelEvaluationException { + return impl.apply((T1) args[0], (T2) args[1]); + } + + @Override + public ListenableFuture applyAsync(Object a1, Object a2) + throws CelEvaluationException { + return impl.apply((T1) a1, (T2) a2); + } + }); + } + + /** + * Create an asynchronous function binding from the {@code overloadId}, {@code argTypes}, and + * {@code impl}. + */ + static CelFunctionBinding fromAsync( + String overloadId, Iterable> argTypes, CelAsyncFunctionOverload impl) { + checkNotNull(overloadId); + checkNotNull(argTypes); + checkNotNull(impl); + return from(overloadId, argTypes, impl); + } /** See {@link #fromOverloads(String, Collection)}. */ static ImmutableSet fromOverloads( @@ -110,11 +180,22 @@ static ImmutableSet fromOverloads( /** * Creates a set of bindings for a function, enabling dynamic dispatch logic to select the correct * overload at runtime based on argument types. + * + *

Note: Overloaded functions with {@link CelAsyncFunctionOverload} are not currently + * supported. */ static ImmutableSet fromOverloads( String functionName, Collection overloadBindings) { checkArgument(!Strings.isNullOrEmpty(functionName), "Function name cannot be null or empty"); checkArgument(!overloadBindings.isEmpty(), "You must provide at least one binding."); + // TODO: Dynamic dispatch grouping does not currently support asynchronous + // function overloads. In parsed-only mode, overloaded async functions must be resolved + // at runtime via CelFunctionResolver. + for (CelFunctionBinding binding : overloadBindings) { + checkArgument( + !(binding.getDefinition() instanceof CelAsyncFunctionOverload), + "Asynchronous function overloads cannot be grouped using fromOverloads."); + } return FunctionBindingImpl.groupOverloadsToFunction( functionName, ImmutableSet.copyOf(overloadBindings)); diff --git a/runtime/src/main/java/dev/cel/runtime/CelFunctionResolver.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionResolver.java index 2fb136a1a..836c48d8b 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelFunctionResolver.java +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionResolver.java @@ -25,6 +25,22 @@ @ThreadSafe public interface CelFunctionResolver { + /** An empty function resolver that resolves no overloads. */ + CelFunctionResolver EMPTY = + new CelFunctionResolver() { + @Override + public Optional findOverloadMatchingArgs( + String functionName, Collection overloadIds, Object[] args) { + return Optional.empty(); + } + + @Override + public Optional findOverloadMatchingArgs( + String functionName, Object[] args) { + return Optional.empty(); + } + }; + /** * Finds a specific function overload to invoke based on given parameters. * @@ -33,7 +49,7 @@ public interface CelFunctionResolver { * from this list with matching arguments. * @param args The arguments to pass to the function. * @return an optional value of the resolved overload. - * @throws CelEvaluationException if the overload resolution is ambiguous, + * @throws CelEvaluationException if the overload resolution is ambiguous. */ Optional findOverloadMatchingArgs( String functionName, Collection overloadIds, Object[] args) diff --git a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel index a2e44223a..f898b66fe 100644 --- a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel @@ -43,6 +43,7 @@ java_library( "//common/exceptions:bad_format", "//common/exceptions:divide_by_zero", "//common/exceptions:numeric_overflow", + "//common/exceptions:overload_not_found", "//common/exceptions:runtime_exception", "//common/internal:cel_descriptor_pools", "//common/internal:converter", @@ -69,9 +70,9 @@ java_library( "//runtime:evaluation_exception_builder", "//runtime:evaluation_listener", "//runtime:function_binding", + "//runtime:function_resolver", "//runtime:interpretable", "//runtime:interpreter", - "//runtime:interpreter_util", "//runtime:late_function_binding", "//runtime:lite_runtime", "//runtime:lite_runtime_factory", diff --git a/runtime/src/test/java/dev/cel/runtime/CelAsyncDrainStrategyTest.java b/runtime/src/test/java/dev/cel/runtime/CelAsyncDrainStrategyTest.java new file mode 100644 index 000000000..e93b8688c --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/CelAsyncDrainStrategyTest.java @@ -0,0 +1,299 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import java.time.Duration; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class CelAsyncDrainStrategyTest { + + private static final CelAsyncCall DUMMY_CALL = + new CelAsyncCall() { + @Override + public long callId() { + return 1L; + } + + @Override + public long exprId() { + return 10L; + } + + @Override + public String functionName() { + return "fn"; + } + + @Override + public String overloadId() { + return "fn_overload"; + } + }; + + @Test + public void drainReady_defaultDebounce_noActiveCalls_reevaluates() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainReady(); + + CelAsyncDrainAction action = strategy.nextAction(ImmutableList.of(), 0); + + assertThat(action.shouldReevaluate()).isTrue(); + assertThat(action.waitDuration()).isEqualTo(Duration.ZERO); + } + + @Test + public void drainReady_defaultDebounce_emptyBatchWithActiveCalls_waitsForMore() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainReady(); + + CelAsyncDrainAction action = strategy.nextAction(ImmutableList.of(), 3); + + assertThat(action.shouldReevaluate()).isFalse(); + assertThat(action.waitDuration()).isEqualTo(Duration.ZERO); + } + + @Test + public void drainReady_defaultDebounce_hasBatchWithActiveCalls_waitsDefaultDuration() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainReady(); + + CelAsyncDrainAction action = strategy.nextAction(ImmutableList.of(DUMMY_CALL), 2); + + assertThat(action.shouldReevaluate()).isFalse(); + assertThat(action.waitDuration()).isEqualTo(Duration.ofNanos(100_000)); + } + + @Test + public void drainReady_zeroDebounce_hasCompletedBatch_reevaluates() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainReady(Duration.ZERO); + + CelAsyncDrainAction actionWithActive = strategy.nextAction(ImmutableList.of(DUMMY_CALL), 5); + + assertThat(actionWithActive.shouldReevaluate()).isTrue(); + assertThat(actionWithActive.waitDuration()).isEqualTo(Duration.ZERO); + } + + @Test + public void drainReady_zeroDebounce_emptyBatchWithActiveCalls_waitsForMore() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainReady(Duration.ZERO); + + CelAsyncDrainAction action = strategy.nextAction(ImmutableList.of(), 5); + + assertThat(action.shouldReevaluate()).isFalse(); + assertThat(action.waitDuration()).isEqualTo(Duration.ZERO); + } + + @Test + public void drainReady_withDebounce_activeZero_reevaluates() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainReady(Duration.ofMillis(50)); + + CelAsyncDrainAction action = strategy.nextAction(ImmutableList.of(), 0); + + assertThat(action.shouldReevaluate()).isTrue(); + assertThat(action.waitDuration()).isEqualTo(Duration.ZERO); + } + + @Test + public void drainReady_withDebounce_emptyBatchAndActiveCalls_waitsForMore() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainReady(Duration.ofMillis(50)); + + CelAsyncDrainAction action = strategy.nextAction(ImmutableList.of(), 3); + + assertThat(action.shouldReevaluate()).isFalse(); + assertThat(action.waitDuration()).isEqualTo(Duration.ZERO); + } + + @Test + public void drainReady_withDebounce_hasCompletedBatchAndActiveCalls_waitsDuration() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainReady(Duration.ofMillis(50)); + + CelAsyncDrainAction action = strategy.nextAction(ImmutableList.of(DUMMY_CALL), 2); + + assertThat(action.shouldReevaluate()).isFalse(); + assertThat(action.waitDuration()).isEqualTo(Duration.ofMillis(50)); + } + + @Test + public void drainNone_noActiveCalls_reevaluates() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainNone(); + + CelAsyncDrainAction action = strategy.nextAction(ImmutableList.of(), 0); + + assertThat(action.shouldReevaluate()).isTrue(); + assertThat(action.waitDuration()).isEqualTo(Duration.ZERO); + } + + @Test + public void drainNone_withCompletedBatchAndActiveCalls_reevaluates() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainNone(); + + CelAsyncDrainAction action = strategy.nextAction(ImmutableList.of(DUMMY_CALL), 2); + + assertThat(action.shouldReevaluate()).isTrue(); + assertThat(action.waitDuration()).isEqualTo(Duration.ZERO); + } + + @Test + public void drainNone_emptyBatchWithActiveCalls_waitsForMore() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainNone(); + + CelAsyncDrainAction action = strategy.nextAction(ImmutableList.of(), 2); + + assertThat(action.shouldReevaluate()).isFalse(); + assertThat(action.waitDuration()).isEqualTo(Duration.ZERO); + } + + @Test + public void drainAll_noActiveCalls_reevaluates() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainAll(); + + CelAsyncDrainAction action = strategy.nextAction(ImmutableList.of(), 0); + + assertThat(action.shouldReevaluate()).isTrue(); + assertThat(action.waitDuration()).isEqualTo(Duration.ZERO); + } + + @Test + public void drainAll_withCompletedBatchAndNoActiveCalls_reevaluates() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainAll(); + + CelAsyncDrainAction action = strategy.nextAction(ImmutableList.of(DUMMY_CALL), 0); + + assertThat(action.shouldReevaluate()).isTrue(); + assertThat(action.waitDuration()).isEqualTo(Duration.ZERO); + } + + @Test + public void drainAll_withCompletedBatchAndActiveCalls_waitsForMore() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainAll(); + + CelAsyncDrainAction action = strategy.nextAction(ImmutableList.of(DUMMY_CALL), 1); + + assertThat(action.shouldReevaluate()).isFalse(); + assertThat(action.waitDuration()).isEqualTo(Duration.ZERO); + } + + @Test + public void drainAll_emptyBatchWithActiveCalls_waitsForMore() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainAll(); + + CelAsyncDrainAction action = strategy.nextAction(ImmutableList.of(), 1); + + assertThat(action.shouldReevaluate()).isFalse(); + assertThat(action.waitDuration()).isEqualTo(Duration.ZERO); + } + + @Test + public void drainReady_nullDebounce_throwsException() { + assertThrows(NullPointerException.class, () -> CelAsyncDrainStrategy.drainReady(null)); + } + + @Test + public void drainReady_nullCompletedBatch_throwsException() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainReady(); + + assertThrows(NullPointerException.class, () -> strategy.nextAction(null, 1)); + } + + @Test + public void drainReady_negativeActiveCalls_throwsException() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainReady(); + + assertThrows(IllegalArgumentException.class, () -> strategy.nextAction(ImmutableList.of(), -1)); + } + + @Test + public void drainNone_nullCompletedBatch_throwsException() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainNone(); + + assertThrows(NullPointerException.class, () -> strategy.nextAction(null, 1)); + } + + @Test + public void drainNone_negativeActiveCalls_throwsException() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainNone(); + + assertThrows(IllegalArgumentException.class, () -> strategy.nextAction(ImmutableList.of(), -1)); + } + + @Test + public void drainAll_nullCompletedBatch_throwsException() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainAll(); + + assertThrows(NullPointerException.class, () -> strategy.nextAction(null, 1)); + } + + @Test + public void drainAll_negativeActiveCalls_throwsException() { + CelAsyncDrainStrategy strategy = CelAsyncDrainStrategy.drainAll(); + + assertThrows(IllegalArgumentException.class, () -> strategy.nextAction(ImmutableList.of(), -1)); + } + + @Test + public void drainReady_negativeDebounce_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> CelAsyncDrainStrategy.drainReady(Duration.ofMillis(-1))); + } + + @Test + public void drainAction_reevaluate() { + CelAsyncDrainAction action = CelAsyncDrainAction.reevaluate(); + + assertThat(action.shouldReevaluate()).isTrue(); + assertThat(action.waitDuration()).isEqualTo(Duration.ZERO); + } + + @Test + public void drainAction_waitForMore() { + CelAsyncDrainAction action = CelAsyncDrainAction.waitForMore(); + + assertThat(action.shouldReevaluate()).isFalse(); + assertThat(action.waitDuration()).isEqualTo(Duration.ZERO); + } + + @Test + public void drainAction_waitDuration_success() { + CelAsyncDrainAction action = CelAsyncDrainAction.waitDuration(Duration.ofSeconds(2)); + + assertThat(action.shouldReevaluate()).isFalse(); + assertThat(action.waitDuration()).isEqualTo(Duration.ofSeconds(2)); + } + + @Test + public void drainAction_waitZero_reevaluates() { + CelAsyncDrainAction action = CelAsyncDrainAction.waitDuration(Duration.ZERO); + + assertThat(action.shouldReevaluate()).isTrue(); + assertThat(action.waitDuration()).isEqualTo(Duration.ZERO); + } + + @Test + public void drainAction_waitDuration_negative_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> CelAsyncDrainAction.waitDuration(Duration.ofMillis(-5))); + } + + @Test + public void drainAction_waitDuration_null_throwsException() { + assertThrows(NullPointerException.class, () -> CelAsyncDrainAction.waitDuration(null)); + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/FunctionBindingImplTest.java b/runtime/src/test/java/dev/cel/runtime/FunctionBindingImplTest.java new file mode 100644 index 000000000..0395cf560 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/FunctionBindingImplTest.java @@ -0,0 +1,365 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.util.concurrent.Futures.immediateFuture; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.exceptions.CelOverloadNotFoundException; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class FunctionBindingImplTest { + + @Test + public void dynamicDispatch_unaryOptimizedOverload_invokesOptimizedUnaryApply() throws Exception { + OptimizedFunctionOverload mockOverload = + new OptimizedFunctionOverload() { + @Override + public Object apply(Object arg) { + return (Long) arg * 10L; + } + + @Override + public Object apply(Object[] args) { + throw new AssertionError("Should not invoke array apply for unary optimized overload!"); + } + }; + CelFunctionBinding b1 = + CelFunctionBinding.from( + "custom_opt_unary_long", ImmutableList.of(Long.class), mockOverload); + CelFunctionBinding b2 = + CelFunctionBinding.from("custom_opt_unary_str", String.class, (String arg) -> arg + "!"); + ImmutableSet bindings = + FunctionBindingImpl.groupOverloadsToFunction("custom_opt_unary", ImmutableSet.of(b1, b2)); + OptimizedFunctionOverload overload = + (OptimizedFunctionOverload) + Iterables.find(bindings, b -> b.getOverloadId().equals("custom_opt_unary")) + .getDefinition(); + + assertThat(overload.apply(5L)).isEqualTo(50L); + assertThat(overload.apply("test")).isEqualTo("test!"); + } + + @Test + public void dynamicDispatch_binaryOptimizedOverload_invokesOptimizedBinaryApply() + throws Exception { + OptimizedFunctionOverload mockOverload = + new OptimizedFunctionOverload() { + @Override + public Object apply(Object arg1, Object arg2) { + return (Long) arg1 + (Long) arg2 + 100L; + } + + @Override + public Object apply(Object[] args) { + throw new AssertionError( + "Should not invoke array apply for binary optimized overload!"); + } + }; + CelFunctionBinding b1 = + CelFunctionBinding.from( + "custom_opt_binary_long", ImmutableList.of(Long.class, Long.class), mockOverload); + CelFunctionBinding b2 = + CelFunctionBinding.from( + "custom_opt_bin_str", String.class, String.class, (String a, String b) -> a + b); + ImmutableSet bindings = + FunctionBindingImpl.groupOverloadsToFunction("custom_opt_binary", ImmutableSet.of(b1, b2)); + OptimizedFunctionOverload overload = + (OptimizedFunctionOverload) + Iterables.find(bindings, b -> b.getOverloadId().equals("custom_opt_binary")) + .getDefinition(); + + assertThat(overload.apply(10L, 20L)).isEqualTo(130L); + assertThat(overload.apply("foo", "bar")).isEqualTo("foobar"); + } + + @Test + public void fromAsync_unary_invokesAsyncAndAdapts() throws Exception { + CelFunctionBinding binding = + CelFunctionBinding.fromAsync( + "unary_async", Long.class, (Long arg) -> immediateFuture(arg * 3L)); + CelAsyncFunctionOverload overload = (CelAsyncFunctionOverload) binding.getDefinition(); + + assertThat(binding.getOverloadId()).isEqualTo("unary_async"); + assertThat(binding.getArgTypes()).containsExactly(Long.class); + assertThat(binding.isStrict()).isTrue(); + assertThat(binding.getDefinition()).isInstanceOf(CelAsyncFunctionOverload.class); + assertThat(overload.applyAsync(10L).get(5, SECONDS)).isEqualTo(30L); + assertThat(overload.applyAsync(new Object[] {10L}).get(5, SECONDS)).isEqualTo(30L); + } + + @Test + public void fromAsync_binary_invokesAsyncAndAdapts() throws Exception { + CelFunctionBinding binding = + CelFunctionBinding.fromAsync( + "binary_async", Long.class, Long.class, (Long a, Long b) -> immediateFuture(a + b)); + CelAsyncFunctionOverload overload = (CelAsyncFunctionOverload) binding.getDefinition(); + + assertThat(binding.getOverloadId()).isEqualTo("binary_async"); + assertThat(binding.getArgTypes()).containsExactly(Long.class, Long.class).inOrder(); + assertThat(binding.isStrict()).isTrue(); + assertThat(binding.getDefinition()).isInstanceOf(CelAsyncFunctionOverload.class); + assertThat(overload.applyAsync(15L, 25L).get(5, SECONDS)).isEqualTo(40L); + assertThat(overload.applyAsync(new Object[] {15L, 25L}).get(5, SECONDS)).isEqualTo(40L); + } + + @Test + public void fromAsync_nullArguments_throwsNullPointerException() { + assertThrows( + NullPointerException.class, + () -> CelFunctionBinding.fromAsync(null, Long.class, (Long x) -> immediateFuture(x))); + assertThrows( + NullPointerException.class, + () -> CelFunctionBinding.fromAsync("id", null, (Long x) -> immediateFuture(x))); + assertThrows( + NullPointerException.class, + () -> + CelFunctionBinding.fromAsync( + "id", Long.class, (CelAsyncFunctionOverload.Unary) null)); + + assertThrows( + NullPointerException.class, + () -> + CelFunctionBinding.fromAsync( + null, Long.class, String.class, (a, b) -> immediateFuture(a))); + assertThrows( + NullPointerException.class, + () -> CelFunctionBinding.fromAsync("id", null, String.class, (a, b) -> immediateFuture(a))); + assertThrows( + NullPointerException.class, + () -> CelFunctionBinding.fromAsync("id", Long.class, null, (a, b) -> immediateFuture(a))); + assertThrows( + NullPointerException.class, + () -> + CelFunctionBinding.fromAsync( + "id", + Long.class, + String.class, + (CelAsyncFunctionOverload.Binary) null)); + + assertThrows( + NullPointerException.class, + () -> + CelFunctionBinding.fromAsync( + null, ImmutableList.of(Long.class), args -> immediateFuture(1L))); + assertThrows( + NullPointerException.class, + () -> + CelFunctionBinding.fromAsync( + "id", (Iterable>) null, args -> immediateFuture(1L))); + assertThrows( + NullPointerException.class, + () -> + CelFunctionBinding.fromAsync( + "id", ImmutableList.of(Long.class), (CelAsyncFunctionOverload) null)); + } + + @Test + public void fromAsync_unary_lambdaReturnsNull_returnsNullFuture() throws Exception { + CelFunctionBinding unaryBinding = + CelFunctionBinding.fromAsync( + "null_async", Long.class, (CelAsyncFunctionOverload.Unary) (Long arg) -> null); + CelAsyncFunctionOverload unaryOverload = + (CelAsyncFunctionOverload) unaryBinding.getDefinition(); + + assertThat(unaryOverload.applyAsync(1L)).isNull(); + assertThat(unaryOverload.applyAsync(new Object[] {1L})).isNull(); + } + + @Test + public void fromAsync_binary_lambdaReturnsNull_returnsNullFuture() throws Exception { + CelFunctionBinding binaryBinding = + CelFunctionBinding.fromAsync( + "null_async_bin", + Long.class, + String.class, + (CelAsyncFunctionOverload.Binary) (Long a, String b) -> null); + CelAsyncFunctionOverload binaryOverload = + (CelAsyncFunctionOverload) binaryBinding.getDefinition(); + + assertThat(binaryOverload.applyAsync(1L, "a")).isNull(); + assertThat(binaryOverload.applyAsync(new Object[] {1L, "a"})).isNull(); + } + + @Test + public void fromAsync_unary_synchronousApplyThrowsUnsupportedOperationException() { + CelFunctionBinding binding = + CelFunctionBinding.fromAsync("unary_async", Long.class, (Long arg) -> immediateFuture(arg)); + CelFunctionOverload overload = binding.getDefinition(); + + UnsupportedOperationException e = + assertThrows(UnsupportedOperationException.class, () -> overload.apply(new Object[] {10L})); + + assertThat(e) + .hasMessageThat() + .contains("Async overload cannot be evaluated synchronously. Use evalAsync instead."); + } + + @Test + public void fromAsync_varargs_invokesAsyncAndAdapts() throws Exception { + CelFunctionBinding binding = + CelFunctionBinding.fromAsync( + "custom_async_varargs", + ImmutableList.of(Long.class, String.class), + (Object[] args) -> immediateFuture((Long) args[0] + (String) args[1])); + CelAsyncFunctionOverload overload = (CelAsyncFunctionOverload) binding.getDefinition(); + + assertThat(binding.getOverloadId()).isEqualTo("custom_async_varargs"); + assertThat(binding.getArgTypes()).containsExactly(Long.class, String.class).inOrder(); + assertThat(binding.isStrict()).isTrue(); + assertThat(binding.getDefinition()).isInstanceOf(CelAsyncFunctionOverload.class); + assertThat(overload.applyAsync(new Object[] {10L, "test"}).get(5, SECONDS)).isEqualTo("10test"); + } + + @Test + public void fromOverloads_asyncBinding_throwsIllegalArgumentException() { + CelFunctionBinding asyncBinding = + CelFunctionBinding.fromAsync("async_fn", Long.class, (Long arg) -> immediateFuture(arg)); + + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> CelFunctionBinding.fromOverloads("async_fn", asyncBinding)); + + assertThat(e) + .hasMessageThat() + .contains("Asynchronous function overloads cannot be grouped using fromOverloads."); + } + + @Test + public void fromAsync_withIterableArgTypes_success() throws Exception { + CelAsyncFunctionOverload overload = + args -> immediateFuture((Long) args[0] + (Long) args[1] + (Long) args[2]); + + CelFunctionBinding binding = + CelFunctionBinding.fromAsync( + "ternary_async", ImmutableList.of(Long.class, Long.class, Long.class), overload); + + assertThat(binding.getOverloadId()).isEqualTo("ternary_async"); + assertThat(binding.getArgTypes()).containsExactly(Long.class, Long.class, Long.class).inOrder(); + assertThat(binding.isStrict()).isTrue(); + assertThat(binding.getDefinition()).isSameInstanceAs(overload); + CelAsyncFunctionOverload bindingDef = (CelAsyncFunctionOverload) binding.getDefinition(); + assertThat(bindingDef.applyAsync(new Object[] {10L, 20L, 30L}).get(5, SECONDS)).isEqualTo(60L); + } + + @Test + public void asyncFunctionOverload_defaultMethods_delegatesToVarargsAndThrowsOnSync() + throws Exception { + CelAsyncFunctionOverload overload = + args -> { + long sum = 0L; + for (Object arg : args) { + sum += (Long) arg; + } + return immediateFuture(sum); + }; + + assertThat(overload.applyAsync(42L).get(5, SECONDS)).isEqualTo(42L); + assertThat(overload.applyAsync(10L, 20L).get(5, SECONDS)).isEqualTo(30L); + + UnsupportedOperationException thrown = + assertThrows(UnsupportedOperationException.class, () -> overload.apply(new Object[] {1L})); + assertThat(thrown) + .hasMessageThat() + .contains("Async overload cannot be evaluated synchronously. Use evalAsync instead."); + } + + @Test + public void celFunctionResolver_empty_alwaysReturnsEmpty() throws Exception { + CelFunctionResolver resolver = CelFunctionResolver.EMPTY; + + assertThat(resolver.findOverloadMatchingArgs("fn", new Object[] {1L})).isEmpty(); + assertThat( + resolver.findOverloadMatchingArgs( + "fn", ImmutableList.of("fn_overload"), new Object[] {1L})) + .isEmpty(); + } + + @Test + public void dynamicDispatch_applyVarargs_matchesCorrectOverload() throws Exception { + CelFunctionBinding binding1 = + CelFunctionBinding.from( + "sum_three_longs", + ImmutableList.of(Long.class, Long.class, Long.class), + args -> (Long) args[0] + (Long) args[1] + (Long) args[2]); + CelFunctionBinding binding2 = + CelFunctionBinding.from( + "concat_three_strings", + ImmutableList.of(String.class, String.class, String.class), + args -> (String) args[0] + (String) args[1] + (String) args[2]); + + ImmutableSet overloads = + CelFunctionBinding.fromOverloads("add3", binding1, binding2); + OptimizedFunctionOverload dispatchOverload = + (OptimizedFunctionOverload) + Iterables.find(overloads, b -> b.getOverloadId().equals("add3")).getDefinition(); + + assertThat(dispatchOverload.apply(new Object[] {1L, 2L, 3L})).isEqualTo(6L); + assertThat(dispatchOverload.apply(new Object[] {"a", "b", "c"})).isEqualTo("abc"); + + CelOverloadNotFoundException thrown = + assertThrows( + CelOverloadNotFoundException.class, + () -> dispatchOverload.apply(new Object[] {1L, "b", 3L})); + assertThat(thrown) + .hasMessageThat() + .contains( + "No matching overload for function 'add3'. Overload candidates: sum_three_longs," + + " concat_three_strings"); + } + + @Test + public void fromOverloads_nullOrEmptyFunctionName_throwsIllegalArgumentException() { + CelFunctionBinding binding = CelFunctionBinding.from("fn_1", Long.class, (Long arg) -> 1L); + + assertThrows( + IllegalArgumentException.class, () -> CelFunctionBinding.fromOverloads(null, binding)); + assertThrows( + IllegalArgumentException.class, () -> CelFunctionBinding.fromOverloads("", binding)); + } + + @Test + public void fromOverloads_emptyOverloadBindings_throwsIllegalArgumentException() { + assertThrows( + IllegalArgumentException.class, + () -> CelFunctionBinding.fromOverloads("fn", ImmutableList.of())); + assertThrows(IllegalArgumentException.class, () -> CelFunctionBinding.fromOverloads("fn")); + } + + @Test + public void fromOverloads_varargsAndCollection_success() { + CelFunctionBinding binding1 = + CelFunctionBinding.from("unary_fn", Long.class, (Long arg) -> arg + 1L); + CelFunctionBinding binding2 = + CelFunctionBinding.from("binary_fn", Long.class, Long.class, (Long a, Long b) -> a + b); + + ImmutableSet varargsBindings = + CelFunctionBinding.fromOverloads("poly_fn", binding1, binding2); + ImmutableSet collectionBindings = + CelFunctionBinding.fromOverloads("poly_fn", ImmutableList.of(binding1, binding2)); + + assertThat(varargsBindings).isNotEmpty(); + assertThat(collectionBindings).isNotEmpty(); + } +} From d374a0dd4a3293b5065458cc2e0e78f143c2a86f Mon Sep 17 00:00:00 2001 From: Cristina Borza Date: Mon, 7 Sep 2026 00:25:01 -0700 Subject: [PATCH 186/204] Add optimizer listener PiperOrigin-RevId: 977315425 --- optimizer/BUILD.bazel | 5 ++ .../main/java/dev/cel/optimizer/BUILD.bazel | 15 ++++ .../cel/optimizer/CelOptimizerBuilder.java | 8 ++ .../dev/cel/optimizer/CelOptimizerImpl.java | 85 +++++++++++++++---- .../cel/optimizer/CelOptimizerListener.java | 72 ++++++++++++++++ .../test/java/dev/cel/optimizer/BUILD.bazel | 1 + .../cel/optimizer/CelOptimizerImplTest.java | 66 ++++++++++++++ 7 files changed, 235 insertions(+), 17 deletions(-) create mode 100644 optimizer/src/main/java/dev/cel/optimizer/CelOptimizerListener.java diff --git a/optimizer/BUILD.bazel b/optimizer/BUILD.bazel index 9468b01a9..a86665b38 100644 --- a/optimizer/BUILD.bazel +++ b/optimizer/BUILD.bazel @@ -15,6 +15,11 @@ java_library( exports = ["//optimizer/src/main/java/dev/cel/optimizer:optimizer_builder"], ) +java_library( + name = "optimizer_listener", + exports = ["//optimizer/src/main/java/dev/cel/optimizer:optimizer_listener"], +) + java_library( name = "ast_optimizer", exports = ["//optimizer/src/main/java/dev/cel/optimizer:ast_optimizer"], diff --git a/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel b/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel index 31e410f6a..7a7072c6a 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel +++ b/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel @@ -27,6 +27,19 @@ java_library( ], ) +java_library( + name = "optimizer_listener", + srcs = ["CelOptimizerListener.java"], + tags = [ + ], + deps = [ + ":ast_optimizer", + "//common:cel_ast", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + java_library( name = "optimizer_builder", srcs = [ @@ -38,6 +51,7 @@ java_library( deps = [ ":ast_optimizer", ":optimization_exception", + ":optimizer_listener", "//common:cel_ast", "@maven//:com_google_errorprone_error_prone_annotations", ], @@ -54,6 +68,7 @@ java_library( ":ast_optimizer", ":optimization_exception", ":optimizer_builder", + ":optimizer_listener", "//bundle:cel", "//common:cel_ast", "//common:compiler_common", diff --git a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerBuilder.java b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerBuilder.java index abfed8f38..fb1e28615 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerBuilder.java +++ b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerBuilder.java @@ -28,6 +28,14 @@ public interface CelOptimizerBuilder { @CanIgnoreReturnValue CelOptimizerBuilder addAstOptimizers(Iterable astOptimizers); + /** Adds one or more listeners to observe optimization lifecycle. */ + @CanIgnoreReturnValue + CelOptimizerBuilder addOptimizerListeners(CelOptimizerListener... listeners); + + /** Adds one or more listeners to observe optimization lifecycle. */ + @CanIgnoreReturnValue + CelOptimizerBuilder addOptimizerListeners(Iterable listeners); + /** Build a new instance of the {@link CelOptimizer}. */ @CheckReturnValue CelOptimizer build(); diff --git a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java index f5e30093a..0d2f151c5 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java +++ b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java @@ -32,10 +32,15 @@ final class CelOptimizerImpl implements CelOptimizer { private final Cel cel; private final ImmutableSet astOptimizers; + private final ImmutableSet listeners; - CelOptimizerImpl(Cel cel, ImmutableSet astOptimizers) { + CelOptimizerImpl( + Cel cel, + ImmutableSet astOptimizers, + ImmutableSet listeners) { this.cel = cel; this.astOptimizers = astOptimizers; + this.listeners = listeners; } @Override @@ -44,27 +49,51 @@ public CelAbstractSyntaxTree optimize(CelAbstractSyntaxTree ast) throws CelOptim throw new IllegalArgumentException("AST must be type-checked."); } + listeners.forEach(listener -> listener.onOptimizationStart(ast)); + Cel celOptimizerEnv = cel; CelAbstractSyntaxTree optimizedAst = ast; + try { for (CelAstOptimizer optimizer : astOptimizers) { - OptimizationResult result = optimizer.optimize(optimizedAst, celOptimizerEnv); - if (!result.newFunctionDecls().isEmpty() || !result.newVarDecls().isEmpty()) { - celOptimizerEnv = - celOptimizerEnv - .toCelBuilder() - .addVarDeclarations(result.newVarDecls()) - .addFunctionDeclarations(result.newFunctionDecls()) - .build(); + CelAbstractSyntaxTree preAst = optimizedAst; + try { + for (CelOptimizerListener listener : listeners) { + listener.onPassStart(optimizer, preAst); + } + + OptimizationResult result = optimizer.optimize(optimizedAst, celOptimizerEnv); + + if (!result.newFunctionDecls().isEmpty() || !result.newVarDecls().isEmpty()) { + celOptimizerEnv = + celOptimizerEnv + .toCelBuilder() + .addVarDeclarations(result.newVarDecls()) + .addFunctionDeclarations(result.newFunctionDecls()) + .build(); + } + optimizedAst = celOptimizerEnv.check(result.optimizedAst()).getAst(); + assertAstIdCorrectness(optimizedAst); + + for (CelOptimizerListener listener : listeners) { + listener.onPassEnd(optimizer, preAst, optimizedAst); + } + } catch (CelValidationException e) { + notifyPassFailure(optimizer, preAst, e); + throw new CelOptimizationException( + "Optimized AST failed to type-check: " + e.getMessage(), e); + } catch (CelOptimizationException e) { + notifyPassFailure(optimizer, preAst, e); + throw e; + } catch (RuntimeException e) { + notifyPassFailure(optimizer, preAst, e); + throw new CelOptimizationException("Optimization failure: " + e.getMessage(), e); } - optimizedAst = celOptimizerEnv.check(result.optimizedAst()).getAst(); - assertAstIdCorrectness(optimizedAst); } - } catch (CelValidationException e) { - throw new CelOptimizationException( - "Optimized AST failed to type-check: " + e.getMessage(), e); - } catch (RuntimeException e) { - throw new CelOptimizationException("Optimization failure: " + e.getMessage(), e); + } finally { + for (CelOptimizerListener listener : listeners) { + listener.onOptimizationEnd(ast, optimizedAst); + } } return optimizedAst; @@ -123,6 +152,13 @@ private static void assertAstIdCorrectness(CelAbstractSyntaxTree ast) { } } + private void notifyPassFailure( + CelAstOptimizer optimizer, CelAbstractSyntaxTree ast, Exception failure) { + for (CelOptimizerListener listener : listeners) { + listener.onPassFailure(optimizer, ast, failure); + } + } + /** Create a new builder for constructing a {@link CelOptimizer} instance. */ static CelOptimizerImpl.Builder newBuilder(Cel cel) { return new CelOptimizerImpl.Builder(cel); @@ -132,10 +168,12 @@ static CelOptimizerImpl.Builder newBuilder(Cel cel) { static final class Builder implements CelOptimizerBuilder { private final Cel cel; private final ImmutableSet.Builder astOptimizers; + private final ImmutableSet.Builder listeners; private Builder(Cel cel) { this.cel = cel; this.astOptimizers = ImmutableSet.builder(); + this.listeners = ImmutableSet.builder(); } @Override @@ -151,9 +189,22 @@ public CelOptimizerBuilder addAstOptimizers(Iterable astOptimiz return this; } + @Override + public CelOptimizerBuilder addOptimizerListeners(CelOptimizerListener... listeners) { + checkNotNull(listeners); + return addOptimizerListeners(Arrays.asList(listeners)); + } + + @Override + public CelOptimizerBuilder addOptimizerListeners(Iterable listeners) { + checkNotNull(listeners); + this.listeners.addAll(listeners); + return this; + } + @Override public CelOptimizer build() { - return new CelOptimizerImpl(cel, astOptimizers.build()); + return new CelOptimizerImpl(cel, astOptimizers.build(), listeners.build()); } } } diff --git a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerListener.java b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerListener.java new file mode 100644 index 000000000..9d1ac5d29 --- /dev/null +++ b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerListener.java @@ -0,0 +1,72 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.optimizer; + +import javax.annotation.concurrent.ThreadSafe; +import dev.cel.common.CelAbstractSyntaxTree; + +/** + * Listener interface for observing the execution lifecycle of {@link CelOptimizer}. + * + *

Implementations must be thread-safe. + */ +@ThreadSafe +public interface CelOptimizerListener { + /** + * Invoked before the optimization pipeline begins. + * + * @param ast the initial AST to be optimized. + */ + default void onOptimizationStart(CelAbstractSyntaxTree ast) {} + + /** + * Invoked before a specific {@link CelAstOptimizer} pass executes. + * + * @param optimizer the optimizer pass that is about to execute. + * @param ast the initial AST that is about to be optimized. + */ + default void onPassStart(CelAstOptimizer optimizer, CelAbstractSyntaxTree ast) {} + + /** + * Invoked after a specific {@link CelAstOptimizer} pass completes successfully. + * + * @param optimizer the optimizer pass that just completed. + * @param preAst the initial AST that was passed to the optimizer pass. + * @param optimizedAst the AST after the optimizer pass completed. + */ + default void onPassEnd( + CelAstOptimizer optimizer, + CelAbstractSyntaxTree preAst, + CelAbstractSyntaxTree optimizedAst) {} + + /** + * Invoked if an optimizer pass throws an unhandled exception. + * + * @param optimizer the optimizer pass that threw the exception. + * @param ast the initial AST that was passed to the optimizer pass. + * @param failure the exception that was thrown by the optimizer pass. + */ + default void onPassFailure( + CelAstOptimizer optimizer, CelAbstractSyntaxTree ast, Exception failure) {} + + /** + * Invoked after all optimization passes and final type-checks complete. + * + * @param initialAst the initial AST that was passed to the optimizer. + * @param finalAst the final AST after all optimization passes. + */ + default void onOptimizationEnd( + CelAbstractSyntaxTree initialAst, CelAbstractSyntaxTree finalAst) {} +} diff --git a/optimizer/src/test/java/dev/cel/optimizer/BUILD.bazel b/optimizer/src/test/java/dev/cel/optimizer/BUILD.bazel index 702fe23f3..539f7d341 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/BUILD.bazel +++ b/optimizer/src/test/java/dev/cel/optimizer/BUILD.bazel @@ -32,6 +32,7 @@ java_library( "//optimizer:optimization_exception", "//optimizer:optimizer_builder", "//optimizer:optimizer_impl", + "//optimizer:optimizer_listener", "//parser:macro", "//parser:parser_factory", "//parser:unparser", diff --git a/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerImplTest.java b/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerImplTest.java index 9e92814f2..0867ac0e0 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerImplTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/CelOptimizerImplTest.java @@ -44,6 +44,41 @@ public class CelOptimizerImplTest { .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .build(); + private final List events = new ArrayList<>(); + + private final CelOptimizerListener listener = + new CelOptimizerListener() { + @Override + public void onOptimizationStart(CelAbstractSyntaxTree ast) { + events.add("start"); + } + + @Override + public void onPassStart(CelAstOptimizer optimizer, CelAbstractSyntaxTree ast) { + events.add("pass_start"); + } + + @Override + public void onPassEnd( + CelAstOptimizer optimizer, + CelAbstractSyntaxTree preAst, + CelAbstractSyntaxTree optimizedAst) { + events.add("pass_end"); + } + + @Override + public void onOptimizationEnd( + CelAbstractSyntaxTree initialAst, CelAbstractSyntaxTree finalAst) { + events.add("end"); + } + + @Override + public void onPassFailure( + CelAstOptimizer optimizer, CelAbstractSyntaxTree ast, Exception failure) { + events.add("pass_failure"); + } + }; + @Test public void constructCelOptimizer_success() { CelOptimizer celOptimizer = @@ -313,4 +348,35 @@ public void optimize_validMacroCalls_success() throws Exception { assertThat(optimizedAst).isNotNull(); assertThat(optimizedAst.getSource().getMacroCalls()).hasSize(1); } + + @Test + public void optimize_withListener_invokesListenerMethods() throws Exception { + CelOptimizer celOptimizer = + CelOptimizerImpl.newBuilder(CEL) + .addAstOptimizers((navigableAst, cel) -> OptimizationResult.create(navigableAst)) + .addOptimizerListeners(listener) + .build(); + + CelAbstractSyntaxTree ast = CEL.compile("'hello world'").getAst(); + CelAbstractSyntaxTree unused = celOptimizer.optimize(ast); + + assertThat(events).containsExactly("start", "pass_start", "pass_end", "end").inOrder(); + } + + @Test + public void optimize_withListener_onPassFailure_invokesListenerMethods() throws Exception { + CelOptimizer celOptimizer = + CelOptimizerImpl.newBuilder(CEL) + .addAstOptimizers( + (navigableAst, cel) -> { + throw new RuntimeException("Test failure"); + }) + .addOptimizerListeners(listener) + .build(); + + CelAbstractSyntaxTree ast = CEL.compile("'hello world'").getAst(); + assertThrows(CelOptimizationException.class, () -> celOptimizer.optimize(ast)); + + assertThat(events).containsExactly("start", "pass_start", "pass_failure", "end").inOrder(); + } } From 892dfcec40b4ed6feaaf0fe8f891b4a8ba70f3f5 Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Wed, 9 Sep 2026 11:07:15 -0700 Subject: [PATCH 187/204] [Pratt parser] Initial implementation - private, for now PiperOrigin-RevId: 978644296 --- parser/BUILD.bazel | 6 + .../src/main/java/dev/cel/parser/BUILD.bazel | 26 + .../src/main/java/dev/cel/parser/Lexer.java | 657 +++++++++ .../main/java/dev/cel/parser/PrattParser.java | 1314 +++++++++++++++++ .../src/test/java/dev/cel/parser/BUILD.bazel | 8 +- .../parser/CelParserParameterizedTest.java | 176 +-- .../java/dev/cel/parser/PrattParserTest.java | 582 ++++++++ .../pratt_parser_core_syntax.baseline | 1208 +++++++++++++++ .../resources/pratt_parser_errors.baseline | 514 +++++++ .../resources/pratt_parser_literals.baseline | 470 ++++++ .../resources/pratt_parser_macros.baseline | 903 +++++++++++ .../src/main/java/dev/cel/testing/BUILD.bazel | 4 + .../cel/testing/CelExprKindAndIdAdorner.java | 140 ++ .../dev/cel/testing/CelLocationAdorner.java | 80 + .../src/test/java/dev/cel/testing/BUILD.bazel | 4 + .../testing/CelExprKindAndIdAdornerTest.java | 124 ++ .../cel/testing/CelLocationAdornerTest.java | 72 + 17 files changed, 6116 insertions(+), 172 deletions(-) create mode 100644 parser/src/main/java/dev/cel/parser/Lexer.java create mode 100644 parser/src/main/java/dev/cel/parser/PrattParser.java create mode 100644 parser/src/test/java/dev/cel/parser/PrattParserTest.java create mode 100644 parser/src/test/resources/pratt_parser_core_syntax.baseline create mode 100644 parser/src/test/resources/pratt_parser_errors.baseline create mode 100644 parser/src/test/resources/pratt_parser_literals.baseline create mode 100644 parser/src/test/resources/pratt_parser_macros.baseline create mode 100644 testing/src/main/java/dev/cel/testing/CelExprKindAndIdAdorner.java create mode 100644 testing/src/main/java/dev/cel/testing/CelLocationAdorner.java create mode 100644 testing/src/test/java/dev/cel/testing/CelExprKindAndIdAdornerTest.java create mode 100644 testing/src/test/java/dev/cel/testing/CelLocationAdornerTest.java diff --git a/parser/BUILD.bazel b/parser/BUILD.bazel index 1e662c3c5..8bd568183 100644 --- a/parser/BUILD.bazel +++ b/parser/BUILD.bazel @@ -11,6 +11,12 @@ java_library( exports = ["//parser/src/main/java/dev/cel/parser"], ) +java_library( + name = "pratt_parser", + visibility = ["//:internal"], + exports = ["//parser/src/main/java/dev/cel/parser:pratt_parser"], +) + java_library( name = "parser_factory", exports = ["//parser/src/main/java/dev/cel/parser:parser_factory"], diff --git a/parser/src/main/java/dev/cel/parser/BUILD.bazel b/parser/src/main/java/dev/cel/parser/BUILD.bazel index e32c50ee8..905bf298f 100644 --- a/parser/src/main/java/dev/cel/parser/BUILD.bazel +++ b/parser/src/main/java/dev/cel/parser/BUILD.bazel @@ -15,6 +15,12 @@ PARSER_SOURCES = [ "Parser.java", ] +# keep sorted +PRATT_PARSER_SOURCES = [ + "Lexer.java", + "PrattParser.java", +] + # keep sorted PARSER_BUILDER_SOURCES = [ "CelParser.java", @@ -75,6 +81,26 @@ java_library( ], ) +java_library( + name = "pratt_parser", + srcs = PRATT_PARSER_SOURCES, + tags = [ + ], + deps = [ + ":macro", + "//common:cel_ast", + "//common:cel_source", + "//common:compiler_common", + "//common:operator", + "//common:options", + "//common:source_location", + "//common/ast", + "//common/internal", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + java_library( name = "parser_builder", srcs = PARSER_BUILDER_SOURCES, diff --git a/parser/src/main/java/dev/cel/parser/Lexer.java b/parser/src/main/java/dev/cel/parser/Lexer.java new file mode 100644 index 000000000..894cda9ce --- /dev/null +++ b/parser/src/main/java/dev/cel/parser/Lexer.java @@ -0,0 +1,657 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.parser; + +import com.google.common.collect.ImmutableMap; +import dev.cel.common.internal.CelCodePointArray; +import java.util.function.IntPredicate; +import org.jspecify.annotations.Nullable; + +/** + * Fast lexer for CEL expressions. + * + *

Ported from {@code third_party/cel/cpp/parser/internal/lexer.h} and {@code lexer.cc}. + */ +final class Lexer { + + enum TokenType { + ERROR("error"), + END("end"), + WHITESPACE("whitespace"), + COMMENT("comment"), + + // Keywords + NULL("null"), + FALSE("false"), + TRUE("true"), + IN("in"), + RESERVED_WORD("reserved_word"), + + // Literals + INT("int"), + UINT("uint"), + FLOAT("float"), + STRING("string"), + BYTES("bytes"), + + // Identifiers + IDENT("ident"), + + // Delimiters + LEFT_BRACKET("["), + RIGHT_BRACKET("]"), + LEFT_BRACE("{"), + RIGHT_BRACE("}"), + LEFT_PAREN("("), + RIGHT_PAREN(")"), + + // Operators + DOT("."), + COMMA(","), + MINUS("-"), + PLUS("+"), + ASTERISK("*"), + SLASH("/"), + PERCENT("%"), + QUESTION("?"), + COLON(":"), + EXCLAMATION("!"), + EQUAL("="), + EQUAL_EQUAL("=="), + EXCLAMATION_EQUAL("!="), + LESS("<"), + LESS_EQUAL("<="), + GREATER(">"), + GREATER_EQUAL(">="), + LOGICAL_AND("&&"), + LOGICAL_OR("||"); + + private final String symbol; + + TokenType(String symbol) { + this.symbol = symbol; + } + + public String getSymbol() { + return symbol; + } + + @Override + public String toString() { + return symbol; + } + } + + static final class Token { + final TokenType type; + final int start; + final int end; + + Token(TokenType type, int start, int end) { + this.type = type; + this.start = start; + this.end = end; + } + + @Override + public String toString() { + return "Token(" + type + ", " + start + ", " + end + ")"; + } + } + + static final class LexerError { + final int start; + final int end; + final String message; + + LexerError(int start, int end, String message) { + this.start = start; + this.end = end; + this.message = message; + } + } + + private static final ImmutableMap KEYWORDS = + ImmutableMap.builder() + .put("false", TokenType.FALSE) + .put("true", TokenType.TRUE) + .put("null", TokenType.NULL) + .put("in", TokenType.IN) + .put("as", TokenType.RESERVED_WORD) + .put("break", TokenType.RESERVED_WORD) + .put("const", TokenType.RESERVED_WORD) + .put("continue", TokenType.RESERVED_WORD) + .put("else", TokenType.RESERVED_WORD) + .put("for", TokenType.RESERVED_WORD) + .put("function", TokenType.RESERVED_WORD) + .put("if", TokenType.RESERVED_WORD) + .put("import", TokenType.RESERVED_WORD) + .put("let", TokenType.RESERVED_WORD) + .put("loop", TokenType.RESERVED_WORD) + .put("package", TokenType.RESERVED_WORD) + .put("namespace", TokenType.RESERVED_WORD) + .put("return", TokenType.RESERVED_WORD) + .put("var", TokenType.RESERVED_WORD) + .put("void", TokenType.RESERVED_WORD) + .put("while", TokenType.RESERVED_WORD) + .buildOrThrow(); + + private final CelCodePointArray content; + private int position; + private LexerError error; + + Lexer(CelCodePointArray content) { + this.content = content; + this.position = 0; + this.error = null; + } + + Token lex() { + int start = position; + if (position >= content.size()) { + return makeToken(TokenType.END, start, start); + } + int c = content.get(position); + switch (c) { + case '\f': + case '\n': + case ' ': + case '\r': + case 0x0B: // \v (vertical tab) + case '\t': + { + consumeWhitespace(); + return makeToken(TokenType.WHITESPACE, start, position); + } + case '.': + { + if (position + 1 < content.size() && isDigit(content.get(position + 1))) { + return consumeNumericLiteral(); + } + advance(1); + return makeToken(TokenType.DOT, start, position); + } + case ',': + { + advance(1); + return makeToken(TokenType.COMMA, start, position); + } + case '!': + { + advance(1); + if (consume('=')) { + return makeToken(TokenType.EXCLAMATION_EQUAL, start, position); + } + return makeToken(TokenType.EXCLAMATION, start, position); + } + case '?': + { + advance(1); + return makeToken(TokenType.QUESTION, start, position); + } + case '(': + { + advance(1); + return makeToken(TokenType.LEFT_PAREN, start, position); + } + case ')': + { + advance(1); + return makeToken(TokenType.RIGHT_PAREN, start, position); + } + case '{': + { + advance(1); + return makeToken(TokenType.LEFT_BRACE, start, position); + } + case '}': + { + advance(1); + return makeToken(TokenType.RIGHT_BRACE, start, position); + } + case '[': + { + advance(1); + return makeToken(TokenType.LEFT_BRACKET, start, position); + } + case ']': + { + advance(1); + return makeToken(TokenType.RIGHT_BRACKET, start, position); + } + case '=': + { + advance(1); + if (consume('=')) { + return makeToken(TokenType.EQUAL_EQUAL, start, position); + } + return makeToken(TokenType.EQUAL, start, position); + } + case '<': + { + advance(1); + if (consume('=')) { + return makeToken(TokenType.LESS_EQUAL, start, position); + } + return makeToken(TokenType.LESS, start, position); + } + case '>': + { + advance(1); + if (consume('=')) { + return makeToken(TokenType.GREATER_EQUAL, start, position); + } + return makeToken(TokenType.GREATER, start, position); + } + case ':': + { + advance(1); + return makeToken(TokenType.COLON, start, position); + } + case '%': + { + advance(1); + return makeToken(TokenType.PERCENT, start, position); + } + case '+': + { + advance(1); + return makeToken(TokenType.PLUS, start, position); + } + case '-': + { + advance(1); + return makeToken(TokenType.MINUS, start, position); + } + case '*': + { + advance(1); + return makeToken(TokenType.ASTERISK, start, position); + } + case '/': + { + advance(1); + if (consume('/')) { + consumeLine(); + return makeToken(TokenType.COMMENT, start, position); + } + return makeToken(TokenType.SLASH, start, position); + } + case '&': + { + advance(1); + if (consume('&')) { + return makeToken(TokenType.LOGICAL_AND, start, position); + } + return setError(start, position, "unexpected single '&', expected '&&'"); + } + case '|': + { + advance(1); + if (consume('|')) { + return makeToken(TokenType.LOGICAL_OR, start, position); + } + return setError(start, position, "unexpected single '|', expected '||'"); + } + case '_': + { + return consumeIdent(); + } + case '`': + { + return consumeQuotedIdent(); + } + case '\'': + { + return consumeStringLiteral(start, '\'', false, false); + } + case '"': + { + return consumeStringLiteral(start, '"', false, false); + } + case 'r': + case 'R': + case 'b': + case 'B': + { + Token token = consumePrefixedStringLiteral(); + if (token != null) { + return token; + } + break; + } + default: + break; + } + if (isDigit(c)) { + return consumeNumericLiteral(); + } + if (isAlpha(c)) { + return consumeIdent(); + } + advance(1); + return setError(start, position, "unexpected character"); + } + + LexerError getError() { + return error; + } + + int savePosition() { + return position; + } + + void restorePosition(int pos) { + this.position = pos; + } + + private static boolean isDigit(int c) { + return c >= '0' && c <= '9'; + } + + private static boolean isHexDigit(int c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + } + + private static boolean isAlpha(int c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + } + + private static boolean isIdentTrailing(int c) { + return isDigit(c) || isAlpha(c) || c == '_'; + } + + private static boolean isPlusOrMinus(int c) { + return c == '+' || c == '-'; + } + + private Token makeToken(TokenType type, int start, int end) { + return new Token(type, start, end); + } + + private Token setError(int start, int end, String message) { + this.error = new LexerError(start, end, message); + return new Token(TokenType.ERROR, start, end); + } + + private void advance(int n) { + position += n; + } + + private boolean match(int c) { + return position < content.size() && content.get(position) == c; + } + + private boolean consume(int c) { + if (match(c)) { + advance(1); + return true; + } + return false; + } + + private boolean consumeIf(IntPredicate predicate) { + if (position < content.size()) { + int cp = content.get(position); + if (predicate.test(cp)) { + advance(1); + return true; + } + } + return false; + } + + private void consumeLine() { + while (position < content.size()) { + if (content.get(position) == '\n') { + advance(1); + return; + } + advance(1); + } + } + + private void consumeWhitespace() { + while (position < content.size()) { + int c = content.get(position); + switch (c) { + case '\f': + case '\n': + case ' ': + case '\r': + case 11: // \v + case '\t': + advance(1); + break; + default: + return; + } + } + } + + private boolean consumeDigits() { + boolean advanced = false; + while (position < content.size()) { + int c = content.get(position); + if (!isDigit(c)) { + break; + } + advance(1); + advanced = true; + } + return advanced; + } + + private boolean consumeHexDigits() { + boolean advanced = false; + while (position < content.size()) { + int c = content.get(position); + if (!isHexDigit(c)) { + break; + } + advance(1); + advanced = true; + } + return advanced; + } + + private TokenType consumeIntegralSuffix() { + if (consume('u') || consume('U')) { + return TokenType.UINT; + } + return TokenType.INT; + } + + private Token consumeQuotedIdent() { + int start = position; + advance(1); + if (!consumeUntilAfter('`', /* isRaw= */ true)) { + return setError(start, position, "unterminated quoted identifier"); + } + return makeToken(TokenType.IDENT, start, position); + } + + private boolean consumeUntilAfter(int c, boolean isRaw) { + int pos = position; + boolean escaped = false; + while (pos < content.size()) { + int cc = content.get(pos); + if (cc == '\n' || cc == '\r') { + position = pos; + return false; + } + if (!isRaw && cc == '\\') { + escaped = !escaped; + } else { + if (cc == c && (isRaw || !escaped)) { + position = pos + 1; + return true; + } + escaped = false; + } + pos++; + } + position = content.size(); + return false; + } + + private boolean consumeUntilAfterTripleQuote(int quote, boolean isRaw) { + int pos = position; + boolean escaped = false; + while (pos < content.size()) { + int cc = content.get(pos); + if (!isRaw && cc == '\\') { + escaped = !escaped; + } else { + if ((isRaw || !escaped) + && pos + 2 < content.size() + && cc == quote + && content.get(pos + 1) == quote + && content.get(pos + 2) == quote) { + position = pos + 3; + return true; + } + escaped = false; + } + pos++; + } + position = content.size(); + return false; + } + + private Token consumeStringLiteral(int start, int quote, boolean isBytes, boolean isRaw) { + advance(1); + boolean isTripleQuote = + position + 1 < content.size() + && content.get(position) == quote + && content.get(position + 1) == quote; + if (isTripleQuote) { + advance(2); + if (!consumeUntilAfterTripleQuote(quote, isRaw)) { + return setError( + start, + position, + isBytes ? "unterminated bytes literal" : "unterminated string literal"); + } + return makeToken(isBytes ? TokenType.BYTES : TokenType.STRING, start, position); + } + if (!consumeUntilAfter(quote, isRaw)) { + return setError( + start, position, isBytes ? "unterminated bytes literal" : "unterminated string literal"); + } + return makeToken(isBytes ? TokenType.BYTES : TokenType.STRING, start, position); + } + + private @Nullable Token consumePrefixedStringLiteral() { + int start = position; + if (position >= content.size()) { + return null; + } + int c = content.get(position); + boolean isBytes = (c == 'b' || c == 'B'); + boolean isRaw = (c == 'r' || c == 'R'); + if (!isBytes && !isRaw) { + return null; + } + int lookahead = 1; + if (position + 1 < content.size()) { + int c2 = content.get(position + 1); + if (isBytes ? (c2 == 'r' || c2 == 'R') : (c2 == 'b' || c2 == 'B')) { + isBytes = true; + isRaw = true; + lookahead = 2; + } + } + if (position + lookahead < content.size()) { + int quote = content.get(position + lookahead); + if (quote == '"' || quote == '\'') { + advance(lookahead); + return consumeStringLiteral(start, quote, isBytes, isRaw); + } + } + return null; + } + + private Token consumeNumericLiteral() { + int start = position; + int c = content.get(position); + boolean floatingPoint = false; + if (c == '.') { + floatingPoint = true; + advance(1); + if (!consumeDigits()) { + return setError( + start, position, "floating point literal missing digits after decimal separator"); + } + } else { + advance(1); + if (c == '0' && consume('x')) { + if (!consumeHexDigits()) { + return setError( + start, position, "integral literal missing digits after hexadecimal separator"); + } + TokenType tokenType = consumeIntegralSuffix(); + if (consumeIf(Lexer::isIdentTrailing)) { + return setError( + start, + position, + tokenType.getSymbol() + " literal has unexpected trailing characters"); + } + return makeToken(tokenType, start, position); + } + consumeDigits(); + if (position < content.size() + && content.get(position) == '.' + && position + 1 < content.size() + && isDigit(content.get(position + 1))) { + floatingPoint = true; + advance(1); + consumeDigits(); + } + } + if (consume('e') || consume('E')) { + floatingPoint = true; + consumeIf(Lexer::isPlusOrMinus); + if (!consumeDigits()) { + return setError( + start, position, "floating point literal missing digits after exponent separator"); + } + } + TokenType tokenType = floatingPoint ? TokenType.FLOAT : consumeIntegralSuffix(); + if (consumeIf(Lexer::isIdentTrailing)) { + return setError( + start, position, tokenType.getSymbol() + " literal has unexpected trailing characters"); + } + return makeToken(tokenType, start, position); + } + + private Token consumeIdent() { + int start = position; + while (position < content.size()) { + int c = content.get(position); + if (!isIdentTrailing(c)) { + break; + } + advance(1); + } + int end = position; + String word = content.slice(start, end).toString(); + TokenType keywordType = KEYWORDS.get(word); + if (keywordType != null) { + return makeToken(keywordType, start, end); + } + return makeToken(TokenType.IDENT, start, end); + } +} diff --git a/parser/src/main/java/dev/cel/parser/PrattParser.java b/parser/src/main/java/dev/cel/parser/PrattParser.java new file mode 100644 index 000000000..4132e9c32 --- /dev/null +++ b/parser/src/main/java/dev/cel/parser/PrattParser.java @@ -0,0 +1,1314 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.parser; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelIssue; +import dev.cel.common.CelOptions; +import dev.cel.common.CelSource; +import dev.cel.common.CelSourceLocation; +import dev.cel.common.CelValidationResult; +import dev.cel.common.Operator; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.internal.Constants; +import java.text.ParseException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.jspecify.annotations.Nullable; + +/** Pratt parser implementation for CEL. */ +final class PrattParser { + + private static final String ACCUMULATOR_NAME = "@result"; + private static final CelExpr ERROR = CelExpr.newBuilder().setConstant(Constants.ERROR).build(); + + private static final class BinaryOpInfo { + final int precedence; + final String name; + final boolean isLogical; + final Lexer.TokenType type; + + BinaryOpInfo(int precedence, String name, boolean isLogical, Lexer.TokenType type) { + this.precedence = precedence; + this.name = name; + this.isLogical = isLogical; + this.type = type; + } + } + + private static final BinaryOpInfo LOGICAL_OR_OP = + new BinaryOpInfo(1, Operator.LOGICAL_OR.getFunction(), true, Lexer.TokenType.LOGICAL_OR); + private static final BinaryOpInfo LOGICAL_AND_OP = + new BinaryOpInfo(2, Operator.LOGICAL_AND.getFunction(), true, Lexer.TokenType.LOGICAL_AND); + private static final BinaryOpInfo LESS_OP = + new BinaryOpInfo(3, Operator.LESS.getFunction(), false, Lexer.TokenType.LESS); + private static final BinaryOpInfo LESS_EQUAL_OP = + new BinaryOpInfo(3, Operator.LESS_EQUALS.getFunction(), false, Lexer.TokenType.LESS_EQUAL); + private static final BinaryOpInfo GREATER_OP = + new BinaryOpInfo(3, Operator.GREATER.getFunction(), false, Lexer.TokenType.GREATER); + private static final BinaryOpInfo GREATER_EQUAL_OP = + new BinaryOpInfo( + 3, Operator.GREATER_EQUALS.getFunction(), false, Lexer.TokenType.GREATER_EQUAL); + private static final BinaryOpInfo EQUAL_EQUAL_OP = + new BinaryOpInfo(3, Operator.EQUALS.getFunction(), false, Lexer.TokenType.EQUAL_EQUAL); + private static final BinaryOpInfo EXCLAMATION_EQUAL_OP = + new BinaryOpInfo( + 3, Operator.NOT_EQUALS.getFunction(), false, Lexer.TokenType.EXCLAMATION_EQUAL); + private static final BinaryOpInfo IN_OP = + new BinaryOpInfo(3, Operator.IN.getFunction(), false, Lexer.TokenType.IN); + private static final BinaryOpInfo PLUS_OP = + new BinaryOpInfo(4, Operator.ADD.getFunction(), false, Lexer.TokenType.PLUS); + private static final BinaryOpInfo MINUS_OP = + new BinaryOpInfo(4, Operator.SUBTRACT.getFunction(), false, Lexer.TokenType.MINUS); + private static final BinaryOpInfo ASTERISK_OP = + new BinaryOpInfo(5, Operator.MULTIPLY.getFunction(), false, Lexer.TokenType.ASTERISK); + private static final BinaryOpInfo SLASH_OP = + new BinaryOpInfo(5, Operator.DIVIDE.getFunction(), false, Lexer.TokenType.SLASH); + private static final BinaryOpInfo PERCENT_OP = + new BinaryOpInfo(5, Operator.MODULO.getFunction(), false, Lexer.TokenType.PERCENT); + private static final BinaryOpInfo DEFAULT_OP = + new BinaryOpInfo(0, "", false, Lexer.TokenType.ERROR); + + private static BinaryOpInfo getBinaryOpInfo(Lexer.TokenType type) { + switch (type) { + case LOGICAL_OR: + return LOGICAL_OR_OP; + case LOGICAL_AND: + return LOGICAL_AND_OP; + case LESS: + return LESS_OP; + case LESS_EQUAL: + return LESS_EQUAL_OP; + case GREATER: + return GREATER_OP; + case GREATER_EQUAL: + return GREATER_EQUAL_OP; + case EQUAL_EQUAL: + return EQUAL_EQUAL_OP; + case EXCLAMATION_EQUAL: + return EXCLAMATION_EQUAL_OP; + case IN: + return IN_OP; + case PLUS: + return PLUS_OP; + case MINUS: + return MINUS_OP; + case ASTERISK: + return ASTERISK_OP; + case SLASH: + return SLASH_OP; + case PERCENT: + return PERCENT_OP; + default: + return DEFAULT_OP; + } + } + + private static final class UnaryOp { + final Lexer.Token token; + long id; + + UnaryOp(Lexer.Token token) { + this.token = token; + } + } + + private final CelSource source; + private final CelOptions options; + private final ImmutableMap macros; + private final Lexer lexer; + private final Map positions; + private final Map macroCalls; + private final List issues; + private final PrattMacroExprFactory macroExprFactory; + + private Lexer.Token currentToken; + private Lexer.Token peekToken; + private int recursionDepth; + private int currentLhsDepth; + private long nextId; + private boolean nodeLimitExceeded; + private boolean recursionLimitExceeded; + private int errorCount; + + static CelValidationResult parse( + CelSource source, CelOptions options, Map macros) { + if (source.getContent().size() > options.maxExpressionCodePointSize()) { + return new CelValidationResult( + source, + ImmutableList.of( + CelIssue.formatError( + CelSourceLocation.NONE, + String.format( + "expression code point size exceeds limit: size: %d, limit %d", + source.getContent().size(), options.maxExpressionCodePointSize())))); + } + PrattParser prattParser = new PrattParser(source, options, macros); + CelExpr expr = prattParser.run(); + if (prattParser.recursionLimitExceeded || prattParser.errorCount > 0) { + return new CelValidationResult(source, ImmutableList.copyOf(prattParser.issues)); + } + + CelSource.Builder sourceBuilder = source.toBuilder(); + sourceBuilder.addPositionsMap(prattParser.positions); + sourceBuilder.addAllMacroCalls(prattParser.macroCalls); + + return new CelValidationResult( + CelAbstractSyntaxTree.newParsedAst(expr, sourceBuilder.build()), + ImmutableList.copyOf(prattParser.issues)); + } + + private PrattParser(CelSource source, CelOptions options, Map macros) { + this.source = source; + this.options = options; + this.macros = ImmutableMap.copyOf(macros); + this.lexer = new Lexer(source.getContent()); + this.positions = new HashMap<>(); + this.macroCalls = new HashMap<>(); + this.issues = new ArrayList<>(); + this.macroExprFactory = new PrattMacroExprFactory(); + this.nextId = 1; + initTokenStream(); + } + + CelExpr run() { + CelExpr expr = parseExpr(); + if (recursionLimitExceeded || isRecoveryLimitExceeded()) { + return expr; + } + while (peekToken.type != Lexer.TokenType.END && peekToken.type != Lexer.TokenType.ERROR) { + if (options.enableReservedIds() + && (peekToken.type == Lexer.TokenType.RESERVED_WORD + || peekToken.type == Lexer.TokenType.IN)) { + Lexer.Token resTok = nextToken(); + String resText = normalizeIdent(resTok, /* allowQuoted= */ false); + reportError(resTok.start, String.format("reserved identifier: %s", resText)); + continue; + } + reportSyntaxError(peekToken, "unexpected token after expression"); + break; + } + return expr; + } + + private boolean isRecoveryLimitExceeded() { + return errorCount > options.maxParseErrorRecoveryLimit(); + } + + private void initTokenStream() { + peekToken = nextSignificantToken(true); + } + + private String getTokenText(Lexer.Token tok) { + if (tok.start >= 0 && tok.end >= tok.start && tok.end <= source.getContent().size()) { + return source.getContent().slice(tok.start, tok.end).toString(); + } + return ""; + } + + private Lexer.Token nextSignificantToken(boolean reportError) { + if (isRecoveryLimitExceeded()) { + return new Lexer.Token(Lexer.TokenType.END, 0, 0); + } + while (true) { + Lexer.Token tok = lexer.lex(); + if (tok.type == Lexer.TokenType.WHITESPACE || tok.type == Lexer.TokenType.COMMENT) { + continue; + } + if (tok.type == Lexer.TokenType.ERROR && reportError) { + reportSyntaxError(tok, lexer.getError().message); + if (isRecoveryLimitExceeded()) { + return new Lexer.Token(Lexer.TokenType.END, 0, 0); + } + } + return tok; + } + } + + private Lexer.Token nextToken() { + currentToken = peekToken; + if (isRecoveryLimitExceeded()) { + peekToken = new Lexer.Token(Lexer.TokenType.END, 0, 0); + return currentToken; + } + if (peekToken.type != Lexer.TokenType.END) { + peekToken = nextSignificantToken(true); + } + return currentToken; + } + + private boolean expect(Lexer.TokenType type, String msg) { + if (peekToken.type == type) { + nextToken(); + return true; + } + if (isRecoveryLimitExceeded()) { + return false; + } + if (peekToken.type != Lexer.TokenType.ERROR) { + String errMsg; + if (msg == null || msg.isEmpty()) { + String tokText = getTokenText(peekToken); + String formattedTok = + (peekToken.type == Lexer.TokenType.END) ? "" : "'" + tokText + "'"; + errMsg = "mismatched input " + formattedTok + " expecting '" + type.getSymbol() + "'"; + } else { + errMsg = msg; + } + reportSyntaxError(peekToken, errMsg); + } + synchronizeOnDelimiter(); + return false; + } + + private void synchronizeOnDelimiter() { + if (isRecoveryLimitExceeded()) { + peekToken = new Lexer.Token(Lexer.TokenType.END, 0, 0); + return; + } + while (peekToken.type != Lexer.TokenType.END) { + if (peekToken.type == Lexer.TokenType.COMMA + || peekToken.type == Lexer.TokenType.RIGHT_PAREN + || peekToken.type == Lexer.TokenType.RIGHT_BRACKET + || peekToken.type == Lexer.TokenType.RIGHT_BRACE) { + break; + } + nextToken(); + } + } + + private long nextId(int position) { + long id = nextId++; + if (id > options.maxParseExpressionNodeCount() && !nodeLimitExceeded) { + reportError( + position, + String.format( + "expression node limit (%d) exceeded", options.maxParseExpressionNodeCount())); + nodeLimitExceeded = true; + } + if (!nodeLimitExceeded && position >= 0) { + positions.put(id, position); + } + return id; + } + + private long nextId(Lexer.Token token) { + return nextId(token.start); + } + + private long nextId() { + return nextId(-1); + } + + private void setPosition(long id, Lexer.Token token) { + if (token.start >= 0) { + positions.put(id, token.start); + } + } + + private long copyId(long id) { + if (id == 0) { + return 0; + } + int pos = positions.getOrDefault(id, 0); + return nextId(pos); + } + + private void eraseId(long id) { + positions.remove(id); + if (nextId == id + 1) { + --nextId; + } + } + + private void reportError(int position, String msg) { + CelSourceLocation loc = source.getOffsetLocation(position).orElse(CelSourceLocation.NONE); + reportError(loc, msg); + } + + private void reportError(CelSourceLocation loc, String msg) { + if (errorCount > options.maxParseErrorRecoveryLimit()) { + return; + } + errorCount++; + if (errorCount == options.maxParseErrorRecoveryLimit() + 1) { + issues.add( + CelIssue.formatError( + CelSourceLocation.NONE, + String.format("More than %d parse errors.", options.maxParseErrorRecoveryLimit()))); + peekToken = new Lexer.Token(Lexer.TokenType.END, 0, 0); + } + if (errorCount <= options.maxParseErrorRecoveryLimit()) { + issues.add(CelIssue.formatError(loc, msg)); + } + } + + private void reportSyntaxError(Lexer.Token token, String msg) { + reportError(token.start, "Syntax error: " + msg); + } + + private boolean checkRecursion(int chainDepth, Lexer.Token token) { + if (recursionDepth + chainDepth >= options.maxParseRecursionDepth()) { + if (!recursionLimitExceeded) { + recursionLimitExceeded = true; + reportError( + token.start, + String.format( + "Expression recursion limit exceeded. limit: %d", + options.maxParseRecursionDepth())); + } + return true; + } + return false; + } + + private CelExpr parseExpr() { + if (recursionLimitExceeded || isRecoveryLimitExceeded()) { + return ERROR; + } + if (checkRecursion(0, peekToken)) { + return ERROR; + } + recursionDepth++; + CelExpr expr = parseBinaryAndTernary(0); + recursionDepth--; + return expr; + } + + private CelExpr parseBinaryAndTernary(int minPrec) { + CelExpr lhs = parseSelectorChain(); + int chainDepth = currentLhsDepth; + while (true) { + Lexer.TokenType tok = peekToken.type; + if (tok == Lexer.TokenType.QUESTION && minPrec <= 0) { + lhs = parseTernary(lhs); + continue; + } + + BinaryOpInfo opInfo = getBinaryOpInfo(tok); + if (opInfo.precedence < minPrec || opInfo.precedence == 0) { + break; + } + + if (opInfo.isLogical) { + lhs = parseBalancedLogicalChain(lhs, opInfo); + continue; + } + + Lexer.Token opTok = nextToken(); + chainDepth++; + if (checkRecursion(chainDepth, opTok)) { + return ERROR; + } + long opId = nextId(opTok); + CelExpr rhs = parseBinaryAndTernary(opInfo.precedence + 1); + lhs = buildBinaryCall(opId, opInfo.name, lhs, rhs); + currentLhsDepth = chainDepth; + } + return lhs; + } + + private CelExpr parseTernary(CelExpr lhs) { + Lexer.Token opTok = nextToken(); + long opId = nextId(opTok); + CelExpr trueExpr = parseBinaryAndTernary(1); + if (!expect(Lexer.TokenType.COLON, "expected ':' in conditional expression")) { + return lhs; + } + CelExpr falseExpr = parseExpr(); + return CelExpr.newBuilder() + .setId(opId) + .setCall( + CelExpr.CelCall.newBuilder() + .setFunction(Operator.CONDITIONAL.getFunction()) + .addArgs(lhs) + .addArgs(trueExpr) + .addArgs(falseExpr) + .build()) + .build(); + } + + private CelExpr buildBinaryCall(long opId, String opName, CelExpr lhs, CelExpr rhs) { + return CelExpr.newBuilder() + .setId(opId) + .setCall(CelExpr.CelCall.newBuilder().setFunction(opName).addArgs(lhs).addArgs(rhs).build()) + .build(); + } + + private CelExpr parseBalancedLogicalChain(CelExpr lhs, BinaryOpInfo opInfo) { + List terms = new ArrayList<>(); + List ops = new ArrayList<>(); + terms.add(lhs); + while (peekToken.type == opInfo.type) { + Lexer.Token opTok = nextToken(); + CelExpr rhs = parseBinaryAndTernary(opInfo.precedence + 1); + ops.add(nextId(opTok)); + terms.add(rhs); + } + return balancedTree(opInfo.name, terms, ops, 0, ops.size() - 1); + } + + private CelExpr balancedTree(String op, List terms, List ops, int lo, int hi) { + int mid = (lo + hi + 1) / 2; + CelExpr left; + if (mid == lo) { + left = terms.get(mid); + } else { + left = balancedTree(op, terms, ops, lo, mid - 1); + } + CelExpr right; + if (mid == hi) { + right = terms.get(mid + 1); + } else { + right = balancedTree(op, terms, ops, mid + 1, hi); + } + return CelExpr.newBuilder() + .setId(ops.get(mid)) + .setCall(CelExpr.CelCall.newBuilder().setFunction(op).addArgs(left).addArgs(right).build()) + .build(); + } + + private CelExpr parseSelectorChain() { + CelExpr lhs = parseUnary(); + currentLhsDepth = 0; + Lexer.TokenType tok = peekToken.type; + if (tok == Lexer.TokenType.DOT + || tok == Lexer.TokenType.LEFT_BRACKET + || tok == Lexer.TokenType.LEFT_BRACE) { + lhs = parseSelectorChainTail(lhs); + } + return lhs; + } + + private CelExpr parseSelectorChainTail(CelExpr initialLhs) { + CelExpr lhs = initialLhs; + int chainDepth = 0; + while (true) { + Lexer.TokenType tok = peekToken.type; + if (tok == Lexer.TokenType.DOT) { + chainDepth++; + if (checkRecursion(chainDepth, peekToken)) { + return ERROR; + } + Lexer.Token dotTok = nextToken(); + boolean optional = false; + if (peekToken.type == Lexer.TokenType.QUESTION) { + nextToken(); + optional = true; + if (!options.enableOptionalSyntax()) { + reportError(dotTok.start, "unsupported syntax '.?'"); + } + } + Lexer.Token idTok = nextToken(); + if (idTok.type != Lexer.TokenType.IDENT + && idTok.type != Lexer.TokenType.RESERVED_WORD + && idTok.type != Lexer.TokenType.IN) { + if (idTok.type != Lexer.TokenType.ERROR) { + reportSyntaxError(idTok, "expected identifier after '.'"); + } + synchronizeOnDelimiter(); + currentLhsDepth = chainDepth; + return lhs; + } + boolean isMemberCall = (peekToken.type == Lexer.TokenType.LEFT_PAREN); + String idText = normalizeIdent(idTok, /* allowQuoted= */ !isMemberCall); + if (optional) { + long opId = nextId(dotTok); + CelExpr arg1 = lhs; + CelExpr arg2 = + CelExpr.newBuilder() + .setId(nextId(idTok)) + .setConstant(CelConstant.ofValue(idText)) + .build(); + lhs = + CelExpr.newBuilder() + .setId(opId) + .setCall( + CelExpr.CelCall.newBuilder() + .setFunction(Operator.OPTIONAL_SELECT.getFunction()) + .addArgs(arg1) + .addArgs(arg2) + .build()) + .build(); + } else if (peekToken.type == Lexer.TokenType.LEFT_PAREN) { + Lexer.Token lparen = nextToken(); + long callId = nextId(lparen); + ImmutableList args = parseArguments(Lexer.TokenType.RIGHT_PAREN); + Optional expanded = tryExpandMacro(callId, idText, lhs, args); + if (expanded.isPresent()) { + lhs = expanded.get(); + } else { + lhs = + CelExpr.newBuilder() + .setId(callId) + .setCall( + CelExpr.CelCall.newBuilder() + .setFunction(idText) + .setTarget(lhs) + .addArgs(args) + .build()) + .build(); + } + } else { + lhs = + CelExpr.newBuilder() + .setId(nextId(dotTok)) + .setSelect( + CelExpr.CelSelect.newBuilder().setOperand(lhs).setField(idText).build()) + .build(); + } + } else if (tok == Lexer.TokenType.LEFT_BRACKET) { + chainDepth++; + if (checkRecursion(chainDepth, peekToken)) { + return ERROR; + } + Lexer.Token bracketTok = nextToken(); + long opId = nextId(bracketTok); + boolean optional = false; + if (peekToken.type == Lexer.TokenType.QUESTION) { + nextToken(); + optional = true; + if (!options.enableOptionalSyntax()) { + reportError(bracketTok.start, "unsupported syntax '?'"); + } + } + CelExpr index = parseExpr(); + expect(Lexer.TokenType.RIGHT_BRACKET, "expected ']'"); + String opName = + optional ? Operator.OPTIONAL_INDEX.getFunction() : Operator.INDEX.getFunction(); + lhs = + CelExpr.newBuilder() + .setId(opId) + .setCall( + CelExpr.CelCall.newBuilder() + .setFunction(opName) + .addArgs(lhs) + .addArgs(index) + .build()) + .build(); + } else if (tok == Lexer.TokenType.LEFT_BRACE) { + // Position must be retrieved before extractStructName erases the expression IDs. + int structPos = getLeftmostPosition(lhs); + String structName = extractStructName(lhs).orElse(null); + if (structName == null) { + break; + } + lhs = parseStruct(nextId(structPos), structName); + } else { + break; + } + } + currentLhsDepth = chainDepth; + return lhs; + } + + private CelExpr parseUnary() { + Lexer.TokenType tok = peekToken.type; + if (tok == Lexer.TokenType.EXCLAMATION || tok == Lexer.TokenType.MINUS) { + return parseUnaryOps(); + } + return parsePrimary(); + } + + private CelExpr parseUnaryOps() { + Lexer.Token op = nextToken(); + Lexer.TokenType opType = op.type; + if (peekToken.type == Lexer.TokenType.EXCLAMATION || peekToken.type == Lexer.TokenType.MINUS) { + return parseUnaryOpsChain(op); + } + + if (opType == Lexer.TokenType.MINUS) { + if (peekToken.type == Lexer.TokenType.INT) { + return parseIntLiteral(nextId(op), /* isNegative= */ true); + } + if (peekToken.type == Lexer.TokenType.FLOAT) { + return parseDoubleLiteral(nextId(op), /* isNegative= */ true); + } + } + + if (checkRecursion(1, op)) { + return ERROR; + } + + long opId = nextId(op); + recursionDepth++; + CelExpr operand = parseSelectorChain(); + recursionDepth--; + if (recursionLimitExceeded) { + return ERROR; + } + + String opName = + (opType == Lexer.TokenType.EXCLAMATION) + ? Operator.LOGICAL_NOT.getFunction() + : Operator.NEGATE.getFunction(); + return CelExpr.newBuilder() + .setId(opId) + .setCall(CelExpr.CelCall.newBuilder().setFunction(opName).addArgs(operand).build()) + .build(); + } + + private CelExpr parseUnaryOpsChain(Lexer.Token firstOp) { + List ops = new ArrayList<>(); + ops.add(new UnaryOp(firstOp)); + while (peekToken.type == Lexer.TokenType.EXCLAMATION + || peekToken.type == Lexer.TokenType.MINUS) { + ops.add(new UnaryOp(nextToken())); + } + + boolean hasSolitaryTrailingMinus = + !ops.isEmpty() + && Iterables.getLast(ops).token.type == Lexer.TokenType.MINUS + && (ops.size() == 1 || ops.get(ops.size() - 2).token.type != Lexer.TokenType.MINUS); + + if (!options.retainRepeatedUnaryOperators()) { + int write = 0; + for (int read = 0; read < ops.size(); ) { + int next = read; + while (next < ops.size() && ops.get(next).token.type == ops.get(read).token.type) { + next++; + } + if ((next - read) % 2 != 0) { + ops.set(write++, ops.get(read)); + } + read = next; + } + ops = new ArrayList<>(ops.subList(0, write)); + } + + for (UnaryOp op : ops) { + op.id = nextId(op.token); + } + + boolean isNegativeNumericLiteral = + hasSolitaryTrailingMinus + && (peekToken.type == Lexer.TokenType.INT || peekToken.type == Lexer.TokenType.FLOAT); + long negativeLiteralOpId = 0; + if (isNegativeNumericLiteral) { + negativeLiteralOpId = Iterables.getLast(ops).id; + ops.remove(ops.size() - 1); + } + + int chainDepth = 0; + for (UnaryOp op : ops) { + chainDepth++; + if (checkRecursion(chainDepth, op.token)) { + return ERROR; + } + } + + recursionDepth += ops.size(); + CelExpr operand; + if (isNegativeNumericLiteral) { + operand = + (peekToken.type == Lexer.TokenType.INT) + ? parseIntLiteral(negativeLiteralOpId, /* isNegative= */ true) + : parseDoubleLiteral(negativeLiteralOpId, /* isNegative= */ true); + operand = parseSelectorChainTail(operand); + } else { + operand = parseSelectorChain(); + } + recursionDepth -= ops.size(); + + if (recursionLimitExceeded) { + return ERROR; + } + + for (int i = ops.size() - 1; i >= 0; --i) { + String opName = + (ops.get(i).token.type == Lexer.TokenType.EXCLAMATION) + ? Operator.LOGICAL_NOT.getFunction() + : Operator.NEGATE.getFunction(); + operand = + CelExpr.newBuilder() + .setId(ops.get(i).id) + .setCall(CelExpr.CelCall.newBuilder().setFunction(opName).addArgs(operand).build()) + .build(); + } + + return operand; + } + + private CelExpr parseIdentOrCall() { + Lexer.TokenType tokType = peekToken.type; + boolean leadingDot = false; + Lexer.Token firstTok = peekToken; + if (tokType == Lexer.TokenType.DOT) { + nextToken(); + leadingDot = true; + } + Lexer.Token idTok = nextToken(); + if (idTok.type != Lexer.TokenType.IDENT && idTok.type != Lexer.TokenType.RESERVED_WORD) { + if (idTok.type != Lexer.TokenType.ERROR) { + reportSyntaxError(idTok, "expected identifier"); + } + return CelExpr.newBuilder().setId(nextId(idTok)).build(); + } + String idText = normalizeIdent(idTok, /* allowQuoted= */ false); + if (idTok.type == Lexer.TokenType.RESERVED_WORD && options.enableReservedIds()) { + reportError(idTok.start, String.format("reserved identifier: %s", idText)); + } + String name = leadingDot ? "." + idText : idText; + if (peekToken.type == Lexer.TokenType.LEFT_PAREN) { + Lexer.Token lparen = nextToken(); + long callId = nextId(lparen); + ImmutableList args = parseArguments(Lexer.TokenType.RIGHT_PAREN); + Optional expanded = tryExpandMacro(callId, name, null, args); + if (expanded.isPresent()) { + return expanded.get(); + } + return CelExpr.newBuilder() + .setId(callId) + .setCall(CelExpr.CelCall.newBuilder().setFunction(name).addArgs(args).build()) + .build(); + } + long id = nextId(leadingDot ? firstTok : idTok); + return CelExpr.newBuilder() + .setId(id) + .setIdent(CelExpr.CelIdent.newBuilder().setName(name).build()) + .build(); + } + + private CelExpr parsePrimary() { + switch (peekToken.type) { + case LEFT_PAREN: + { + int groupingParenCount = countGroupingParentheses(); + for (int i = 0; i < groupingParenCount; ++i) { + nextToken(); + } + CelExpr expr = parseExpr(); + for (int i = 0; i < groupingParenCount; ++i) { + expect(Lexer.TokenType.RIGHT_PAREN, ""); + } + return expr; + } + case NULL: + return CelExpr.newBuilder().setId(nextId(nextToken())).setConstant(Constants.NULL).build(); + case TRUE: + case FALSE: + { + Lexer.Token tok = nextToken(); + return CelExpr.newBuilder() + .setId(nextId(tok)) + .setConstant(tok.type == Lexer.TokenType.TRUE ? Constants.TRUE : Constants.FALSE) + .build(); + } + case INT: + return parseIntLiteral(/* nodeId= */ -1, /* isNegative= */ false); + case UINT: + return parseUintLiteral(); + case FLOAT: + return parseDoubleLiteral(/* nodeId= */ -1, /* isNegative= */ false); + case STRING: + return parseStringLiteral(); + case BYTES: + return parseBytesLiteral(); + case LEFT_BRACKET: + return parseList(); + case LEFT_BRACE: + return parseMap(); + case DOT: + case IDENT: + case RESERVED_WORD: + return parseIdentOrCall(); + default: + { + Lexer.Token badTok = nextToken(); + if (badTok.type != Lexer.TokenType.ERROR) { + if (badTok.type == Lexer.TokenType.END) { + reportSyntaxError(badTok, "mismatched input '' expecting expression"); + } else { + reportSyntaxError(badTok, "unexpected token"); + } + } + return CelExpr.newBuilder().setId(nextId(badTok)).build(); + } + } + } + + private CelExpr parseList() { + Lexer.Token openTok = nextToken(); + long listId = nextId(openTok); + CelExpr.CelList.Builder listBuilder = CelExpr.CelList.newBuilder(); + int elemIndex = 0; + while (peekToken.type != Lexer.TokenType.RIGHT_BRACKET + && peekToken.type != Lexer.TokenType.END) { + boolean optional = false; + if (peekToken.type == Lexer.TokenType.QUESTION) { + Lexer.Token q = nextToken(); + optional = true; + if (!options.enableOptionalSyntax()) { + reportError(q.start, "unsupported syntax '?'"); + } + } + listBuilder.addElements(parseExpr()); + if (optional) { + listBuilder.addOptionalIndices(elemIndex); + } + elemIndex++; + if (peekToken.type == Lexer.TokenType.COMMA) { + nextToken(); + } else { + break; + } + } + expect(Lexer.TokenType.RIGHT_BRACKET, "expected ']'"); + return CelExpr.newBuilder().setId(listId).setList(listBuilder.build()).build(); + } + + private CelExpr parseMap() { + Lexer.Token openTok = nextToken(); + long mapId = nextId(openTok); + CelExpr.CelMap.Builder mapBuilder = CelExpr.CelMap.newBuilder(); + while (peekToken.type != Lexer.TokenType.RIGHT_BRACE && peekToken.type != Lexer.TokenType.END) { + boolean optional = false; + Lexer.Token keyStart = peekToken; + if (keyStart.type == Lexer.TokenType.QUESTION) { + Lexer.Token q = nextToken(); + optional = true; + if (!options.enableOptionalSyntax()) { + reportError(q.start, "unsupported syntax '?'"); + } + keyStart = peekToken; + } + long entryId = nextId(); + CelExpr key = parseExpr(); + Lexer.Token colon = peekToken; + if (!expect(Lexer.TokenType.COLON, "expected ':' in map entry")) { + break; + } + setPosition(entryId, colon); + CelExpr value = parseExpr(); + mapBuilder.addEntries( + CelExpr.CelMap.Entry.newBuilder() + .setId(entryId) + .setKey(key) + .setValue(value) + .setOptionalEntry(optional) + .build()); + if (peekToken.type == Lexer.TokenType.COMMA) { + nextToken(); + } else { + break; + } + } + expect(Lexer.TokenType.RIGHT_BRACE, "expected '}'"); + return CelExpr.newBuilder().setId(mapId).setMap(mapBuilder.build()).build(); + } + + private CelExpr parseStruct(long objId, String structName) { + nextToken(); + CelExpr.CelStruct.Builder structBuilder = + CelExpr.CelStruct.newBuilder().setMessageName(structName); + while (peekToken.type != Lexer.TokenType.RIGHT_BRACE && peekToken.type != Lexer.TokenType.END) { + boolean optional = false; + if (peekToken.type == Lexer.TokenType.QUESTION) { + Lexer.Token q = nextToken(); + optional = true; + if (!options.enableOptionalSyntax()) { + reportError(q.start, "unsupported syntax '?'"); + } + } + Lexer.Token fieldTok = nextToken(); + if (fieldTok.type != Lexer.TokenType.IDENT + && fieldTok.type != Lexer.TokenType.RESERVED_WORD) { + reportSyntaxError(fieldTok, "expected struct field name"); + synchronizeOnDelimiter(); + break; + } + String fieldName = normalizeIdent(fieldTok, /* allowQuoted= */ true); + Lexer.Token colon = peekToken; + if (!expect(Lexer.TokenType.COLON, "expected ':' in struct field")) { + break; + } + long fieldId = nextId(colon); + CelExpr value = parseExpr(); + structBuilder.addEntries( + CelExpr.CelStruct.Entry.newBuilder() + .setId(fieldId) + .setFieldKey(fieldName) + .setValue(value) + .setOptionalEntry(optional) + .build()); + if (peekToken.type == Lexer.TokenType.COMMA) { + nextToken(); + } else { + break; + } + } + expect(Lexer.TokenType.RIGHT_BRACE, "expected '}'"); + return CelExpr.newBuilder().setId(objId).setStruct(structBuilder.build()).build(); + } + + private ImmutableList parseArguments(Lexer.TokenType closeToken) { + ImmutableList.Builder args = ImmutableList.builder(); + if (peekToken.type != closeToken && peekToken.type != Lexer.TokenType.END) { + while (true) { + args.add(parseExpr()); + if (peekToken.type == Lexer.TokenType.COMMA) { + nextToken(); + if (peekToken.type == closeToken) { + reportError(peekToken.start, "unexpected token"); + break; + } + continue; + } + break; + } + } + expect(closeToken, ""); + return args.build(); + } + + private CelExpr parseIntLiteral(long nodeId, boolean isNegative) { + Lexer.Token tok = nextToken(); + String text = isNegative ? "-" + getTokenText(tok) : getTokenText(tok); + long id = nodeId == -1 ? nextId(tok) : nodeId; + try { + CelConstant constExpr = Constants.parseInt(text); + return CelExpr.newBuilder().setId(id).setConstant(constExpr).build(); + } catch (ParseException e) { + reportSyntaxError(tok, "invalid int literal"); + return CelExpr.newBuilder().setId(nextId(tok)).build(); + } + } + + private CelExpr parseUintLiteral() { + Lexer.Token tok = nextToken(); + String value = getTokenText(tok); + try { + CelConstant constExpr = Constants.parseUint(value); + return CelExpr.newBuilder().setId(nextId(tok)).setConstant(constExpr).build(); + } catch (ParseException e) { + reportSyntaxError(tok, "invalid uint literal"); + return CelExpr.newBuilder().setId(nextId(tok)).build(); + } + } + + private CelExpr parseDoubleLiteral(long nodeId, boolean isNegative) { + Lexer.Token tok = nextToken(); + String text = isNegative ? "-" + getTokenText(tok) : getTokenText(tok); + long id = nodeId == -1 ? nextId(tok) : nodeId; + try { + CelConstant constExpr = Constants.parseDouble(text); + if (Double.isInfinite(constExpr.doubleValue())) { + reportSyntaxError(tok, "invalid double literal"); + return CelExpr.newBuilder().setId(id).build(); + } + return CelExpr.newBuilder().setId(id).setConstant(constExpr).build(); + } catch (ParseException e) { + reportSyntaxError(tok, "invalid double literal"); + return CelExpr.newBuilder().setId(nextId(tok)).build(); + } + } + + private CelExpr parseStringLiteral() { + Lexer.Token tok = nextToken(); + String value = getTokenText(tok); + try { + CelConstant constExpr = Constants.parseString(value); + return CelExpr.newBuilder().setId(nextId(tok)).setConstant(constExpr).build(); + } catch (ParseException e) { + reportError(tok.start, e.getMessage()); + return CelExpr.newBuilder().setId(nextId(tok)).build(); + } + } + + private CelExpr parseBytesLiteral() { + Lexer.Token tok = nextToken(); + String value = getTokenText(tok); + try { + CelConstant constExpr = Constants.parseBytes(value); + return CelExpr.newBuilder().setId(nextId(tok)).setConstant(constExpr).build(); + } catch (ParseException e) { + reportError(tok.start, e.getMessage()); + return CelExpr.newBuilder().setId(nextId(tok)).build(); + } + } + + private String normalizeIdent(Lexer.Token tok, boolean allowQuoted) { + String text = getTokenText(tok); + if (text.isEmpty()) { + return ""; + } + if (text.charAt(0) == '`') { + if (!allowQuoted) { + reportError(tok.start, "unexpected quoted identifier"); + return ""; + } + if (!options.enableQuotedIdentifierSyntax()) { + reportError(tok.start, "unsupported syntax '`'"); + } + if (text.length() < 2 || text.charAt(text.length() - 1) != '`') { + reportError(tok.start, "unterminated quoted identifier"); + return ""; + } + String inner = text.substring(1, text.length() - 1); + if (inner.isEmpty()) { + reportError(tok.start, "unexpected quoted identifier"); + return ""; + } + for (int i = 0; i < inner.length(); i++) { + char c = inner.charAt(i); + if (!isAsciiAlphanumeric(c) && c != '_' && c != '.' && c != '-' && c != '/' && c != ' ') { + reportError(tok.start, "unexpected quoted identifier"); + return ""; + } + } + return inner; + } + return text; + } + + private static boolean isAsciiAlphanumeric(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'); + } + + private Optional extractStructName(CelExpr expr) { + if (expr.exprKind().getKind() == CelExpr.ExprKind.Kind.IDENT) { + String name = expr.ident().name(); + eraseId(expr.id()); + return Optional.of(name); + } + if (expr.exprKind().getKind() == CelExpr.ExprKind.Kind.SELECT) { + if (expr.select().testOnly()) { + return Optional.empty(); + } + CelExpr operand = expr.select().operand(); + eraseId(expr.id()); + return extractStructName(operand) + .map(prefix -> prefix + "." + expr.select().field()); + } + return Optional.empty(); + } + + private int getLeftmostPosition(CelExpr expr) { + if (expr.exprKind().getKind() == CelExpr.ExprKind.Kind.IDENT) { + return positions.getOrDefault(expr.id(), 0); + } + if (expr.exprKind().getKind() == CelExpr.ExprKind.Kind.SELECT) { + return getLeftmostPosition(expr.select().operand()); + } + return positions.getOrDefault(expr.id(), 0); + } + + private Optional lookupMacro(String id, int argCount, boolean receiverStyle) { + String key = CelMacro.formatKey(id, argCount, receiverStyle); + CelMacro macro = macros.get(key); + if (macro != null) { + return Optional.of(macro); + } + key = CelMacro.formatVarArgKey(id, receiverStyle); + return Optional.ofNullable(macros.get(key)); + } + + private Optional tryExpandMacro( + long exprId, String function, @Nullable CelExpr target, ImmutableList args) { + if (function.isEmpty()) { + return Optional.empty(); + } + boolean isReceiver = (target != null); + int argCount = args.size(); + Optional macro = lookupMacro(function, argCount, isReceiver); + if (!macro.isPresent()) { + return Optional.empty(); + } + if (nodeLimitExceeded) { + reportError( + positions.getOrDefault(exprId, 0), + "could not expand macro: expression node limit exceeded"); + return Optional.empty(); + } + + Optional errorArg = args.stream().filter(ERROR::equals).findAny(); + if (errorArg.isPresent() || (target != null && target.equals(ERROR))) { + eraseId(exprId); + return Optional.of(ERROR); + } + + int macroPosition = positions.getOrDefault(exprId, 0); + CelExpr targetExpr = (target != null ? target : CelExpr.newBuilder().build()); + Optional expandedExpr = expandMacro(macroPosition, macro.get(), targetExpr, args); + + if (expandedExpr.isPresent()) { + if (options.populateMacroCalls()) { + recordMacroCall(expandedExpr.get().id(), function, target, args); + } + eraseId(exprId); + return expandedExpr; + } + return Optional.empty(); + } + + private Optional expandMacro( + int position, CelMacro macro, CelExpr target, ImmutableList arguments) { + macroExprFactory.pushPosition(position); + try { + return macro.getExpander().expandMacro(macroExprFactory, target, arguments); + } finally { + macroExprFactory.popPosition(); + } + } + + private void recordMacroCall( + long macroId, String function, CelExpr target, ImmutableList args) { + CelExpr.CelCall.Builder callExpr = CelExpr.CelCall.newBuilder().setFunction(function); + if (target != null) { + if (macroCalls.containsKey(target.id())) { + callExpr.setTarget(CelExpr.newBuilder().setId(target.id()).build()); + } else { + callExpr.setTarget(buildMacroCallArgs(target)); + } + } + for (CelExpr arg : args) { + callExpr.addArgs(buildMacroCallArgs(arg)); + } + macroCalls.put(macroId, CelExpr.newBuilder().setCall(callExpr.build()).build()); + } + + private CelExpr buildMacroCallArgs(CelExpr expr) { + CelExpr.Builder resultExpr = CelExpr.newBuilder().setId(expr.id()); + if (macroCalls.containsKey(expr.id())) { + return resultExpr.build(); + } + if (expr.exprKind().getKind() == CelExpr.ExprKind.Kind.CALL) { + CelExpr.CelCall.Builder callExpr = + CelExpr.CelCall.newBuilder().setFunction(expr.call().function()); + expr.call().args().forEach(arg -> callExpr.addArgs(buildMacroCallArgs(arg))); + expr.call().target().ifPresent(target -> callExpr.setTarget(buildMacroCallArgs(target))); + return resultExpr.setCall(callExpr.build()).build(); + } + return expr; + } + + private int countGroupingParentheses() { + if (peekToken.type != Lexer.TokenType.LEFT_PAREN) { + return 0; + } + + int savedPos = lexer.savePosition(); + try { + int leadingOpenParens = 1; + Lexer.Token tok = nextSignificantToken(/* reportError= */ false); + while (tok.type == Lexer.TokenType.LEFT_PAREN) { + leadingOpenParens++; + tok = nextSignificantToken(/* reportError= */ false); + } + if (leadingOpenParens == 1) { + return 1; + } + + int openParens = leadingOpenParens; + int consecutiveLeadingClosed = 0; + + while (openParens > 0) { + if (tok.type == Lexer.TokenType.END || tok.type == Lexer.TokenType.ERROR) { + return 1; + } + + if (tok.type == Lexer.TokenType.LEFT_PAREN) { + openParens++; + consecutiveLeadingClosed = 0; + } else if (tok.type == Lexer.TokenType.RIGHT_PAREN) { + if (leadingOpenParens == openParens) { + leadingOpenParens--; + consecutiveLeadingClosed++; + } else { + consecutiveLeadingClosed = 0; + } + openParens--; + } else { + consecutiveLeadingClosed = 0; + } + + if (openParens > 0) { + tok = nextSignificantToken(/* reportError= */ false); + } + } + + return Math.max(1, consecutiveLeadingClosed); + } finally { + lexer.restorePosition(savedPos); + } + } + + private final class PrattMacroExprFactory extends CelMacroExprFactory { + private final ArrayDeque macroPositions = new ArrayDeque<>(1); + + void pushPosition(int position) { + macroPositions.addLast(position); + } + + void popPosition() { + macroPositions.removeLast(); + } + + int peekPosition() { + return macroPositions.peekLast(); + } + + @Override + public CelExpr reportError(CelIssue error) { + issues.add(error); + if (!error.getSourceLocation().equals(CelSourceLocation.NONE)) { + Optional offset = source.getLocationOffset(error.getSourceLocation()); + if (offset.isPresent()) { + return CelExpr.newBuilder().setId(nextId(offset.get())).build(); + } + } + return ERROR; + } + + @Override + public String getAccumulatorVarName() { + return ACCUMULATOR_NAME; + } + + @Override + protected CelSourceLocation getSourceLocation(long exprId) { + int pos = positions.getOrDefault(exprId, -1); + return source.getOffsetLocation(pos).orElse(CelSourceLocation.NONE); + } + + @Override + protected CelSourceLocation currentSourceLocationForMacro() { + int pos = + !macroPositions.isEmpty() + ? peekPosition() + : (currentToken != null ? currentToken.start : 0); + return source.getOffsetLocation(pos).orElse(CelSourceLocation.NONE); + } + + @Override + protected long copyExprId(long id) { + return copyId(id); + } + + @Override + public long nextExprId() { + int pos = !macroPositions.isEmpty() ? peekPosition() : -1; + return nextId(pos); + } + } +} diff --git a/parser/src/test/java/dev/cel/parser/BUILD.bazel b/parser/src/test/java/dev/cel/parser/BUILD.bazel index 1ade0181d..eea155e92 100644 --- a/parser/src/test/java/dev/cel/parser/BUILD.bazel +++ b/parser/src/test/java/dev/cel/parser/BUILD.bazel @@ -10,10 +10,12 @@ package( java_library( name = "tests", testonly = True, - srcs = glob(["*Test.java"]), + srcs = glob( + ["*Test.java"], + exclude = ["TmpPrattParserTest.java"], + ), resources = ["//parser/src/test/resources:baselines"], deps = [ - "//:auto_value", "//:java_truth", "//common:cel_ast", "//common:cel_source", @@ -30,12 +32,12 @@ java_library( "//parser:macro", "//parser:parser_builder", "//parser:parser_factory", + "//parser:pratt_parser", "//parser:unparser", "//parser:unparser_visitor", "//testing:adorner", "//testing:baseline_test_case", "@cel_spec//proto/cel/expr:syntax_java_proto", - "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_guava_guava_testlib", "@maven//:com_google_protobuf_protobuf_java", diff --git a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java index 019cea520..7c364cbb9 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java @@ -14,24 +14,10 @@ package dev.cel.parser; -import static java.util.Collections.reverseOrder; -import static java.util.Map.Entry.comparingByKey; -import static java.util.stream.Collectors.joining; -import dev.cel.expr.Constant; -import dev.cel.expr.Expr; -import dev.cel.expr.ExprOrBuilder; import dev.cel.expr.ParsedExpr; import dev.cel.expr.SourceInfo; -import com.google.auto.value.AutoValue; -import com.google.common.base.Ascii; -import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; -import com.google.errorprone.annotations.Immutable; -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.Descriptors.EnumDescriptor; -import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.Descriptors.OneofDescriptor; import com.google.protobuf.TextFormat; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.common.CelAbstractSyntaxTree; @@ -44,11 +30,9 @@ import dev.cel.common.ast.CelExpr; import dev.cel.extensions.CelOptionalLibrary; import dev.cel.testing.BaselineTestCase; -import dev.cel.testing.CelAdorner; import dev.cel.testing.CelDebug; -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Map; +import dev.cel.testing.CelExprKindAndIdAdorner; +import dev.cel.testing.CelLocationAdorner; import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -349,16 +333,18 @@ private void runTest(CelParser parser, String expression, boolean validateParseO testOutput() .println( "P: " - + CelDebug.toAdornedDebugString(parsedExpr.getExpr(), new KindAndIdAdorner())); + + CelDebug.toAdornedDebugString( + parsedExpr.getExpr(), new CelExprKindAndIdAdorner())); String locationOutput = CelDebug.toAdornedDebugString( - parsedExpr.getExpr(), new LocationAdorner(parsedExpr.getSourceInfo())); + parsedExpr.getExpr(), new CelLocationAdorner(parsedExpr.getSourceInfo())); if (!locationOutput.isEmpty()) { testOutput().println("L: " + locationOutput); } } - String macroOutput = convertMacroCallsToString(parsedExpr.getSourceInfo()); + String macroOutput = + CelExprKindAndIdAdorner.convertMacroCallsToString(parsedExpr.getSourceInfo()); if (!macroOutput.isEmpty()) { testOutput().println("M: " + macroOutput); } @@ -377,152 +363,4 @@ private void runSourceInfoTest(String expression) throws Exception { testOutput().println("=====>"); testOutput().println("S: " + TextFormat.printer().printToString(sourceInfo)); } - - private String convertMacroCallsToString(SourceInfo sourceInfo) { - KindAndIdAdorner macroCallsAdorner = new KindAndIdAdorner(sourceInfo); - // Sort in ascending order so that nested macro calls are always in the same order for tests - // output debug string. Ascending order keeps the macro calls map in order from outermost/first - // macro to the innermost/last macro for readability. - return sourceInfo.getMacroCallsMap().entrySet().stream() - .sorted(reverseOrder(comparingByKey())) - .map((entry) -> CelDebug.toAdornedDebugString(entry.getValue(), macroCallsAdorner)) - .collect(joining(",\n")); - } - - private static final class KindAndIdAdorner implements CelAdorner { - - private final SourceInfo sourceInfo; - - KindAndIdAdorner() { - this(SourceInfo.getDefaultInstance()); - } - - KindAndIdAdorner(SourceInfo sourceInfo) { - this.sourceInfo = sourceInfo; - } - - @Override - public String adorn(ExprOrBuilder expr) { - if (this.sourceInfo != null && this.sourceInfo.containsMacroCalls(expr.getId())) { - return String.format( - "^#%d:%s#", - expr.getId(), - this.sourceInfo.getMacroCallsOrThrow(expr.getId()).getCallExpr().getFunction()); - } - - if (expr.hasConstExpr()) { - Constant constExpr = expr.getConstExpr(); - Descriptor descriptor = Constant.getDescriptor(); - OneofDescriptor oneof = findOneofByName(descriptor, "constant_kind"); - FieldDescriptor field = constExpr.getOneofFieldDescriptor(oneof); - if (field.getType() == FieldDescriptor.Type.ENUM) { - return String.format("^#%d:%s#", expr.getId(), getContainedName(field.getEnumType())); - } else { - return String.format( - "^#%d:%s#", expr.getId(), Ascii.toLowerCase(field.getType().toString())); - } - } - Descriptor descriptor = Expr.getDescriptor(); - OneofDescriptor oneof = findOneofByName(descriptor, "expr_kind"); - FieldDescriptor field = expr.getOneofFieldDescriptor(oneof); - return String.format("^#%d:%s#", expr.getId(), getContainedName(field.getMessageType())); - } - - @Override - public String adorn(Expr.CreateStruct.EntryOrBuilder entry) { - return String.format("^#%d:Expr.CreateStruct.Entry#", entry.getId()); - } - } - - @AutoValue - @Immutable - abstract static class LineAndColumn { - - public abstract int getLine(); - - public abstract int getColumn(); - } - - private static final class LocationAdorner implements CelAdorner { - - private final SourceInfo sourceInfo; - - LocationAdorner(SourceInfo sourceInfo) { - this.sourceInfo = sourceInfo; - } - - @Override - public String adorn(ExprOrBuilder expr) { - return getLocation(expr.getId()) - .map( - location -> - String.format( - "^#%d[%d,%d]#", expr.getId(), location.getLine(), location.getColumn())) - .orElseGet(() -> String.format("^#%d[NO_POS]#", expr.getId())); - } - - @Override - public String adorn(Expr.CreateStruct.EntryOrBuilder entry) { - return getLocation(entry.getId()) - .map( - location -> - String.format( - "^#%d[%d,%d]#", entry.getId(), location.getLine(), location.getColumn())) - .orElseGet(() -> String.format("^#%d[NO_POS]#", entry.getId())); - } - - private Optional getLocation(long exprId) { - Map positions = sourceInfo.getPositionsMap(); - Integer position = positions.get(exprId); - if (position == null) { - return Optional.empty(); - } - int line = 1; - for (int index = 0; index < sourceInfo.getLineOffsetsCount(); index++) { - if (sourceInfo.getLineOffsets(index) > position) { - break; - } else { - line++; - } - } - int column = position; - if (line > 1) { - column = position - sourceInfo.getLineOffsets(line - 2); - } - return Optional.of(new AutoValue_CelParserParameterizedTest_LineAndColumn(line, column)); - } - } - - private static OneofDescriptor findOneofByName(Descriptor descriptor, String name) { - for (OneofDescriptor oneof : descriptor.getOneofs()) { - if (oneof.getName().equals(name)) { - return oneof; - } - } - return null; - } - - private static final Joiner JOINER = Joiner.on('.'); - - private static String getContainedName(Descriptor descriptor) { - Deque parts = new ArrayDeque<>(); - parts.addFirst(descriptor.getName()); - Descriptor containing = descriptor.getContainingType(); - while (containing != null) { - parts.addFirst(containing.getName()); - containing = containing.getContainingType(); - } - return JOINER.join(parts); - } - - private static String getContainedName(EnumDescriptor descriptor) { - Deque parts = new ArrayDeque<>(); - parts.addFirst(descriptor.getName()); - Descriptor containing = descriptor.getContainingType(); - while (containing != null) { - parts.addFirst(containing.getName()); - containing = containing.getContainingType(); - } - return JOINER.join(parts); - } } diff --git a/parser/src/test/java/dev/cel/parser/PrattParserTest.java b/parser/src/test/java/dev/cel/parser/PrattParserTest.java new file mode 100644 index 000000000..53ac0703b --- /dev/null +++ b/parser/src/test/java/dev/cel/parser/PrattParserTest.java @@ -0,0 +1,582 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.parser; + +import static com.google.common.collect.ImmutableMap.toImmutableMap; + +import dev.cel.expr.ParsedExpr; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelOptions; +import dev.cel.common.CelProtoAbstractSyntaxTree; +import dev.cel.common.CelSource; +import dev.cel.common.CelValidationException; +import dev.cel.common.CelValidationResult; +import dev.cel.testing.BaselineTestCase; +import dev.cel.testing.CelDebug; +import dev.cel.testing.CelExprKindAndIdAdorner; +import dev.cel.testing.CelLocationAdorner; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class PrattParserTest extends BaselineTestCase { + + private static final CelOptions OPTIONS = + CelOptions.current() + .populateMacroCalls(true) + .enableOptionalSyntax(true) + .enableQuotedIdentifierSyntax(true) + .build(); + + private static final CelOptions OPTIONS_MAX_RECURSION_DEPTH_32 = + OPTIONS.toBuilder().maxParseRecursionDepth(32).build(); + + private static final CelOptions OPTIONS_NO_OPTIONAL_SYNTAX = + OPTIONS.toBuilder().enableOptionalSyntax(false).build(); + + private static final CelOptions OPTIONS_QUOTED_IDENTIFIER_SYNTAX = + OPTIONS.toBuilder().enableQuotedIdentifierSyntax(true).build(); + + private static final CelOptions OPTIONS_NO_QUOTED_IDENTIFIER_SYNTAX = + OPTIONS.toBuilder().enableQuotedIdentifierSyntax(false).build(); + + private static final CelOptions OPTIONS_MAX_CODE_POINT_SIZE_5 = + OPTIONS.toBuilder().maxExpressionCodePointSize(5).build(); + + private static final CelOptions OPTIONS_MAX_NODE_COUNT_2 = + OPTIONS.toBuilder().maxParseExpressionNodeCount(2).build(); + + private static final CelOptions OPTIONS_MAX_ERROR_RECOVERY_LIMIT_2 = + OPTIONS.toBuilder().maxParseErrorRecoveryLimit(2).build(); + + private static final ImmutableMap MACROS = + ImmutableMap.builder() + .putAll( + CelStandardMacro.STANDARD_MACROS.stream() + .map(CelStandardMacro::getDefinition) + .collect(toImmutableMap(CelMacro::getKey, Function.identity()))) + .put( + CelStandardMacro.EXISTS_ONE_NEW.getDefinition().getKey(), + CelStandardMacro.EXISTS_ONE_NEW.getDefinition()) + .put( + "noop_macro", + CelMacro.newGlobalVarArgMacro("noop_macro", (a, b, c) -> Optional.empty())) + .buildOrThrow(); + + @Test + public void pratt_parser_literals() { + // Null + runTest("null"); + + // Boolean + runTest("true"); + runTest("false"); + + // Int + runTest("0"); + runTest("42"); + runTest("0xF"); + runTest("0x2A"); + runTest("-1"); + runTest("-42"); + runTest("0xFFFFFFFFFFFFFFFFF"); + runTest("9223372036854775807"); // Long.MAX_VALUE + runTest("-9223372036854775808"); // Long.MIN_VALUE + runTest("-(9223372036854775808)"); // error + runTest("123a"); + + // Uint + runTest("0u"); + runTest("23u"); + runTest("0xFu"); + runTest("0xFFFFFFFFFFFFFFFFFu"); + runTest("123u_"); + + // Double + runTest("3.14"); + runTest("23.39"); + runTest("1."); + runTest("1e+5"); + runTest("1e-5"); + runTest("2.5e+10"); + runTest("2.5e-10"); + runTest("1.99e90000009"); + runTest("1e"); + runTest("1e+"); + runTest("1e-"); + runTest("2.5e"); + runTest("2.5e+"); + runTest("2.5e-"); + runTest("((1e))"); + runTest("0x123z"); + + // String + runTest("'hello'"); + runTest("\"A\""); + runTest("'''hello\nworld'''"); + runTest("\"\\u2764\""); + runTest("\"\u2764\""); + runTest("\"\\\"\""); + runTest("\"\\xC3\\XBF\""); + runTest("\"\\303\\277\""); + runTest("\"hi\\u263A \\u263Athere\""); + runTest("\"\\U000003A8\\?\""); + runTest("\"\\a\\b\\f\\n\\r\\t\\v'\\\"\\\\\\? Legal escapes\""); + runTest("\"\"\"hello\nworld\"\"\""); + runTest("r\"\"\"hello\nworld\"\"\""); + runTest("\"\"\"\"\"\""); + runTest("''''''"); + runTest("\"\"\"hello\\\"\"\"world\"\"\""); + runTest("'''hello\\'''world'''"); + runTest("\"\\xFh\""); + runTest("\"\\a\\b\\f\\n\\r\\t\\v\\'\\\"\\\\\\? Illegal escape \\>\""); + runTest( + " '\ud83d\ude01' in ['\ud83d\ude01', '\ud83d\ude11', '\ud83d\ude26']\n" + + "\t\t\t&& in.\ud83d\ude01"); + runTest("\"\"\"hello\nworld"); + runTest("'''hello\nworld"); + runTest("r\"\"\"hello\nworld"); + runTest("\"hello\nworld\""); + runTest("'hello\nworld'"); + runTest("r\"hello\nworld\""); + runTest("`hello\nworld`"); + runTest("\"hello\rworld\""); + runTest("'unterminated"); + + // Bytes + runTest("b'abc'"); + runTest("b\"abc\""); + runTest("b\"\"\"hello\nworld"); + runTest("b\"hello\nworld\""); + runTest("rb\"hello\nworld\""); + runTest("br'abc'"); + runTest("bR'abc'"); + runTest("Br'abc'"); + runTest("BR'abc'"); + runTest("rb'abc'"); + runTest("rB'abc'"); + runTest("Rb'abc'"); + runTest("RB'abc'"); + runTest("br'a\\'b'"); + runTest("rb'a\\'b'"); + } + + @Test + @SuppressWarnings("InlineMeInliner") // String.repeat is unavailable under Java 8 + public void pratt_parser_core_syntax() { + // Identifiers + runTest("a"); + runTest("foo"); + + // Parentheses + runTest("(a)"); + runTest("((a))"); + runTest("(((1 + 2))) * 3"); + + // Lists + runTest("[]"); + runTest("[a]"); + runTest("[a, b, c]"); + runTest("[1, 2, 3]"); + runTest("[3, 4, 5]"); + runTest("[3, 4, 5,]"); + runTest("[?a, b]"); + runTest("[?a, ?b]"); + runTest("[?a[?b]]"); + + // Maps + runTest("{}"); + runTest("{a:b, c:d}"); + runTest("{foo: 5, bar: \"xyz\"}"); + runTest("{foo: 5, bar: \"xyz\", }"); + runTest("{\"a\": 1, \"b\": 2}"); + runTest("{1:2u, 2:3u}"); + runTest("{?a: b}"); + runTest("{?'key': value}"); + + // Messages + runTest("foo{ }"); + runTest("foo{ a:b }"); + runTest("foo{ a:b, c:d }"); + runTest("SomeMessage{foo: 5, bar: \"xyz\"}"); + runTest("TestAllTypes{single_int32: 1, single_int64: 2}"); + runTest("MyType{foo: 1, bar: 'baz'}"); + runTest("Message{`in`: true}"); + runTest("Msg{?field: value}"); + + // Field selection + runTest("a.b"); + runTest("a.b.c"); + runTest("a.?b"); + runTest("a.`b-c`"); + runTest("a.`b c`"); + runTest("a.`b.c`"); + runTest("a.`in`"); + runTest("a.`/foo`"); + runTest("a.`my-var`"); + + // Indexing + runTest("a[b]"); + runTest("a[0]"); + runTest("a[3]"); + runTest("[1,3,4][0]"); + runTest("a[?0]"); + + // Function calls + runTest("a()"); + runTest("a(b)"); + runTest("a(b, c)"); + runTest("a.b()"); + runTest("a.b(c)"); + runTest("a.b(5)"); + runTest("a.foo(1, 2)"); + + // Unary operators + runTest("!a"); + runTest("!x"); + runTest("! false"); + runTest("-a"); + + // Arithmetic operators + runTest("x * 2"); + runTest("x * 2u"); + runTest("x * 2.0"); + runTest("a * b"); + runTest("a / b"); + runTest("a % b"); + runTest("a + b"); + runTest("a - b"); + runTest("4--4"); + runTest("4--4.1"); + runTest("\"abc\" + \"def\""); + runTest("b\"abc\" + B\"def\""); + runTest("[] + [1,2,3,] + [4]"); + runTest("1 + 2 * 3"); + + // Comparison operators + runTest("a == b"); + runTest("a != b"); + runTest("a < b"); + runTest("a <= b"); + runTest("a > b"); + runTest("a >= b"); + runTest("a in b"); + runTest("\"\ud83d\ude01\" in [\"\ud83d\ude01\", \"\ud83d\ude11\", \"\ud83d\ude26\"]"); + runTest("size(x) == x.size()"); + runTest("x.single_nested_message != null"); + + // Logical operators + runTest("a && b"); + runTest("a && b && c"); + runTest("a && b && c && d && e && f && g"); + runTest("a > 5 && a < 10"); + runTest("a || b"); + runTest("a || b || c || d || e || f"); + runTest("a < 5 || a > 10"); + runTest("a && b && c && d || e && f && g && h"); + + // Conditional operator + runTest("a?b:c"); + runTest("cond ? 1 : 2"); + runTest("false && !true || false ? 2 : 3"); + runTest(OPTIONS_MAX_RECURSION_DEPTH_32, Strings.repeat("true ? 1 : ", 31) + "1", false); + runTest(OPTIONS_MAX_RECURSION_DEPTH_32, Strings.repeat("!-", 15) + "x", false); + + // Complex expressions + runTest("1 + 2 * 3 - 1 / 2 == 6 % 1"); + runTest("x[\"a\"].single_int32 == 23"); + runTest("a.?b[?0] && a[?c]"); + + // Whitespace and comments + runTest("// comment\na"); + runTest("a // comment"); + runTest("a\n// comment\n+ b"); + runTest("a / // comment\n b"); + runTest("[\n 1, // comment\n 2,\n]"); + } + + @Test + public void pratt_parser_macros() { + runTest("has(m.f)"); + runTest("has(a.b)"); + runTest("has(m)"); + + runTest("m.all(v, f)"); + runTest("[1, 2].all(x, x > 0)"); + + runTest("m.exists(v, f)"); + + runTest("m.existsOne(v, f)"); + runTest("[].existsOne(__result__, __result__)"); + + runTest("m.map(v, f)"); + runTest("m.map(v, p, f)"); + runTest("m.map(__result__, __result__)"); + + runTest("m.filter(v, p)"); + runTest("m.filter(__result__, false)"); + runTest("m.filter(a.b, false)"); + + // Nested / Chained macros + runTest("x.filter(y, y.filter(z, z > 0))"); + runTest("has(a.b).filter(c, c)"); + runTest("x.filter(y, y.exists(z, has(z.a)) && y.exists(z, has(z.b)))"); + runTest("(has(a.b) || has(c.d)).string()"); + runTest("has(a.b).asList().exists(c, c)"); + runTest("[has(a.b), has(c.d)].exists(e, e)"); + + // Custom macros + runTest("noop_macro(123)"); + } + + @Test + @SuppressWarnings("InlineMeInliner") // String.repeat is unavailable under Java 8 + public void pratt_parser_errors() { + // Lexical errors + runTest("*@a | b"); + runTest("((@))"); + runTest("1 + $"); + runTest( + "\u00f3\u00a0\u00a2\n" + + "\t\t\u00f3\u00a00\u00a0\n" + + "\t\t\u007f0\"\"\"\\\"\"\\\"\"\"\\\"\"\\\"\"\"\\\"\"\\\"\"\"\\\"\"\\\"\"\"\\\"\\\"\"\"\\\"\"\\\"\"\"\\\"\"\\\"\"\"\\\"\"\\\"\"\"\\\"!\\\"\"\"\\\"\"\\\"\"\"\\\"\"\\\""); + runTest("'\\udead' == '\\ufffd'"); + runTest("a | b"); + runTest("'3# < 10\" '& tru ^^"); + + // Unexpected tokens + runTest("1 + +"); + runTest("?"); + runTest("a ? b ((?))"); + runTest("a ? b @"); + runTest( + "-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1\n" + + "\t\t--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1--1--0--1--1--1\n" + + "\t\t--3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1\n" + + "\t\t--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1\n" + + "\t\t--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1\n" + + "\t\t--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1\n" + + "\t\t--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1--1--0--1--1--1\n" + + "\t\t--3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1\n" + + "\t\t--3-[-1--1--1--1---1-1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1\n" + + "\t\t--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1\n" + + "\t\t--1--1---1--1-\u00c01--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1\n" + + "\t\t--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1\n" + + "\t\t--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1\n" + + "\t\t--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1\n" + + "\t\t--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1\n" + + "\t\t--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1\n" + + "\t\t--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1\n" + + "\t\t--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1\n" + + "\t\t--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1\n" + + "\t\t--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1\n" + + "\t\t--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1\n" + + "\t\t--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1\n" + + "\t\t--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1\n" + + "\t\t--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1\n" + + "\t\t--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1\n" + + "\t\t--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1\n" + + "\t\t--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1\n" + + "\t\t--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1\n" + + "\t\t--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1\n" + + "\t\t--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1\n" + + "\t\t--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1\n" + + "\t\t--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1\n" + + "\t\t--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1\n" + + "\t\t--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1"); + + // Reserved identifiers + runTest( + "as break const continue else for function if import in let loop package namespace" + + " return var void while"); + runTest("[1, 2, 3].map(var, var * var)"); + + // Incomplete expressions + runTest("1 +"); + runTest("--"); + runTest("{"); + runTest("0x"); + + // Unexpected token after expression + runTest("TestAllTypes(){}"); + runTest("TestAllTypes{}()"); + runTest("1 + 2\n3 +"); + + // Member selection errors + runTest("{\"a\": 1}.\"a\""); + runTest("self.true == 1"); + + // Map syntax errors + runTest("{a}"); + runTest("{:a}"); + + // Message syntax errors + runTest("func{{a}}"); + runTest("msg{:a}"); + runTest("ind[a{b}]"); + runTest("x{?."); + runTest("x{."); + + // Macro errors + runTest("1.all(2, 3)"); + + // Unsupported optional syntax + runTest(OPTIONS_NO_OPTIONAL_SYNTAX, "a.?b && a[?b]"); + runTest(OPTIONS_NO_OPTIONAL_SYNTAX, "[?a, ?b]"); + runTest(OPTIONS_NO_OPTIONAL_SYNTAX, "Msg{?field: value} && {?'key': value}"); + + // Unsupported quoted identifier syntax + runTest(OPTIONS_NO_QUOTED_IDENTIFIER_SYNTAX, "a.`b-c`"); + runTest(OPTIONS_NO_QUOTED_IDENTIFIER_SYNTAX, "a.`b.c`"); + runTest(OPTIONS_NO_QUOTED_IDENTIFIER_SYNTAX, "a.`in`"); + runTest(OPTIONS_NO_QUOTED_IDENTIFIER_SYNTAX, "a.`/foo`"); + runTest(OPTIONS_NO_QUOTED_IDENTIFIER_SYNTAX, "Message{`in`: true}"); + + // Unsupported quoted identifier location + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "`b-c`"); + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "`b-c`()"); + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "a.`$b`"); + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "a.`b.c`()"); + + // Recursion limit exceeded + runTest( + OPTIONS_MAX_RECURSION_DEPTH_32, + "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[\n" + + "\t\t\t[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['too many']]]]]]]]]]]]]]]]]]]]]]]]]]]]\n" + + "\t\t\t]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"); + runTest( + OPTIONS_MAX_RECURSION_DEPTH_32, + "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['not fine']]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"); + runTest( + OPTIONS_MAX_RECURSION_DEPTH_32, + "a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.A.B.C.D.E.F.G.H"); + runTest( + OPTIONS_MAX_RECURSION_DEPTH_32, + "a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]\n" + + "\t\t [21][22][23][24][25][26][27][28][29][30][31][32][33]"); + runTest( + OPTIONS_MAX_RECURSION_DEPTH_32, + "1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10\n" + + "\t\t+ 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20\n" + + "\t\t+ 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 30\n" + + "\t\t+ 31 + 32 + 33 + 34"); + runTest( + OPTIONS_MAX_RECURSION_DEPTH_32, + "a < 1 < 2 < 3 < 4 < 5 < 6 < 7 < 8 < 9 < 10 < 11\n" + + "\t\t < 12 < 13 < 14 < 15 < 16 < 17 < 18 < 19 < 20 < 21\n" + + "\t\t\t < 22 < 23 < 24 < 25 < 26 < 27 < 28 < 29 < 30 < 31\n" + + "\t\t\t < 32 < 33"); + runTest( + OPTIONS_MAX_RECURSION_DEPTH_32, + "y!=y!=y!=y!=y!=y!=y!=y!=y!=-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y\n" + + "\t\t!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y\n" + + "\t\t!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y\n" + + "\t\t!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y\n" + + "\t\t!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y\n" + + "\t\t!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y"); + runTest( + OPTIONS_MAX_RECURSION_DEPTH_32, + "a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]"); + runTest(OPTIONS_MAX_RECURSION_DEPTH_32, Strings.repeat("true ? 1 : ", 33) + "1"); + runTest(OPTIONS_MAX_RECURSION_DEPTH_32, Strings.repeat("!-", 16) + "!x"); + runTest(OPTIONS_MAX_CODE_POINT_SIZE_5, "123456"); + runTest(OPTIONS_MAX_NODE_COUNT_2, "1 + 2 + 3"); + runTest(OPTIONS_MAX_ERROR_RECOVERY_LIMIT_2, "[?, ?, ?]"); + runTest(OPTIONS_MAX_ERROR_RECOVERY_LIMIT_2, "[1 2 3 a b c]"); + } + + private void runTest(String expression) { + runTest(OPTIONS, expression); + } + + private void runTest(CelOptions options, String expression) { + runTest(options, expression, true); + } + + private void runTest(CelOptions options, String expression, boolean validateParseOutput) { + runTest(options, MACROS, expression, validateParseOutput); + } + + private void runTest( + CelOptions options, + Map macros, + String expression, + boolean validateParseOutput) { + testOutput().println("I: " + expression.replace("\t", "»")); + testOutput().println("=====>"); + + CelSource source = CelSource.newBuilder(expression).setDescription("").build(); + CelValidationResult parseResult = PrattParser.parse(source, options, macros); + + try { + CelProtoAbstractSyntaxTree protoAst = + CelProtoAbstractSyntaxTree.fromCelAst(parseResult.getAst()); + ParsedExpr parsedExpr = protoAst.toParsedExpr(); + if (validateParseOutput) { + testOutput() + .println( + "P: " + + CelDebug.toAdornedDebugString( + parsedExpr.getExpr(), new CelExprKindAndIdAdorner())); + String locationOutput = + CelDebug.toAdornedDebugString( + parsedExpr.getExpr(), new CelLocationAdorner(parsedExpr.getSourceInfo())); + if (!locationOutput.isEmpty()) { + testOutput().println("L: " + locationOutput); + } + } + + String macroOutput = + CelExprKindAndIdAdorner.convertMacroCallsToString(parsedExpr.getSourceInfo()); + if (!macroOutput.isEmpty()) { + testOutput().println("M: " + macroOutput); + } + } catch (CelValidationException e) { + testOutput().println("E: " + e.getMessage()); + } + + testOutput().println(); + } +} diff --git a/parser/src/test/resources/pratt_parser_core_syntax.baseline b/parser/src/test/resources/pratt_parser_core_syntax.baseline new file mode 100644 index 000000000..278f27eb8 --- /dev/null +++ b/parser/src/test/resources/pratt_parser_core_syntax.baseline @@ -0,0 +1,1208 @@ +I: a +=====> +P: a^#1:Expr.Ident# +L: a^#1[1,0]# + +I: foo +=====> +P: foo^#1:Expr.Ident# +L: foo^#1[1,0]# + +I: (a) +=====> +P: a^#1:Expr.Ident# +L: a^#1[1,1]# + +I: ((a)) +=====> +P: a^#1:Expr.Ident# +L: a^#1[1,2]# + +I: (((1 + 2))) * 3 +=====> +P: _*_( + _+_( + 1^#1:int64#, + 2^#3:int64# + )^#2:Expr.Call#, + 3^#5:int64# +)^#4:Expr.Call# +L: _*_( + _+_( + 1^#1[1,3]#, + 2^#3[1,7]# + )^#2[1,5]#, + 3^#5[1,14]# +)^#4[1,12]# + +I: [] +=====> +P: []^#1:Expr.CreateList# +L: []^#1[1,0]# + +I: [a] +=====> +P: [ + a^#2:Expr.Ident# +]^#1:Expr.CreateList# +L: [ + a^#2[1,1]# +]^#1[1,0]# + +I: [a, b, c] +=====> +P: [ + a^#2:Expr.Ident#, + b^#3:Expr.Ident#, + c^#4:Expr.Ident# +]^#1:Expr.CreateList# +L: [ + a^#2[1,1]#, + b^#3[1,4]#, + c^#4[1,7]# +]^#1[1,0]# + +I: [1, 2, 3] +=====> +P: [ + 1^#2:int64#, + 2^#3:int64#, + 3^#4:int64# +]^#1:Expr.CreateList# +L: [ + 1^#2[1,1]#, + 2^#3[1,4]#, + 3^#4[1,7]# +]^#1[1,0]# + +I: [3, 4, 5] +=====> +P: [ + 3^#2:int64#, + 4^#3:int64#, + 5^#4:int64# +]^#1:Expr.CreateList# +L: [ + 3^#2[1,1]#, + 4^#3[1,4]#, + 5^#4[1,7]# +]^#1[1,0]# + +I: [3, 4, 5,] +=====> +P: [ + 3^#2:int64#, + 4^#3:int64#, + 5^#4:int64# +]^#1:Expr.CreateList# +L: [ + 3^#2[1,1]#, + 4^#3[1,4]#, + 5^#4[1,7]# +]^#1[1,0]# + +I: [?a, b] +=====> +P: [ + ?a^#2:Expr.Ident#, + b^#3:Expr.Ident# +]^#1:Expr.CreateList# +L: [ + ?a^#2[1,2]#, + b^#3[1,5]# +]^#1[1,0]# + +I: [?a, ?b] +=====> +P: [ + ?a^#2:Expr.Ident#, + ?b^#3:Expr.Ident# +]^#1:Expr.CreateList# +L: [ + ?a^#2[1,2]#, + ?b^#3[1,6]# +]^#1[1,0]# + +I: [?a[?b]] +=====> +P: [ + ?_[?_]( + a^#2:Expr.Ident#, + b^#4:Expr.Ident# + )^#3:Expr.Call# +]^#1:Expr.CreateList# +L: [ + ?_[?_]( + a^#2[1,2]#, + b^#4[1,5]# + )^#3[1,3]# +]^#1[1,0]# + +I: {} +=====> +P: {}^#1:Expr.CreateStruct# +L: {}^#1[1,0]# + +I: {a:b, c:d} +=====> +P: { + a^#3:Expr.Ident#:b^#4:Expr.Ident#^#2:Expr.CreateStruct.Entry#, + c^#6:Expr.Ident#:d^#7:Expr.Ident#^#5:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: { + a^#3[1,1]#:b^#4[1,3]#^#2[1,2]#, + c^#6[1,6]#:d^#7[1,8]#^#5[1,7]# +}^#1[1,0]# + +I: {foo: 5, bar: "xyz"} +=====> +P: { + foo^#3:Expr.Ident#:5^#4:int64#^#2:Expr.CreateStruct.Entry#, + bar^#6:Expr.Ident#:"xyz"^#7:string#^#5:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: { + foo^#3[1,1]#:5^#4[1,6]#^#2[1,4]#, + bar^#6[1,9]#:"xyz"^#7[1,14]#^#5[1,12]# +}^#1[1,0]# + +I: {foo: 5, bar: "xyz", } +=====> +P: { + foo^#3:Expr.Ident#:5^#4:int64#^#2:Expr.CreateStruct.Entry#, + bar^#6:Expr.Ident#:"xyz"^#7:string#^#5:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: { + foo^#3[1,1]#:5^#4[1,6]#^#2[1,4]#, + bar^#6[1,9]#:"xyz"^#7[1,14]#^#5[1,12]# +}^#1[1,0]# + +I: {"a": 1, "b": 2} +=====> +P: { + "a"^#3:string#:1^#4:int64#^#2:Expr.CreateStruct.Entry#, + "b"^#6:string#:2^#7:int64#^#5:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: { + "a"^#3[1,1]#:1^#4[1,6]#^#2[1,4]#, + "b"^#6[1,9]#:2^#7[1,14]#^#5[1,12]# +}^#1[1,0]# + +I: {1:2u, 2:3u} +=====> +P: { + 1^#3:int64#:2u^#4:uint64#^#2:Expr.CreateStruct.Entry#, + 2^#6:int64#:3u^#7:uint64#^#5:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: { + 1^#3[1,1]#:2u^#4[1,3]#^#2[1,2]#, + 2^#6[1,7]#:3u^#7[1,9]#^#5[1,8]# +}^#1[1,0]# + +I: {?a: b} +=====> +P: { + ?a^#3:Expr.Ident#:b^#4:Expr.Ident#^#2:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: { + ?a^#3[1,2]#:b^#4[1,5]#^#2[1,3]# +}^#1[1,0]# + +I: {?'key': value} +=====> +P: { + ?"key"^#3:string#:value^#4:Expr.Ident#^#2:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: { + ?"key"^#3[1,2]#:value^#4[1,9]#^#2[1,7]# +}^#1[1,0]# + +I: foo{ } +=====> +P: foo{}^#1:Expr.CreateStruct# +L: foo{}^#1[1,0]# + +I: foo{ a:b } +=====> +P: foo{ + a:b^#3:Expr.Ident#^#2:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: foo{ + a:b^#3[1,7]#^#2[1,6]# +}^#1[1,0]# + +I: foo{ a:b, c:d } +=====> +P: foo{ + a:b^#3:Expr.Ident#^#2:Expr.CreateStruct.Entry#, + c:d^#5:Expr.Ident#^#4:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: foo{ + a:b^#3[1,7]#^#2[1,6]#, + c:d^#5[1,12]#^#4[1,11]# +}^#1[1,0]# + +I: SomeMessage{foo: 5, bar: "xyz"} +=====> +P: SomeMessage{ + foo:5^#3:int64#^#2:Expr.CreateStruct.Entry#, + bar:"xyz"^#5:string#^#4:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: SomeMessage{ + foo:5^#3[1,17]#^#2[1,15]#, + bar:"xyz"^#5[1,25]#^#4[1,23]# +}^#1[1,0]# + +I: TestAllTypes{single_int32: 1, single_int64: 2} +=====> +P: TestAllTypes{ + single_int32:1^#3:int64#^#2:Expr.CreateStruct.Entry#, + single_int64:2^#5:int64#^#4:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: TestAllTypes{ + single_int32:1^#3[1,27]#^#2[1,25]#, + single_int64:2^#5[1,44]#^#4[1,42]# +}^#1[1,0]# + +I: MyType{foo: 1, bar: 'baz'} +=====> +P: MyType{ + foo:1^#3:int64#^#2:Expr.CreateStruct.Entry#, + bar:"baz"^#5:string#^#4:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: MyType{ + foo:1^#3[1,12]#^#2[1,10]#, + bar:"baz"^#5[1,20]#^#4[1,18]# +}^#1[1,0]# + +I: Message{`in`: true} +=====> +P: Message{ + in:true^#3:bool#^#2:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: Message{ + in:true^#3[1,14]#^#2[1,12]# +}^#1[1,0]# + +I: Msg{?field: value} +=====> +P: Msg{ + ?field:value^#3:Expr.Ident#^#2:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: Msg{ + ?field:value^#3[1,12]#^#2[1,10]# +}^#1[1,0]# + +I: a.b +=====> +P: a^#1:Expr.Ident#.b^#2:Expr.Select# +L: a^#1[1,0]#.b^#2[1,1]# + +I: a.b.c +=====> +P: a^#1:Expr.Ident#.b^#2:Expr.Select#.c^#3:Expr.Select# +L: a^#1[1,0]#.b^#2[1,1]#.c^#3[1,3]# + +I: a.?b +=====> +P: _?._( + a^#1:Expr.Ident#, + "b"^#3:string# +)^#2:Expr.Call# +L: _?._( + a^#1[1,0]#, + "b"^#3[1,3]# +)^#2[1,1]# + +I: a.`b-c` +=====> +P: a^#1:Expr.Ident#.b-c^#2:Expr.Select# +L: a^#1[1,0]#.b-c^#2[1,1]# + +I: a.`b c` +=====> +P: a^#1:Expr.Ident#.b c^#2:Expr.Select# +L: a^#1[1,0]#.b c^#2[1,1]# + +I: a.`b.c` +=====> +P: a^#1:Expr.Ident#.b.c^#2:Expr.Select# +L: a^#1[1,0]#.b.c^#2[1,1]# + +I: a.`in` +=====> +P: a^#1:Expr.Ident#.in^#2:Expr.Select# +L: a^#1[1,0]#.in^#2[1,1]# + +I: a.`/foo` +=====> +P: a^#1:Expr.Ident#./foo^#2:Expr.Select# +L: a^#1[1,0]#./foo^#2[1,1]# + +I: a.`my-var` +=====> +P: a^#1:Expr.Ident#.my-var^#2:Expr.Select# +L: a^#1[1,0]#.my-var^#2[1,1]# + +I: a[b] +=====> +P: _[_]( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _[_]( + a^#1[1,0]#, + b^#3[1,2]# +)^#2[1,1]# + +I: a[0] +=====> +P: _[_]( + a^#1:Expr.Ident#, + 0^#3:int64# +)^#2:Expr.Call# +L: _[_]( + a^#1[1,0]#, + 0^#3[1,2]# +)^#2[1,1]# + +I: a[3] +=====> +P: _[_]( + a^#1:Expr.Ident#, + 3^#3:int64# +)^#2:Expr.Call# +L: _[_]( + a^#1[1,0]#, + 3^#3[1,2]# +)^#2[1,1]# + +I: [1,3,4][0] +=====> +P: _[_]( + [ + 1^#2:int64#, + 3^#3:int64#, + 4^#4:int64# + ]^#1:Expr.CreateList#, + 0^#6:int64# +)^#5:Expr.Call# +L: _[_]( + [ + 1^#2[1,1]#, + 3^#3[1,3]#, + 4^#4[1,5]# + ]^#1[1,0]#, + 0^#6[1,8]# +)^#5[1,7]# + +I: a[?0] +=====> +P: _[?_]( + a^#1:Expr.Ident#, + 0^#3:int64# +)^#2:Expr.Call# +L: _[?_]( + a^#1[1,0]#, + 0^#3[1,3]# +)^#2[1,1]# + +I: a() +=====> +P: a()^#1:Expr.Call# +L: a()^#1[1,1]# + +I: a(b) +=====> +P: a( + b^#2:Expr.Ident# +)^#1:Expr.Call# +L: a( + b^#2[1,2]# +)^#1[1,1]# + +I: a(b, c) +=====> +P: a( + b^#2:Expr.Ident#, + c^#3:Expr.Ident# +)^#1:Expr.Call# +L: a( + b^#2[1,2]#, + c^#3[1,5]# +)^#1[1,1]# + +I: a.b() +=====> +P: a^#1:Expr.Ident#.b()^#2:Expr.Call# +L: a^#1[1,0]#.b()^#2[1,3]# + +I: a.b(c) +=====> +P: a^#1:Expr.Ident#.b( + c^#3:Expr.Ident# +)^#2:Expr.Call# +L: a^#1[1,0]#.b( + c^#3[1,4]# +)^#2[1,3]# + +I: a.b(5) +=====> +P: a^#1:Expr.Ident#.b( + 5^#3:int64# +)^#2:Expr.Call# +L: a^#1[1,0]#.b( + 5^#3[1,4]# +)^#2[1,3]# + +I: a.foo(1, 2) +=====> +P: a^#1:Expr.Ident#.foo( + 1^#3:int64#, + 2^#4:int64# +)^#2:Expr.Call# +L: a^#1[1,0]#.foo( + 1^#3[1,6]#, + 2^#4[1,9]# +)^#2[1,5]# + +I: !a +=====> +P: !_( + a^#2:Expr.Ident# +)^#1:Expr.Call# +L: !_( + a^#2[1,1]# +)^#1[1,0]# + +I: !x +=====> +P: !_( + x^#2:Expr.Ident# +)^#1:Expr.Call# +L: !_( + x^#2[1,1]# +)^#1[1,0]# + +I: ! false +=====> +P: !_( + false^#2:bool# +)^#1:Expr.Call# +L: !_( + false^#2[1,2]# +)^#1[1,0]# + +I: -a +=====> +P: -_( + a^#2:Expr.Ident# +)^#1:Expr.Call# +L: -_( + a^#2[1,1]# +)^#1[1,0]# + +I: x * 2 +=====> +P: _*_( + x^#1:Expr.Ident#, + 2^#3:int64# +)^#2:Expr.Call# +L: _*_( + x^#1[1,0]#, + 2^#3[1,4]# +)^#2[1,2]# + +I: x * 2u +=====> +P: _*_( + x^#1:Expr.Ident#, + 2u^#3:uint64# +)^#2:Expr.Call# +L: _*_( + x^#1[1,0]#, + 2u^#3[1,4]# +)^#2[1,2]# + +I: x * 2.0 +=====> +P: _*_( + x^#1:Expr.Ident#, + 2.0^#3:double# +)^#2:Expr.Call# +L: _*_( + x^#1[1,0]#, + 2.0^#3[1,4]# +)^#2[1,2]# + +I: a * b +=====> +P: _*_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _*_( + a^#1[1,0]#, + b^#3[1,4]# +)^#2[1,2]# + +I: a / b +=====> +P: _/_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _/_( + a^#1[1,0]#, + b^#3[1,4]# +)^#2[1,2]# + +I: a % b +=====> +P: _%_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _%_( + a^#1[1,0]#, + b^#3[1,4]# +)^#2[1,2]# + +I: a + b +=====> +P: _+_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _+_( + a^#1[1,0]#, + b^#3[1,4]# +)^#2[1,2]# + +I: a - b +=====> +P: _-_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _-_( + a^#1[1,0]#, + b^#3[1,4]# +)^#2[1,2]# + +I: 4--4 +=====> +P: _-_( + 4^#1:int64#, + -4^#3:int64# +)^#2:Expr.Call# +L: _-_( + 4^#1[1,0]#, + -4^#3[1,2]# +)^#2[1,1]# + +I: 4--4.1 +=====> +P: _-_( + 4^#1:int64#, + -4.1^#3:double# +)^#2:Expr.Call# +L: _-_( + 4^#1[1,0]#, + -4.1^#3[1,2]# +)^#2[1,1]# + +I: "abc" + "def" +=====> +P: _+_( + "abc"^#1:string#, + "def"^#3:string# +)^#2:Expr.Call# +L: _+_( + "abc"^#1[1,0]#, + "def"^#3[1,8]# +)^#2[1,6]# + +I: b"abc" + B"def" +=====> +P: _+_( + b"abc"^#1:bytes#, + b"def"^#3:bytes# +)^#2:Expr.Call# +L: _+_( + b"abc"^#1[1,0]#, + b"def"^#3[1,9]# +)^#2[1,7]# + +I: [] + [1,2,3,] + [4] +=====> +P: _+_( + _+_( + []^#1:Expr.CreateList#, + [ + 1^#4:int64#, + 2^#5:int64#, + 3^#6:int64# + ]^#3:Expr.CreateList# + )^#2:Expr.Call#, + [ + 4^#9:int64# + ]^#8:Expr.CreateList# +)^#7:Expr.Call# +L: _+_( + _+_( + []^#1[1,0]#, + [ + 1^#4[1,6]#, + 2^#5[1,8]#, + 3^#6[1,10]# + ]^#3[1,5]# + )^#2[1,3]#, + [ + 4^#9[1,17]# + ]^#8[1,16]# +)^#7[1,14]# + +I: 1 + 2 * 3 +=====> +P: _+_( + 1^#1:int64#, + _*_( + 2^#3:int64#, + 3^#5:int64# + )^#4:Expr.Call# +)^#2:Expr.Call# +L: _+_( + 1^#1[1,0]#, + _*_( + 2^#3[1,4]#, + 3^#5[1,8]# + )^#4[1,6]# +)^#2[1,2]# + +I: a == b +=====> +P: _==_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _==_( + a^#1[1,0]#, + b^#3[1,5]# +)^#2[1,2]# + +I: a != b +=====> +P: _!=_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _!=_( + a^#1[1,0]#, + b^#3[1,5]# +)^#2[1,2]# + +I: a < b +=====> +P: _<_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _<_( + a^#1[1,0]#, + b^#3[1,4]# +)^#2[1,2]# + +I: a <= b +=====> +P: _<=_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _<=_( + a^#1[1,0]#, + b^#3[1,5]# +)^#2[1,2]# + +I: a > b +=====> +P: _>_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _>_( + a^#1[1,0]#, + b^#3[1,4]# +)^#2[1,2]# + +I: a >= b +=====> +P: _>=_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _>=_( + a^#1[1,0]#, + b^#3[1,5]# +)^#2[1,2]# + +I: a in b +=====> +P: @in( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: @in( + a^#1[1,0]#, + b^#3[1,5]# +)^#2[1,2]# + +I: "😁" in ["😁", "😑", "😦"] +=====> +P: @in( + "😁"^#1:string#, + [ + "😁"^#4:string#, + "😑"^#5:string#, + "😦"^#6:string# + ]^#3:Expr.CreateList# +)^#2:Expr.Call# +L: @in( + "😁"^#1[1,0]#, + [ + "😁"^#4[1,8]#, + "😑"^#5[1,13]#, + "😦"^#6[1,18]# + ]^#3[1,7]# +)^#2[1,4]# + +I: size(x) == x.size() +=====> +P: _==_( + size( + x^#2:Expr.Ident# + )^#1:Expr.Call#, + x^#4:Expr.Ident#.size()^#5:Expr.Call# +)^#3:Expr.Call# +L: _==_( + size( + x^#2[1,5]# + )^#1[1,4]#, + x^#4[1,11]#.size()^#5[1,17]# +)^#3[1,8]# + +I: x.single_nested_message != null +=====> +P: _!=_( + x^#1:Expr.Ident#.single_nested_message^#2:Expr.Select#, + null^#4:NullValue# +)^#3:Expr.Call# +L: _!=_( + x^#1[1,0]#.single_nested_message^#2[1,1]#, + null^#4[1,27]# +)^#3[1,24]# + +I: a && b +=====> +P: _&&_( + a^#1:Expr.Ident#, + b^#2:Expr.Ident# +)^#3:Expr.Call# +L: _&&_( + a^#1[1,0]#, + b^#2[1,5]# +)^#3[1,2]# + +I: a && b && c +=====> +P: _&&_( + _&&_( + a^#1:Expr.Ident#, + b^#2:Expr.Ident# + )^#3:Expr.Call#, + c^#4:Expr.Ident# +)^#5:Expr.Call# +L: _&&_( + _&&_( + a^#1[1,0]#, + b^#2[1,5]# + )^#3[1,2]#, + c^#4[1,10]# +)^#5[1,7]# + +I: a && b && c && d && e && f && g +=====> +P: _&&_( + _&&_( + _&&_( + a^#1:Expr.Ident#, + b^#2:Expr.Ident# + )^#3:Expr.Call#, + _&&_( + c^#4:Expr.Ident#, + d^#6:Expr.Ident# + )^#7:Expr.Call# + )^#5:Expr.Call#, + _&&_( + _&&_( + e^#8:Expr.Ident#, + f^#10:Expr.Ident# + )^#11:Expr.Call#, + g^#12:Expr.Ident# + )^#13:Expr.Call# +)^#9:Expr.Call# +L: _&&_( + _&&_( + _&&_( + a^#1[1,0]#, + b^#2[1,5]# + )^#3[1,2]#, + _&&_( + c^#4[1,10]#, + d^#6[1,15]# + )^#7[1,12]# + )^#5[1,7]#, + _&&_( + _&&_( + e^#8[1,20]#, + f^#10[1,25]# + )^#11[1,22]#, + g^#12[1,30]# + )^#13[1,27]# +)^#9[1,17]# + +I: a > 5 && a < 10 +=====> +P: _&&_( + _>_( + a^#1:Expr.Ident#, + 5^#3:int64# + )^#2:Expr.Call#, + _<_( + a^#4:Expr.Ident#, + 10^#6:int64# + )^#5:Expr.Call# +)^#7:Expr.Call# +L: _&&_( + _>_( + a^#1[1,0]#, + 5^#3[1,4]# + )^#2[1,2]#, + _<_( + a^#4[1,9]#, + 10^#6[1,13]# + )^#5[1,11]# +)^#7[1,6]# + +I: a || b +=====> +P: _||_( + a^#1:Expr.Ident#, + b^#2:Expr.Ident# +)^#3:Expr.Call# +L: _||_( + a^#1[1,0]#, + b^#2[1,5]# +)^#3[1,2]# + +I: a || b || c || d || e || f +=====> +P: _||_( + _||_( + _||_( + a^#1:Expr.Ident#, + b^#2:Expr.Ident# + )^#3:Expr.Call#, + c^#4:Expr.Ident# + )^#5:Expr.Call#, + _||_( + _||_( + d^#6:Expr.Ident#, + e^#8:Expr.Ident# + )^#9:Expr.Call#, + f^#10:Expr.Ident# + )^#11:Expr.Call# +)^#7:Expr.Call# +L: _||_( + _||_( + _||_( + a^#1[1,0]#, + b^#2[1,5]# + )^#3[1,2]#, + c^#4[1,10]# + )^#5[1,7]#, + _||_( + _||_( + d^#6[1,15]#, + e^#8[1,20]# + )^#9[1,17]#, + f^#10[1,25]# + )^#11[1,22]# +)^#7[1,12]# + +I: a < 5 || a > 10 +=====> +P: _||_( + _<_( + a^#1:Expr.Ident#, + 5^#3:int64# + )^#2:Expr.Call#, + _>_( + a^#4:Expr.Ident#, + 10^#6:int64# + )^#5:Expr.Call# +)^#7:Expr.Call# +L: _||_( + _<_( + a^#1[1,0]#, + 5^#3[1,4]# + )^#2[1,2]#, + _>_( + a^#4[1,9]#, + 10^#6[1,13]# + )^#5[1,11]# +)^#7[1,6]# + +I: a && b && c && d || e && f && g && h +=====> +P: _||_( + _&&_( + _&&_( + a^#1:Expr.Ident#, + b^#2:Expr.Ident# + )^#3:Expr.Call#, + _&&_( + c^#4:Expr.Ident#, + d^#6:Expr.Ident# + )^#7:Expr.Call# + )^#5:Expr.Call#, + _&&_( + _&&_( + e^#8:Expr.Ident#, + f^#9:Expr.Ident# + )^#10:Expr.Call#, + _&&_( + g^#11:Expr.Ident#, + h^#13:Expr.Ident# + )^#14:Expr.Call# + )^#12:Expr.Call# +)^#15:Expr.Call# +L: _||_( + _&&_( + _&&_( + a^#1[1,0]#, + b^#2[1,5]# + )^#3[1,2]#, + _&&_( + c^#4[1,10]#, + d^#6[1,15]# + )^#7[1,12]# + )^#5[1,7]#, + _&&_( + _&&_( + e^#8[1,20]#, + f^#9[1,25]# + )^#10[1,22]#, + _&&_( + g^#11[1,30]#, + h^#13[1,35]# + )^#14[1,32]# + )^#12[1,27]# +)^#15[1,17]# + +I: a?b:c +=====> +P: _?_:_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident#, + c^#4:Expr.Ident# +)^#2:Expr.Call# +L: _?_:_( + a^#1[1,0]#, + b^#3[1,2]#, + c^#4[1,4]# +)^#2[1,1]# + +I: cond ? 1 : 2 +=====> +P: _?_:_( + cond^#1:Expr.Ident#, + 1^#3:int64#, + 2^#4:int64# +)^#2:Expr.Call# +L: _?_:_( + cond^#1[1,0]#, + 1^#3[1,7]#, + 2^#4[1,11]# +)^#2[1,5]# + +I: false && !true || false ? 2 : 3 +=====> +P: _?_:_( + _||_( + _&&_( + false^#1:bool#, + !_( + true^#3:bool# + )^#2:Expr.Call# + )^#4:Expr.Call#, + false^#5:bool# + )^#6:Expr.Call#, + 2^#8:int64#, + 3^#9:int64# +)^#7:Expr.Call# +L: _?_:_( + _||_( + _&&_( + false^#1[1,0]#, + !_( + true^#3[1,10]# + )^#2[1,9]# + )^#4[1,6]#, + false^#5[1,18]# + )^#6[1,15]#, + 2^#8[1,26]#, + 3^#9[1,30]# +)^#7[1,24]# + +I: true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : 1 +=====> + +I: !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-x +=====> + +I: 1 + 2 * 3 - 1 / 2 == 6 % 1 +=====> +P: _==_( + _-_( + _+_( + 1^#1:int64#, + _*_( + 2^#3:int64#, + 3^#5:int64# + )^#4:Expr.Call# + )^#2:Expr.Call#, + _/_( + 1^#7:int64#, + 2^#9:int64# + )^#8:Expr.Call# + )^#6:Expr.Call#, + _%_( + 6^#11:int64#, + 1^#13:int64# + )^#12:Expr.Call# +)^#10:Expr.Call# +L: _==_( + _-_( + _+_( + 1^#1[1,0]#, + _*_( + 2^#3[1,4]#, + 3^#5[1,8]# + )^#4[1,6]# + )^#2[1,2]#, + _/_( + 1^#7[1,12]#, + 2^#9[1,16]# + )^#8[1,14]# + )^#6[1,10]#, + _%_( + 6^#11[1,21]#, + 1^#13[1,25]# + )^#12[1,23]# +)^#10[1,18]# + +I: x["a"].single_int32 == 23 +=====> +P: _==_( + _[_]( + x^#1:Expr.Ident#, + "a"^#3:string# + )^#2:Expr.Call#.single_int32^#4:Expr.Select#, + 23^#6:int64# +)^#5:Expr.Call# +L: _==_( + _[_]( + x^#1[1,0]#, + "a"^#3[1,2]# + )^#2[1,1]#.single_int32^#4[1,6]#, + 23^#6[1,23]# +)^#5[1,20]# + +I: a.?b[?0] && a[?c] +=====> +P: _&&_( + _[?_]( + _?._( + a^#1:Expr.Ident#, + "b"^#3:string# + )^#2:Expr.Call#, + 0^#5:int64# + )^#4:Expr.Call#, + _[?_]( + a^#6:Expr.Ident#, + c^#8:Expr.Ident# + )^#7:Expr.Call# +)^#9:Expr.Call# +L: _&&_( + _[?_]( + _?._( + a^#1[1,0]#, + "b"^#3[1,3]# + )^#2[1,1]#, + 0^#5[1,6]# + )^#4[1,4]#, + _[?_]( + a^#6[1,12]#, + c^#8[1,15]# + )^#7[1,13]# +)^#9[1,9]# + +I: // comment +a +=====> +P: a^#1:Expr.Ident# +L: a^#1[2,0]# + +I: a // comment +=====> +P: a^#1:Expr.Ident# +L: a^#1[1,0]# + +I: a +// comment ++ b +=====> +P: _+_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _+_( + a^#1[1,0]#, + b^#3[3,2]# +)^#2[3,0]# + +I: a / // comment + b +=====> +P: _/_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _/_( + a^#1[1,0]#, + b^#3[2,2]# +)^#2[1,2]# + +I: [ + 1, // comment + 2, +] +=====> +P: [ + 1^#2:int64#, + 2^#3:int64# +]^#1:Expr.CreateList# +L: [ + 1^#2[2,2]#, + 2^#3[3,2]# +]^#1[1,0]# diff --git a/parser/src/test/resources/pratt_parser_errors.baseline b/parser/src/test/resources/pratt_parser_errors.baseline new file mode 100644 index 000000000..e88dd385e --- /dev/null +++ b/parser/src/test/resources/pratt_parser_errors.baseline @@ -0,0 +1,514 @@ +I: *@a | b +=====> +E: ERROR: :1:1: Syntax error: unexpected token + | *@a | b + | ^ +ERROR: :1:2: Syntax error: unexpected character + | *@a | b + | .^ + +I: ((@)) +=====> +E: ERROR: :1:3: Syntax error: unexpected character + | ((@)) + | ..^ + +I: 1 + $ +=====> +E: ERROR: :1:5: Syntax error: unexpected character + | 1 + $ + | ....^ + +I: ó ¢ +»»ó 0  +»»0"""\""\"""\""\"""\""\"""\""\"""\"\"""\""\"""\""\"""\""\"""\"!\"""\""\"""\""\" +=====> +E: ERROR: :1:1: Syntax error: unexpected character + | ó ¢ + | ^ +ERROR: :1:2: Syntax error: unexpected character + | ó ¢ + | .^ + +I: '\udead' == '\ufffd' +=====> +E: ERROR: :1:1: Invalid unicode code point + | '\udead' == '\ufffd' + | ^ + +I: a | b +=====> +E: ERROR: :1:3: Syntax error: unexpected single '|', expected '||' + | a | b + | ..^ + +I: '3# < 10" '& tru ^^ +=====> +E: ERROR: :1:12: Syntax error: unexpected single '&', expected '&&' + | '3# < 10" '& tru ^^ + | ...........^ + +I: 1 + + +=====> +E: ERROR: :1:5: Syntax error: unexpected token + | 1 + + + | ....^ + +I: ? +=====> +E: ERROR: :1:1: Syntax error: unexpected token + | ? + | ^ + +I: a ? b ((?)) +=====> +E: ERROR: :1:9: Syntax error: unexpected token + | a ? b ((?)) + | ........^ +ERROR: :1:12: Syntax error: expected ':' in conditional expression + | a ? b ((?)) + | ...........^ + +I: a ? b @ +=====> +E: ERROR: :1:7: Syntax error: unexpected character + | a ? b @ + | ......^ + +I: -[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1 +»»--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1--1--0--1--1--1 +»»--3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1 +»»--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1 +»»--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1 +»»--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1 +»»--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1--1--0--1--1--1 +»»--3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1 +»»--3-[-1--1--1--1---1-1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 +»»--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 +»»--1--1---1--1-À1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 +»»--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 +»»--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 +»»--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 +»»--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 +»»--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 +»»--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 +»»--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1 +»»--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1 +»»--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1 +»»--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1 +»»--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1 +»»--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1 +»»--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1 +»»--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1 +»»--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1 +»»--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1 +»»--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1 +»»--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1 +»»--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1 +»»--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1 +»»--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1 +»»--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1 +»»--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1 +=====> +E: ERROR: :3:33: Syntax error: unexpected token + | --3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1 + | ................................^ +ERROR: :3:34: Syntax error: expected ']' + | --3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1 + | .................................^ +ERROR: :11:17: Syntax error: unexpected character + | --1--1---1--1-À1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 + | ................^ +ERROR: :34:49: Syntax error: expected ']' + | --1---1--1--1--0--1--1--1--1--0--3--1--1--0--1 + | ................................................^ +ERROR: :34:49: Syntax error: expected ']' + | --1---1--1--1--0--1--1--1--1--0--3--1--1--0--1 + | ................................................^ + +I: as break const continue else for function if import in let loop package namespace return var void while +=====> +E: ERROR: :1:1: reserved identifier: as + | as break const continue else for function if import in let loop package namespace return var void while + | ^ +ERROR: :1:4: reserved identifier: break + | as break const continue else for function if import in let loop package namespace return var void while + | ...^ +ERROR: :1:10: reserved identifier: const + | as break const continue else for function if import in let loop package namespace return var void while + | .........^ +ERROR: :1:16: reserved identifier: continue + | as break const continue else for function if import in let loop package namespace return var void while + | ...............^ +ERROR: :1:25: reserved identifier: else + | as break const continue else for function if import in let loop package namespace return var void while + | ........................^ +ERROR: :1:30: reserved identifier: for + | as break const continue else for function if import in let loop package namespace return var void while + | .............................^ +ERROR: :1:34: reserved identifier: function + | as break const continue else for function if import in let loop package namespace return var void while + | .................................^ +ERROR: :1:43: reserved identifier: if + | as break const continue else for function if import in let loop package namespace return var void while + | ..........................................^ +ERROR: :1:46: reserved identifier: import + | as break const continue else for function if import in let loop package namespace return var void while + | .............................................^ +ERROR: :1:53: reserved identifier: in + | as break const continue else for function if import in let loop package namespace return var void while + | ....................................................^ +ERROR: :1:56: reserved identifier: let + | as break const continue else for function if import in let loop package namespace return var void while + | .......................................................^ +ERROR: :1:60: reserved identifier: loop + | as break const continue else for function if import in let loop package namespace return var void while + | ...........................................................^ +ERROR: :1:65: reserved identifier: package + | as break const continue else for function if import in let loop package namespace return var void while + | ................................................................^ +ERROR: :1:73: reserved identifier: namespace + | as break const continue else for function if import in let loop package namespace return var void while + | ........................................................................^ +ERROR: :1:83: reserved identifier: return + | as break const continue else for function if import in let loop package namespace return var void while + | ..................................................................................^ +ERROR: :1:90: reserved identifier: var + | as break const continue else for function if import in let loop package namespace return var void while + | .........................................................................................^ +ERROR: :1:94: reserved identifier: void + | as break const continue else for function if import in let loop package namespace return var void while + | .............................................................................................^ +ERROR: :1:99: reserved identifier: while + | as break const continue else for function if import in let loop package namespace return var void while + | ..................................................................................................^ + +I: [1, 2, 3].map(var, var * var) +=====> +E: ERROR: :1:15: reserved identifier: var + | [1, 2, 3].map(var, var * var) + | ..............^ +ERROR: :1:20: reserved identifier: var + | [1, 2, 3].map(var, var * var) + | ...................^ +ERROR: :1:26: reserved identifier: var + | [1, 2, 3].map(var, var * var) + | .........................^ + +I: 1 + +=====> +E: ERROR: :1:4: Syntax error: mismatched input '' expecting expression + | 1 + + | ...^ + +I: -- +=====> +E: ERROR: :1:3: Syntax error: mismatched input '' expecting expression + | -- + | ..^ + +I: { +=====> +E: ERROR: :1:2: Syntax error: expected '}' + | { + | .^ + +I: 0x +=====> +E: ERROR: :1:1: Syntax error: integral literal missing digits after hexadecimal separator + | 0x + | ^ + +I: TestAllTypes(){} +=====> +E: ERROR: :1:15: Syntax error: unexpected token after expression + | TestAllTypes(){} + | ..............^ + +I: TestAllTypes{}() +=====> +E: ERROR: :1:15: Syntax error: unexpected token after expression + | TestAllTypes{}() + | ..............^ + +I: 1 + 2 +3 + +=====> +E: ERROR: :2:1: Syntax error: unexpected token after expression + | 3 + + | ^ + +I: {"a": 1}."a" +=====> +E: ERROR: :1:10: Syntax error: expected identifier after '.' + | {"a": 1}."a" + | .........^ + +I: self.true == 1 +=====> +E: ERROR: :1:6: Syntax error: expected identifier after '.' + | self.true == 1 + | .....^ + +I: {a} +=====> +E: ERROR: :1:3: Syntax error: expected ':' in map entry + | {a} + | ..^ + +I: {:a} +=====> +E: ERROR: :1:2: Syntax error: unexpected token + | {:a} + | .^ +ERROR: :1:3: Syntax error: expected ':' in map entry + | {:a} + | ..^ + +I: func{{a}} +=====> +E: ERROR: :1:6: Syntax error: expected struct field name + | func{{a}} + | .....^ +ERROR: :1:9: Syntax error: unexpected token after expression + | func{{a}} + | ........^ + +I: msg{:a} +=====> +E: ERROR: :1:5: Syntax error: expected struct field name + | msg{:a} + | ....^ + +I: ind[a{b}] +=====> +E: ERROR: :1:8: Syntax error: expected ':' in struct field + | ind[a{b}] + | .......^ + +I: x{?. +=====> +E: ERROR: :1:4: Syntax error: expected struct field name + | x{?. + | ...^ +ERROR: :1:5: Syntax error: expected '}' + | x{?. + | ....^ + +I: x{. +=====> +E: ERROR: :1:3: Syntax error: expected struct field name + | x{. + | ..^ +ERROR: :1:4: Syntax error: expected '}' + | x{. + | ...^ + +I: 1.all(2, 3) +=====> +E: ERROR: :1:7: The argument must be a simple name + | 1.all(2, 3) + | ......^ + +I: a.?b && a[?b] +=====> +E: ERROR: :1:2: unsupported syntax '.?' + | a.?b && a[?b] + | .^ +ERROR: :1:10: unsupported syntax '?' + | a.?b && a[?b] + | .........^ + +I: [?a, ?b] +=====> +E: ERROR: :1:2: unsupported syntax '?' + | [?a, ?b] + | .^ +ERROR: :1:6: unsupported syntax '?' + | [?a, ?b] + | .....^ + +I: Msg{?field: value} && {?'key': value} +=====> +E: ERROR: :1:5: unsupported syntax '?' + | Msg{?field: value} && {?'key': value} + | ....^ +ERROR: :1:24: unsupported syntax '?' + | Msg{?field: value} && {?'key': value} + | .......................^ + +I: a.`b-c` +=====> +E: ERROR: :1:3: unsupported syntax '`' + | a.`b-c` + | ..^ + +I: a.`b.c` +=====> +E: ERROR: :1:3: unsupported syntax '`' + | a.`b.c` + | ..^ + +I: a.`in` +=====> +E: ERROR: :1:3: unsupported syntax '`' + | a.`in` + | ..^ + +I: a.`/foo` +=====> +E: ERROR: :1:3: unsupported syntax '`' + | a.`/foo` + | ..^ + +I: Message{`in`: true} +=====> +E: ERROR: :1:9: unsupported syntax '`' + | Message{`in`: true} + | ........^ + +I: `b-c` +=====> +E: ERROR: :1:1: unexpected quoted identifier + | `b-c` + | ^ + +I: `b-c`() +=====> +E: ERROR: :1:1: unexpected quoted identifier + | `b-c`() + | ^ + +I: a.`$b` +=====> +E: ERROR: :1:3: unexpected quoted identifier + | a.`$b` + | ..^ + +I: a.`b.c`() +=====> +E: ERROR: :1:3: unexpected quoted identifier + | a.`b.c`() + | ..^ + +I: [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[ +»»»[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['too many']]]]]]]]]]]]]]]]]]]]]]]]]]]] +»»»]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] +=====> +E: ERROR: :1:33: Expression recursion limit exceeded. limit: 32 + | [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[ + | ................................^ +ERROR: :1:33: Syntax error: expected ']' + | [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[ + | ................................^ + +I: [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['not fine']]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] +=====> +E: ERROR: :1:33: Expression recursion limit exceeded. limit: 32 + | [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['not fine']]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] + | ................................^ +ERROR: :1:33: Syntax error: expected ']' + | [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['not fine']]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] + | ................................^ + +I: a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.A.B.C.D.E.F.G.H +=====> +E: ERROR: :1:62: Expression recursion limit exceeded. limit: 32 + | a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.A.B.C.D.E.F.G.H + | .............................................................^ + +I: a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] +»» [21][22][23][24][25][26][27][28][29][30][31][32][33] +=====> +E: ERROR: :2:48: Expression recursion limit exceeded. limit: 32 + | [21][22][23][24][25][26][27][28][29][30][31][32][33] + | ...............................................^ + +I: 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 +»»+ 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 +»»+ 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 30 +»»+ 31 + 32 + 33 + 34 +=====> +E: ERROR: :4:8: Expression recursion limit exceeded. limit: 32 + | + 31 + 32 + 33 + 34 + | .......^ + +I: a < 1 < 2 < 3 < 4 < 5 < 6 < 7 < 8 < 9 < 10 < 11 +»» < 12 < 13 < 14 < 15 < 16 < 17 < 18 < 19 < 20 < 21 +»»» < 22 < 23 < 24 < 25 < 26 < 27 < 28 < 29 < 30 < 31 +»»» < 32 < 33 +=====> +E: ERROR: :3:51: Expression recursion limit exceeded. limit: 32 + | < 22 < 23 < 24 < 25 < 26 < 27 < 28 < 29 < 30 < 31 + | ..................................................^ + +I: y!=y!=y!=y!=y!=y!=y!=y!=y!=-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y +»»!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y +»»!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y +»»!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y +»»!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y +»»!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y +=====> +E: ERROR: :2:55: Expression recursion limit exceeded. limit: 32 + | !=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y + | ......................................................^ + +I: a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] +=====> +E: ERROR: :11:76: Expression recursion limit exceeded. limit: 32 + | a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != + | ...........................................................................^ + +I: true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : 1 +=====> +E: ERROR: :1:353: Expression recursion limit exceeded. limit: 32 + | true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : 1 + | ................................................................................................................................................................................................................................................................................................................................................................^ + +I: !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x +=====> +E: ERROR: :1:31: Expression recursion limit exceeded. limit: 32 + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x + | ..............................^ + +I: 123456 +=====> +E: ERROR: :-1:0: expression code point size exceeds limit: size: 6, limit 5 + +I: 1 + 2 + 3 +=====> +E: ERROR: :1:5: expression node limit (2) exceeded + | 1 + 2 + 3 + | ....^ + +I: [?, ?, ?] +=====> +E: ERROR: :1:3: Syntax error: unexpected token + | [?, ?, ?] + | ..^ +ERROR: :1:6: Syntax error: unexpected token + | [?, ?, ?] + | .....^ +ERROR: :-1:0: More than 2 parse errors. + +I: [1 2 3 a b c] +=====> +E: ERROR: :1:4: Syntax error: expected ']' + | [1 2 3 a b c] + | ...^ +ERROR: :1:13: Syntax error: unexpected token after expression + | [1 2 3 a b c] + | ............^ \ No newline at end of file diff --git a/parser/src/test/resources/pratt_parser_literals.baseline b/parser/src/test/resources/pratt_parser_literals.baseline new file mode 100644 index 000000000..5a3b7ec77 --- /dev/null +++ b/parser/src/test/resources/pratt_parser_literals.baseline @@ -0,0 +1,470 @@ +I: null +=====> +P: null^#1:NullValue# +L: null^#1[1,0]# + +I: true +=====> +P: true^#1:bool# +L: true^#1[1,0]# + +I: false +=====> +P: false^#1:bool# +L: false^#1[1,0]# + +I: 0 +=====> +P: 0^#1:int64# +L: 0^#1[1,0]# + +I: 42 +=====> +P: 42^#1:int64# +L: 42^#1[1,0]# + +I: 0xF +=====> +P: 15^#1:int64# +L: 15^#1[1,0]# + +I: 0x2A +=====> +P: 42^#1:int64# +L: 42^#1[1,0]# + +I: -1 +=====> +P: -1^#1:int64# +L: -1^#1[1,0]# + +I: -42 +=====> +P: -42^#1:int64# +L: -42^#1[1,0]# + +I: 0xFFFFFFFFFFFFFFFFF +=====> +E: ERROR: :1:1: Syntax error: invalid int literal + | 0xFFFFFFFFFFFFFFFFF + | ^ + +I: 9223372036854775807 +=====> +P: 9223372036854775807^#1:int64# +L: 9223372036854775807^#1[1,0]# + +I: -9223372036854775808 +=====> +P: -9223372036854775808^#1:int64# +L: -9223372036854775808^#1[1,0]# + +I: -(9223372036854775808) +=====> +E: ERROR: :1:3: Syntax error: invalid int literal + | -(9223372036854775808) + | ..^ + +I: 123a +=====> +E: ERROR: :1:1: Syntax error: int literal has unexpected trailing characters + | 123a + | ^ + +I: 0u +=====> +P: 0u^#1:uint64# +L: 0u^#1[1,0]# + +I: 23u +=====> +P: 23u^#1:uint64# +L: 23u^#1[1,0]# + +I: 0xFu +=====> +P: 15u^#1:uint64# +L: 15u^#1[1,0]# + +I: 0xFFFFFFFFFFFFFFFFFu +=====> +E: ERROR: :1:1: Syntax error: invalid uint literal + | 0xFFFFFFFFFFFFFFFFFu + | ^ + +I: 123u_ +=====> +E: ERROR: :1:1: Syntax error: uint literal has unexpected trailing characters + | 123u_ + | ^ + +I: 3.14 +=====> +P: 3.14^#1:double# +L: 3.14^#1[1,0]# + +I: 23.39 +=====> +P: 23.39^#1:double# +L: 23.39^#1[1,0]# + +I: 1. +=====> +E: ERROR: :1:3: Syntax error: expected identifier after '.' + | 1. + | ..^ + +I: 1e+5 +=====> +P: 100000.0^#1:double# +L: 100000.0^#1[1,0]# + +I: 1e-5 +=====> +P: 0.00001^#1:double# +L: 0.00001^#1[1,0]# + +I: 2.5e+10 +=====> +P: 25000000000.0^#1:double# +L: 25000000000.0^#1[1,0]# + +I: 2.5e-10 +=====> +P: 0.0^#1:double# +L: 0.0^#1[1,0]# + +I: 1.99e90000009 +=====> +E: ERROR: :1:1: Syntax error: invalid double literal + | 1.99e90000009 + | ^ + +I: 1e +=====> +E: ERROR: :1:1: Syntax error: floating point literal missing digits after exponent separator + | 1e + | ^ + +I: 1e+ +=====> +E: ERROR: :1:1: Syntax error: floating point literal missing digits after exponent separator + | 1e+ + | ^ + +I: 1e- +=====> +E: ERROR: :1:1: Syntax error: floating point literal missing digits after exponent separator + | 1e- + | ^ + +I: 2.5e +=====> +E: ERROR: :1:1: Syntax error: floating point literal missing digits after exponent separator + | 2.5e + | ^ + +I: 2.5e+ +=====> +E: ERROR: :1:1: Syntax error: floating point literal missing digits after exponent separator + | 2.5e+ + | ^ + +I: 2.5e- +=====> +E: ERROR: :1:1: Syntax error: floating point literal missing digits after exponent separator + | 2.5e- + | ^ + +I: ((1e)) +=====> +E: ERROR: :1:3: Syntax error: floating point literal missing digits after exponent separator + | ((1e)) + | ..^ + +I: 0x123z +=====> +E: ERROR: :1:1: Syntax error: int literal has unexpected trailing characters + | 0x123z + | ^ + +I: 'hello' +=====> +P: "hello"^#1:string# +L: "hello"^#1[1,0]# + +I: "A" +=====> +P: "A"^#1:string# +L: "A"^#1[1,0]# + +I: '''hello +world''' +=====> +P: "hello\nworld"^#1:string# +L: "hello\nworld"^#1[1,0]# + +I: "\u2764" +=====> +P: "❤"^#1:string# +L: "❤"^#1[1,0]# + +I: "❤" +=====> +P: "❤"^#1:string# +L: "❤"^#1[1,0]# + +I: "\"" +=====> +P: "\""^#1:string# +L: "\""^#1[1,0]# + +I: "\xC3\XBF" +=====> +P: "ÿ"^#1:string# +L: "ÿ"^#1[1,0]# + +I: "\303\277" +=====> +P: "ÿ"^#1:string# +L: "ÿ"^#1[1,0]# + +I: "hi\u263A \u263Athere" +=====> +P: "hi☺ ☺there"^#1:string# +L: "hi☺ ☺there"^#1[1,0]# + +I: "\U000003A8\?" +=====> +P: "Ψ?"^#1:string# +L: "Ψ?"^#1[1,0]# + +I: "\a\b\f\n\r\t\v'\"\\\? Legal escapes" +=====> +P: "\a\b\f\n\r\t\v'\"\? Legal escapes"^#1:string# +L: "\a\b\f\n\r\t\v'\"\? Legal escapes"^#1[1,0]# + +I: """hello +world""" +=====> +P: "hello\nworld"^#1:string# +L: "hello\nworld"^#1[1,0]# + +I: r"""hello +world""" +=====> +P: "hello\nworld"^#1:string# +L: "hello\nworld"^#1[1,0]# + +I: """""" +=====> +P: ""^#1:string# +L: ""^#1[1,0]# + +I: '''''' +=====> +P: ""^#1:string# +L: ""^#1[1,0]# + +I: """hello\"""world""" +=====> +P: "hello\"\"\"world"^#1:string# +L: "hello\"\"\"world"^#1[1,0]# + +I: '''hello\'''world''' +=====> +P: "hello'''world"^#1:string# +L: "hello'''world"^#1[1,0]# + +I: "\xFh" +=====> +E: ERROR: :1:1: Invalid hex escape sequence + | "\xFh" + | ^ + +I: "\a\b\f\n\r\t\v\'\"\\\? Illegal escape \>" +=====> +E: ERROR: :1:1: Illegal escape sequence + | "\a\b\f\n\r\t\v\'\"\\\? Illegal escape \>" + | ^ + +I: '😁' in ['😁', '😑', '😦'] +»»»&& in.😁 +=====> +E: ERROR: :2:7: Syntax error: unexpected token + | && in.😁 + | ......^ +ERROR: :2:10: Syntax error: unexpected character + | && in.😁 + | .........^ + +I: """hello +world +=====> +E: ERROR: :1:1: Syntax error: unterminated string literal + | """hello + | ^ + +I: '''hello +world +=====> +E: ERROR: :1:1: Syntax error: unterminated string literal + | '''hello + | ^ + +I: r"""hello +world +=====> +E: ERROR: :1:1: Syntax error: unterminated string literal + | r"""hello + | ^ + +I: "hello +world" +=====> +E: ERROR: :1:1: Syntax error: unterminated string literal + | "hello + | ^ +ERROR: :2:1: Syntax error: unexpected token after expression + | world" + | ^ + +I: 'hello +world' +=====> +E: ERROR: :1:1: Syntax error: unterminated string literal + | 'hello + | ^ +ERROR: :2:1: Syntax error: unexpected token after expression + | world' + | ^ + +I: r"hello +world" +=====> +E: ERROR: :1:1: Syntax error: unterminated string literal + | r"hello + | ^ +ERROR: :2:1: Syntax error: unexpected token after expression + | world" + | ^ + +I: `hello +world` +=====> +E: ERROR: :1:1: Syntax error: unterminated quoted identifier + | `hello + | ^ +ERROR: :2:1: Syntax error: unexpected token after expression + | world` + | ^ + +I: "hello world" +=====> +E: ERROR: :1:1: Syntax error: unterminated string literal + | "hello world" + | ^ +ERROR: :1:8: Syntax error: unexpected token after expression + | "hello world" + | .......^ + +I: 'unterminated +=====> +E: ERROR: :1:1: Syntax error: unterminated string literal + | 'unterminated + | ^ + +I: b'abc' +=====> +P: b"abc"^#1:bytes# +L: b"abc"^#1[1,0]# + +I: b"abc" +=====> +P: b"abc"^#1:bytes# +L: b"abc"^#1[1,0]# + +I: b"""hello +world +=====> +E: ERROR: :1:1: Syntax error: unterminated bytes literal + | b"""hello + | ^ + +I: b"hello +world" +=====> +E: ERROR: :1:1: Syntax error: unterminated bytes literal + | b"hello + | ^ +ERROR: :2:1: Syntax error: unexpected token after expression + | world" + | ^ + +I: rb"hello +world" +=====> +E: ERROR: :1:1: Syntax error: unterminated bytes literal + | rb"hello + | ^ +ERROR: :2:1: Syntax error: unexpected token after expression + | world" + | ^ + +I: br'abc' +=====> +P: b"abc"^#1:bytes# +L: b"abc"^#1[1,0]# + +I: bR'abc' +=====> +P: b"abc"^#1:bytes# +L: b"abc"^#1[1,0]# + +I: Br'abc' +=====> +P: b"abc"^#1:bytes# +L: b"abc"^#1[1,0]# + +I: BR'abc' +=====> +P: b"abc"^#1:bytes# +L: b"abc"^#1[1,0]# + +I: rb'abc' +=====> +P: b"abc"^#1:bytes# +L: b"abc"^#1[1,0]# + +I: rB'abc' +=====> +P: b"abc"^#1:bytes# +L: b"abc"^#1[1,0]# + +I: Rb'abc' +=====> +P: b"abc"^#1:bytes# +L: b"abc"^#1[1,0]# + +I: RB'abc' +=====> +P: b"abc"^#1:bytes# +L: b"abc"^#1[1,0]# + +I: br'a\'b' +=====> +E: ERROR: :1:1: String literal contains unescaped terminating quote ' + | br'a\'b' + | ^ +ERROR: :1:7: Syntax error: unterminated bytes literal + | br'a\'b' + | ......^ + +I: rb'a\'b' +=====> +E: ERROR: :1:1: String literal contains unescaped terminating quote ' + | rb'a\'b' + | ^ +ERROR: :1:7: Syntax error: unterminated bytes literal + | rb'a\'b' + | ......^ \ No newline at end of file diff --git a/parser/src/test/resources/pratt_parser_macros.baseline b/parser/src/test/resources/pratt_parser_macros.baseline new file mode 100644 index 000000000..dabf57e31 --- /dev/null +++ b/parser/src/test/resources/pratt_parser_macros.baseline @@ -0,0 +1,903 @@ +I: has(m.f) +=====> +P: m^#2:Expr.Ident#.f~test-only~^#4:Expr.Select# +L: m^#2[1,4]#.f~test-only~^#4[1,3]# +M: has( + m^#2:Expr.Ident#.f^#3:Expr.Select# +)^#0:Expr.Call# + +I: has(a.b) +=====> +P: a^#2:Expr.Ident#.b~test-only~^#4:Expr.Select# +L: a^#2[1,4]#.b~test-only~^#4[1,3]# +M: has( + a^#2:Expr.Ident#.b^#3:Expr.Select# +)^#0:Expr.Call# + +I: has(m) +=====> +E: ERROR: :1:4: invalid argument to has() macro + | has(m) + | ...^ + +I: m.all(v, f) +=====> +P: __comprehension__( + // Variable + v, + // Target + m^#1:Expr.Ident#, + // Accumulator + @result, + // Init + true^#5:bool#, + // LoopCondition + @not_strictly_false( + @result^#6:Expr.Ident# + )^#7:Expr.Call#, + // LoopStep + _&&_( + @result^#8:Expr.Ident#, + f^#4:Expr.Ident# + )^#9:Expr.Call#, + // Result + @result^#10:Expr.Ident#)^#11:Expr.Comprehension# +L: __comprehension__( + // Variable + v, + // Target + m^#1[1,0]#, + // Accumulator + @result, + // Init + true^#5[1,5]#, + // LoopCondition + @not_strictly_false( + @result^#6[1,5]# + )^#7[1,5]#, + // LoopStep + _&&_( + @result^#8[1,5]#, + f^#4[1,9]# + )^#9[1,5]#, + // Result + @result^#10[1,5]#)^#11[1,5]# +M: m^#1:Expr.Ident#.all( + v^#3:Expr.Ident#, + f^#4:Expr.Ident# +)^#0:Expr.Call# + +I: [1, 2].all(x, x > 0) +=====> +P: __comprehension__( + // Variable + x, + // Target + [ + 1^#2:int64#, + 2^#3:int64# + ]^#1:Expr.CreateList#, + // Accumulator + @result, + // Init + true^#9:bool#, + // LoopCondition + @not_strictly_false( + @result^#10:Expr.Ident# + )^#11:Expr.Call#, + // LoopStep + _&&_( + @result^#12:Expr.Ident#, + _>_( + x^#6:Expr.Ident#, + 0^#8:int64# + )^#7:Expr.Call# + )^#13:Expr.Call#, + // Result + @result^#14:Expr.Ident#)^#15:Expr.Comprehension# +L: __comprehension__( + // Variable + x, + // Target + [ + 1^#2[1,1]#, + 2^#3[1,4]# + ]^#1[1,0]#, + // Accumulator + @result, + // Init + true^#9[1,10]#, + // LoopCondition + @not_strictly_false( + @result^#10[1,10]# + )^#11[1,10]#, + // LoopStep + _&&_( + @result^#12[1,10]#, + _>_( + x^#6[1,14]#, + 0^#8[1,18]# + )^#7[1,16]# + )^#13[1,10]#, + // Result + @result^#14[1,10]#)^#15[1,10]# +M: [ + 1^#2:int64#, + 2^#3:int64# +]^#1:Expr.CreateList#.all( + x^#5:Expr.Ident#, + _>_( + x^#6:Expr.Ident#, + 0^#8:int64# + )^#7:Expr.Call# +)^#0:Expr.Call# + +I: m.exists(v, f) +=====> +P: __comprehension__( + // Variable + v, + // Target + m^#1:Expr.Ident#, + // Accumulator + @result, + // Init + false^#5:bool#, + // LoopCondition + @not_strictly_false( + !_( + @result^#6:Expr.Ident# + )^#7:Expr.Call# + )^#8:Expr.Call#, + // LoopStep + _||_( + @result^#9:Expr.Ident#, + f^#4:Expr.Ident# + )^#10:Expr.Call#, + // Result + @result^#11:Expr.Ident#)^#12:Expr.Comprehension# +L: __comprehension__( + // Variable + v, + // Target + m^#1[1,0]#, + // Accumulator + @result, + // Init + false^#5[1,8]#, + // LoopCondition + @not_strictly_false( + !_( + @result^#6[1,8]# + )^#7[1,8]# + )^#8[1,8]#, + // LoopStep + _||_( + @result^#9[1,8]#, + f^#4[1,12]# + )^#10[1,8]#, + // Result + @result^#11[1,8]#)^#12[1,8]# +M: m^#1:Expr.Ident#.exists( + v^#3:Expr.Ident#, + f^#4:Expr.Ident# +)^#0:Expr.Call# + +I: m.existsOne(v, f) +=====> +P: __comprehension__( + // Variable + v, + // Target + m^#1:Expr.Ident#, + // Accumulator + @result, + // Init + 0^#5:int64#, + // LoopCondition + true^#6:bool#, + // LoopStep + _?_:_( + f^#4:Expr.Ident#, + _+_( + @result^#7:Expr.Ident#, + 1^#8:int64# + )^#9:Expr.Call#, + @result^#10:Expr.Ident# + )^#11:Expr.Call#, + // Result + _==_( + @result^#12:Expr.Ident#, + 1^#13:int64# + )^#14:Expr.Call#)^#15:Expr.Comprehension# +L: __comprehension__( + // Variable + v, + // Target + m^#1[1,0]#, + // Accumulator + @result, + // Init + 0^#5[1,11]#, + // LoopCondition + true^#6[1,11]#, + // LoopStep + _?_:_( + f^#4[1,15]#, + _+_( + @result^#7[1,11]#, + 1^#8[1,11]# + )^#9[1,11]#, + @result^#10[1,11]# + )^#11[1,11]#, + // Result + _==_( + @result^#12[1,11]#, + 1^#13[1,11]# + )^#14[1,11]#)^#15[1,11]# +M: m^#1:Expr.Ident#.existsOne( + v^#3:Expr.Ident#, + f^#4:Expr.Ident# +)^#0:Expr.Call# + +I: [].existsOne(__result__, __result__) +=====> +E: ERROR: :1:14: The iteration variable __result__ overwrites accumulator variable + | [].existsOne(__result__, __result__) + | .............^ + +I: m.map(v, f) +=====> +P: __comprehension__( + // Variable + v, + // Target + m^#1:Expr.Ident#, + // Accumulator + @result, + // Init + []^#5:Expr.CreateList#, + // LoopCondition + true^#6:bool#, + // LoopStep + _+_( + @result^#7:Expr.Ident#, + [ + f^#4:Expr.Ident# + ]^#8:Expr.CreateList# + )^#9:Expr.Call#, + // Result + @result^#10:Expr.Ident#)^#11:Expr.Comprehension# +L: __comprehension__( + // Variable + v, + // Target + m^#1[1,0]#, + // Accumulator + @result, + // Init + []^#5[1,5]#, + // LoopCondition + true^#6[1,5]#, + // LoopStep + _+_( + @result^#7[1,5]#, + [ + f^#4[1,9]# + ]^#8[1,5]# + )^#9[1,5]#, + // Result + @result^#10[1,5]#)^#11[1,5]# +M: m^#1:Expr.Ident#.map( + v^#3:Expr.Ident#, + f^#4:Expr.Ident# +)^#0:Expr.Call# + +I: m.map(v, p, f) +=====> +P: __comprehension__( + // Variable + v, + // Target + m^#1:Expr.Ident#, + // Accumulator + @result, + // Init + []^#6:Expr.CreateList#, + // LoopCondition + true^#7:bool#, + // LoopStep + _?_:_( + p^#4:Expr.Ident#, + _+_( + @result^#8:Expr.Ident#, + [ + f^#5:Expr.Ident# + ]^#9:Expr.CreateList# + )^#10:Expr.Call#, + @result^#11:Expr.Ident# + )^#12:Expr.Call#, + // Result + @result^#13:Expr.Ident#)^#14:Expr.Comprehension# +L: __comprehension__( + // Variable + v, + // Target + m^#1[1,0]#, + // Accumulator + @result, + // Init + []^#6[1,5]#, + // LoopCondition + true^#7[1,5]#, + // LoopStep + _?_:_( + p^#4[1,9]#, + _+_( + @result^#8[1,5]#, + [ + f^#5[1,12]# + ]^#9[1,5]# + )^#10[1,5]#, + @result^#11[1,5]# + )^#12[1,5]#, + // Result + @result^#13[1,5]#)^#14[1,5]# +M: m^#1:Expr.Ident#.map( + v^#3:Expr.Ident#, + p^#4:Expr.Ident#, + f^#5:Expr.Ident# +)^#0:Expr.Call# + +I: m.map(__result__, __result__) +=====> +E: ERROR: :1:7: The iteration variable __result__ overwrites accumulator variable + | m.map(__result__, __result__) + | ......^ + +I: m.filter(v, p) +=====> +P: __comprehension__( + // Variable + v, + // Target + m^#1:Expr.Ident#, + // Accumulator + @result, + // Init + []^#5:Expr.CreateList#, + // LoopCondition + true^#6:bool#, + // LoopStep + _?_:_( + p^#4:Expr.Ident#, + _+_( + @result^#7:Expr.Ident#, + [ + v^#3:Expr.Ident# + ]^#8:Expr.CreateList# + )^#9:Expr.Call#, + @result^#10:Expr.Ident# + )^#11:Expr.Call#, + // Result + @result^#12:Expr.Ident#)^#13:Expr.Comprehension# +L: __comprehension__( + // Variable + v, + // Target + m^#1[1,0]#, + // Accumulator + @result, + // Init + []^#5[1,8]#, + // LoopCondition + true^#6[1,8]#, + // LoopStep + _?_:_( + p^#4[1,12]#, + _+_( + @result^#7[1,8]#, + [ + v^#3[1,9]# + ]^#8[1,8]# + )^#9[1,8]#, + @result^#10[1,8]# + )^#11[1,8]#, + // Result + @result^#12[1,8]#)^#13[1,8]# +M: m^#1:Expr.Ident#.filter( + v^#3:Expr.Ident#, + p^#4:Expr.Ident# +)^#0:Expr.Call# + +I: m.filter(__result__, false) +=====> +E: ERROR: :1:10: The iteration variable __result__ overwrites accumulator variable + | m.filter(__result__, false) + | .........^ + +I: m.filter(a.b, false) +=====> +E: ERROR: :1:11: The argument must be a simple name + | m.filter(a.b, false) + | ..........^ + +I: x.filter(y, y.filter(z, z > 0)) +=====> +P: __comprehension__( + // Variable + y, + // Target + x^#1:Expr.Ident#, + // Accumulator + @result, + // Init + []^#19:Expr.CreateList#, + // LoopCondition + true^#20:bool#, + // LoopStep + _?_:_( + __comprehension__( + // Variable + z, + // Target + y^#4:Expr.Ident#, + // Accumulator + @result, + // Init + []^#10:Expr.CreateList#, + // LoopCondition + true^#11:bool#, + // LoopStep + _?_:_( + _>_( + z^#7:Expr.Ident#, + 0^#9:int64# + )^#8:Expr.Call#, + _+_( + @result^#12:Expr.Ident#, + [ + z^#6:Expr.Ident# + ]^#13:Expr.CreateList# + )^#14:Expr.Call#, + @result^#15:Expr.Ident# + )^#16:Expr.Call#, + // Result + @result^#17:Expr.Ident#)^#18:Expr.Comprehension#, + _+_( + @result^#21:Expr.Ident#, + [ + y^#3:Expr.Ident# + ]^#22:Expr.CreateList# + )^#23:Expr.Call#, + @result^#24:Expr.Ident# + )^#25:Expr.Call#, + // Result + @result^#26:Expr.Ident#)^#27:Expr.Comprehension# +L: __comprehension__( + // Variable + y, + // Target + x^#1[1,0]#, + // Accumulator + @result, + // Init + []^#19[1,8]#, + // LoopCondition + true^#20[1,8]#, + // LoopStep + _?_:_( + __comprehension__( + // Variable + z, + // Target + y^#4[1,12]#, + // Accumulator + @result, + // Init + []^#10[1,20]#, + // LoopCondition + true^#11[1,20]#, + // LoopStep + _?_:_( + _>_( + z^#7[1,24]#, + 0^#9[1,28]# + )^#8[1,26]#, + _+_( + @result^#12[1,20]#, + [ + z^#6[1,21]# + ]^#13[1,20]# + )^#14[1,20]#, + @result^#15[1,20]# + )^#16[1,20]#, + // Result + @result^#17[1,20]#)^#18[1,20]#, + _+_( + @result^#21[1,8]#, + [ + y^#3[1,9]# + ]^#22[1,8]# + )^#23[1,8]#, + @result^#24[1,8]# + )^#25[1,8]#, + // Result + @result^#26[1,8]#)^#27[1,8]# +M: x^#1:Expr.Ident#.filter( + y^#3:Expr.Ident#, + ^#18:filter# +)^#0:Expr.Call#, +y^#4:Expr.Ident#.filter( + z^#6:Expr.Ident#, + _>_( + z^#7:Expr.Ident#, + 0^#9:int64# + )^#8:Expr.Call# +)^#0:Expr.Call# + +I: has(a.b).filter(c, c) +=====> +P: __comprehension__( + // Variable + c, + // Target + a^#2:Expr.Ident#.b~test-only~^#4:Expr.Select#, + // Accumulator + @result, + // Init + []^#8:Expr.CreateList#, + // LoopCondition + true^#9:bool#, + // LoopStep + _?_:_( + c^#7:Expr.Ident#, + _+_( + @result^#10:Expr.Ident#, + [ + c^#6:Expr.Ident# + ]^#11:Expr.CreateList# + )^#12:Expr.Call#, + @result^#13:Expr.Ident# + )^#14:Expr.Call#, + // Result + @result^#15:Expr.Ident#)^#16:Expr.Comprehension# +L: __comprehension__( + // Variable + c, + // Target + a^#2[1,4]#.b~test-only~^#4[1,3]#, + // Accumulator + @result, + // Init + []^#8[1,15]#, + // LoopCondition + true^#9[1,15]#, + // LoopStep + _?_:_( + c^#7[1,19]#, + _+_( + @result^#10[1,15]#, + [ + c^#6[1,16]# + ]^#11[1,15]# + )^#12[1,15]#, + @result^#13[1,15]# + )^#14[1,15]#, + // Result + @result^#15[1,15]#)^#16[1,15]# +M: ^#4:has#.filter( + c^#6:Expr.Ident#, + c^#7:Expr.Ident# +)^#0:Expr.Call#, +has( + a^#2:Expr.Ident#.b^#3:Expr.Select# +)^#0:Expr.Call# + +I: x.filter(y, y.exists(z, has(z.a)) && y.exists(z, has(z.b))) +=====> +P: __comprehension__( + // Variable + y, + // Target + x^#1:Expr.Ident#, + // Accumulator + @result, + // Init + []^#35:Expr.CreateList#, + // LoopCondition + true^#36:bool#, + // LoopStep + _?_:_( + _&&_( + __comprehension__( + // Variable + z, + // Target + y^#4:Expr.Ident#, + // Accumulator + @result, + // Init + false^#11:bool#, + // LoopCondition + @not_strictly_false( + !_( + @result^#12:Expr.Ident# + )^#13:Expr.Call# + )^#14:Expr.Call#, + // LoopStep + _||_( + @result^#15:Expr.Ident#, + z^#8:Expr.Ident#.a~test-only~^#10:Expr.Select# + )^#16:Expr.Call#, + // Result + @result^#17:Expr.Ident#)^#18:Expr.Comprehension#, + __comprehension__( + // Variable + z, + // Target + y^#19:Expr.Ident#, + // Accumulator + @result, + // Init + false^#26:bool#, + // LoopCondition + @not_strictly_false( + !_( + @result^#27:Expr.Ident# + )^#28:Expr.Call# + )^#29:Expr.Call#, + // LoopStep + _||_( + @result^#30:Expr.Ident#, + z^#23:Expr.Ident#.b~test-only~^#25:Expr.Select# + )^#31:Expr.Call#, + // Result + @result^#32:Expr.Ident#)^#33:Expr.Comprehension# + )^#34:Expr.Call#, + _+_( + @result^#37:Expr.Ident#, + [ + y^#3:Expr.Ident# + ]^#38:Expr.CreateList# + )^#39:Expr.Call#, + @result^#40:Expr.Ident# + )^#41:Expr.Call#, + // Result + @result^#42:Expr.Ident#)^#43:Expr.Comprehension# +L: __comprehension__( + // Variable + y, + // Target + x^#1[1,0]#, + // Accumulator + @result, + // Init + []^#35[1,8]#, + // LoopCondition + true^#36[1,8]#, + // LoopStep + _?_:_( + _&&_( + __comprehension__( + // Variable + z, + // Target + y^#4[1,12]#, + // Accumulator + @result, + // Init + false^#11[1,20]#, + // LoopCondition + @not_strictly_false( + !_( + @result^#12[1,20]# + )^#13[1,20]# + )^#14[1,20]#, + // LoopStep + _||_( + @result^#15[1,20]#, + z^#8[1,28]#.a~test-only~^#10[1,27]# + )^#16[1,20]#, + // Result + @result^#17[1,20]#)^#18[1,20]#, + __comprehension__( + // Variable + z, + // Target + y^#19[1,37]#, + // Accumulator + @result, + // Init + false^#26[1,45]#, + // LoopCondition + @not_strictly_false( + !_( + @result^#27[1,45]# + )^#28[1,45]# + )^#29[1,45]#, + // LoopStep + _||_( + @result^#30[1,45]#, + z^#23[1,53]#.b~test-only~^#25[1,52]# + )^#31[1,45]#, + // Result + @result^#32[1,45]#)^#33[1,45]# + )^#34[1,34]#, + _+_( + @result^#37[1,8]#, + [ + y^#3[1,9]# + ]^#38[1,8]# + )^#39[1,8]#, + @result^#40[1,8]# + )^#41[1,8]#, + // Result + @result^#42[1,8]#)^#43[1,8]# +M: x^#1:Expr.Ident#.filter( + y^#3:Expr.Ident#, + _&&_( + ^#18:exists#, + ^#33:exists# + )^#34:Expr.Call# +)^#0:Expr.Call#, +y^#19:Expr.Ident#.exists( + z^#21:Expr.Ident#, + ^#25:has# +)^#0:Expr.Call#, +has( + z^#23:Expr.Ident#.b^#24:Expr.Select# +)^#0:Expr.Call#, +y^#4:Expr.Ident#.exists( + z^#6:Expr.Ident#, + ^#10:has# +)^#0:Expr.Call#, +has( + z^#8:Expr.Ident#.a^#9:Expr.Select# +)^#0:Expr.Call# + +I: (has(a.b) || has(c.d)).string() +=====> +P: _||_( + a^#2:Expr.Ident#.b~test-only~^#4:Expr.Select#, + c^#6:Expr.Ident#.d~test-only~^#8:Expr.Select# +)^#9:Expr.Call#.string()^#10:Expr.Call# +L: _||_( + a^#2[1,5]#.b~test-only~^#4[1,4]#, + c^#6[1,17]#.d~test-only~^#8[1,16]# +)^#9[1,10]#.string()^#10[1,29]# +M: has( + c^#6:Expr.Ident#.d^#7:Expr.Select# +)^#0:Expr.Call#, +has( + a^#2:Expr.Ident#.b^#3:Expr.Select# +)^#0:Expr.Call# + +I: has(a.b).asList().exists(c, c) +=====> +P: __comprehension__( + // Variable + c, + // Target + a^#2:Expr.Ident#.b~test-only~^#4:Expr.Select#.asList()^#5:Expr.Call#, + // Accumulator + @result, + // Init + false^#9:bool#, + // LoopCondition + @not_strictly_false( + !_( + @result^#10:Expr.Ident# + )^#11:Expr.Call# + )^#12:Expr.Call#, + // LoopStep + _||_( + @result^#13:Expr.Ident#, + c^#8:Expr.Ident# + )^#14:Expr.Call#, + // Result + @result^#15:Expr.Ident#)^#16:Expr.Comprehension# +L: __comprehension__( + // Variable + c, + // Target + a^#2[1,4]#.b~test-only~^#4[1,3]#.asList()^#5[1,15]#, + // Accumulator + @result, + // Init + false^#9[1,24]#, + // LoopCondition + @not_strictly_false( + !_( + @result^#10[1,24]# + )^#11[1,24]# + )^#12[1,24]#, + // LoopStep + _||_( + @result^#13[1,24]#, + c^#8[1,28]# + )^#14[1,24]#, + // Result + @result^#15[1,24]#)^#16[1,24]# +M: ^#4:has#.asList()^#5:Expr.Call#.exists( + c^#7:Expr.Ident#, + c^#8:Expr.Ident# +)^#0:Expr.Call#, +has( + a^#2:Expr.Ident#.b^#3:Expr.Select# +)^#0:Expr.Call# + +I: [has(a.b), has(c.d)].exists(e, e) +=====> +P: __comprehension__( + // Variable + e, + // Target + [ + a^#3:Expr.Ident#.b~test-only~^#5:Expr.Select#, + c^#7:Expr.Ident#.d~test-only~^#9:Expr.Select# + ]^#1:Expr.CreateList#, + // Accumulator + @result, + // Init + false^#13:bool#, + // LoopCondition + @not_strictly_false( + !_( + @result^#14:Expr.Ident# + )^#15:Expr.Call# + )^#16:Expr.Call#, + // LoopStep + _||_( + @result^#17:Expr.Ident#, + e^#12:Expr.Ident# + )^#18:Expr.Call#, + // Result + @result^#19:Expr.Ident#)^#20:Expr.Comprehension# +L: __comprehension__( + // Variable + e, + // Target + [ + a^#3[1,5]#.b~test-only~^#5[1,4]#, + c^#7[1,15]#.d~test-only~^#9[1,14]# + ]^#1[1,0]#, + // Accumulator + @result, + // Init + false^#13[1,27]#, + // LoopCondition + @not_strictly_false( + !_( + @result^#14[1,27]# + )^#15[1,27]# + )^#16[1,27]#, + // LoopStep + _||_( + @result^#17[1,27]#, + e^#12[1,31]# + )^#18[1,27]#, + // Result + @result^#19[1,27]#)^#20[1,27]# +M: [ + a^#3:Expr.Ident#.b~test-only~^#5:has#, + c^#7:Expr.Ident#.d~test-only~^#9:has# +]^#1:Expr.CreateList#.exists( + e^#11:Expr.Ident#, + e^#12:Expr.Ident# +)^#0:Expr.Call#, +has( + c^#7:Expr.Ident#.d^#8:Expr.Select# +)^#0:Expr.Call#, +has( + a^#3:Expr.Ident#.b^#4:Expr.Select# +)^#0:Expr.Call# + +I: noop_macro(123) +=====> +P: noop_macro( + 123^#2:int64# +)^#1:Expr.Call# +L: noop_macro( + 123^#2[1,11]# +)^#1[1,10]# \ No newline at end of file diff --git a/testing/src/main/java/dev/cel/testing/BUILD.bazel b/testing/src/main/java/dev/cel/testing/BUILD.bazel index 69765b549..ec8a9841b 100644 --- a/testing/src/main/java/dev/cel/testing/BUILD.bazel +++ b/testing/src/main/java/dev/cel/testing/BUILD.bazel @@ -33,10 +33,14 @@ java_library( srcs = [ "CelAdorner.java", "CelDebug.java", + "CelExprKindAndIdAdorner.java", + "CelLocationAdorner.java", ], deps = [ + "//common:source_location", "@cel_spec//proto/cel/expr:syntax_java_proto", "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", ], ) diff --git a/testing/src/main/java/dev/cel/testing/CelExprKindAndIdAdorner.java b/testing/src/main/java/dev/cel/testing/CelExprKindAndIdAdorner.java new file mode 100644 index 000000000..1aaf54439 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/CelExprKindAndIdAdorner.java @@ -0,0 +1,140 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.testing; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Collections.reverseOrder; +import static java.util.Map.Entry.comparingByKey; +import static java.util.stream.Collectors.joining; + +import dev.cel.expr.Constant; +import dev.cel.expr.Expr; +import dev.cel.expr.Expr.CreateStruct.EntryOrBuilder; +import dev.cel.expr.ExprOrBuilder; +import dev.cel.expr.SourceInfo; +import com.google.common.base.Ascii; +import com.google.common.base.Joiner; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.EnumDescriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Descriptors.OneofDescriptor; +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * An implementation of {@link CelAdorner} that decorates expressions with their ID and expression + * kind (or macro call name if applicable). + */ +public final class CelExprKindAndIdAdorner implements CelAdorner { + + private static final Joiner JOINER = Joiner.on('.'); + + private final SourceInfo sourceInfo; + + public CelExprKindAndIdAdorner() { + this(SourceInfo.getDefaultInstance()); + } + + public CelExprKindAndIdAdorner(SourceInfo sourceInfo) { + this.sourceInfo = checkNotNull(sourceInfo); + } + + public static CelExprKindAndIdAdorner newInstance() { + return new CelExprKindAndIdAdorner(); + } + + public static CelExprKindAndIdAdorner newInstance(SourceInfo sourceInfo) { + return new CelExprKindAndIdAdorner(sourceInfo); + } + + /** + * Formats the macro calls from {@link SourceInfo} to an adorned debug string, sorted in + * ascending order of expression ID. + */ + public static String convertMacroCallsToString(SourceInfo sourceInfo) { + CelExprKindAndIdAdorner macroCallsAdorner = new CelExprKindAndIdAdorner(sourceInfo); + // Sort in ascending order so that nested macro calls are always in the same order for tests + // output debug string. Ascending order keeps the macro calls map in order from outermost/first + // macro to the innermost/last macro for readability. + return sourceInfo.getMacroCallsMap().entrySet().stream() + .sorted(reverseOrder(comparingByKey())) + .map((entry) -> CelDebug.toAdornedDebugString(entry.getValue(), macroCallsAdorner)) + .collect(joining(",\n")); + } + + @Override + public String adorn(ExprOrBuilder expr) { + if (this.sourceInfo.containsMacroCalls(expr.getId())) { + return String.format( + "^#%d:%s#", + expr.getId(), + this.sourceInfo.getMacroCallsOrThrow(expr.getId()).getCallExpr().getFunction()); + } + + if (expr.hasConstExpr()) { + Constant constExpr = expr.getConstExpr(); + Descriptor descriptor = Constant.getDescriptor(); + OneofDescriptor oneof = findOneofByName(descriptor, "constant_kind"); + FieldDescriptor field = constExpr.getOneofFieldDescriptor(oneof); + if (field.getType() == FieldDescriptor.Type.ENUM) { + return String.format("^#%d:%s#", expr.getId(), getContainedName(field.getEnumType())); + } else { + return String.format( + "^#%d:%s#", expr.getId(), Ascii.toLowerCase(field.getType().toString())); + } + } + Descriptor descriptor = Expr.getDescriptor(); + OneofDescriptor oneof = findOneofByName(descriptor, "expr_kind"); + FieldDescriptor field = expr.getOneofFieldDescriptor(oneof); + return String.format("^#%d:%s#", expr.getId(), getContainedName(field.getMessageType())); + } + + @Override + public String adorn(EntryOrBuilder entry) { + return String.format("^#%d:Expr.CreateStruct.Entry#", entry.getId()); + } + + static OneofDescriptor findOneofByName(Descriptor descriptor, String name) { + for (OneofDescriptor oneof : descriptor.getOneofs()) { + if (oneof.getName().equals(name)) { + return oneof; + } + } + throw new IllegalArgumentException( + String.format("Oneof '%s' not found in descriptor '%s'", name, descriptor.getName())); + } + + static String getContainedName(Descriptor descriptor) { + Deque parts = new ArrayDeque<>(); + parts.addFirst(descriptor.getName()); + Descriptor containing = descriptor.getContainingType(); + while (containing != null) { + parts.addFirst(containing.getName()); + containing = containing.getContainingType(); + } + return JOINER.join(parts); + } + + static String getContainedName(EnumDescriptor descriptor) { + Deque parts = new ArrayDeque<>(); + parts.addFirst(descriptor.getName()); + Descriptor containing = descriptor.getContainingType(); + while (containing != null) { + parts.addFirst(containing.getName()); + containing = containing.getContainingType(); + } + return JOINER.join(parts); + } +} diff --git a/testing/src/main/java/dev/cel/testing/CelLocationAdorner.java b/testing/src/main/java/dev/cel/testing/CelLocationAdorner.java new file mode 100644 index 000000000..a32742760 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/CelLocationAdorner.java @@ -0,0 +1,80 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.testing; + +import static com.google.common.base.Preconditions.checkNotNull; + +import dev.cel.expr.Expr.CreateStruct.EntryOrBuilder; +import dev.cel.expr.ExprOrBuilder; +import dev.cel.expr.SourceInfo; +import dev.cel.common.CelSourceLocation; +import java.util.Map; +import java.util.Optional; + +/** + * An implementation of {@link CelAdorner} that decorates expressions with their source location + * (line and column numbers). + */ +public final class CelLocationAdorner implements CelAdorner { + + private final SourceInfo sourceInfo; + + public CelLocationAdorner(SourceInfo sourceInfo) { + this.sourceInfo = checkNotNull(sourceInfo); + } + + public static CelLocationAdorner newInstance(SourceInfo sourceInfo) { + return new CelLocationAdorner(sourceInfo); + } + + @Override + public String adorn(ExprOrBuilder expr) { + return adorn(expr.getId()); + } + + @Override + public String adorn(EntryOrBuilder entry) { + return adorn(entry.getId()); + } + + private String adorn(long exprId) { + return getLocation(exprId) + .map( + location -> + String.format( + "^#%d[%d,%d]#", exprId, location.getLine(), location.getColumn())) + .orElseGet(() -> String.format("^#%d[NO_POS]#", exprId)); + } + + public Optional getLocation(long exprId) { + Map positions = sourceInfo.getPositionsMap(); + Integer position = positions.get(exprId); + if (position == null) { + return Optional.empty(); + } + int line = 1; + for (int index = 0; index < sourceInfo.getLineOffsetsCount(); index++) { + if (sourceInfo.getLineOffsets(index) > position) { + break; + } + line++; + } + int column = position; + if (line > 1) { + column = position - sourceInfo.getLineOffsets(line - 2); + } + return Optional.of(CelSourceLocation.of(line, column)); + } +} diff --git a/testing/src/test/java/dev/cel/testing/BUILD.bazel b/testing/src/test/java/dev/cel/testing/BUILD.bazel index 8fe831bb2..3d79c76e7 100644 --- a/testing/src/test/java/dev/cel/testing/BUILD.bazel +++ b/testing/src/test/java/dev/cel/testing/BUILD.bazel @@ -11,8 +11,12 @@ java_library( srcs = glob(["*.java"]), deps = [ "//:java_truth", + "//common:source_location", + "//testing:adorner", "//testing:line_differ", + "@cel_spec//proto/cel/expr:syntax_java_proto", "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", "@maven//:junit_junit", ], ) diff --git a/testing/src/test/java/dev/cel/testing/CelExprKindAndIdAdornerTest.java b/testing/src/test/java/dev/cel/testing/CelExprKindAndIdAdornerTest.java new file mode 100644 index 000000000..fab89b23a --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/CelExprKindAndIdAdornerTest.java @@ -0,0 +1,124 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.testing; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import dev.cel.expr.Constant; +import dev.cel.expr.Expr; +import dev.cel.expr.SourceInfo; +import com.google.protobuf.Descriptors; +import com.google.protobuf.NullValue; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class CelExprKindAndIdAdornerTest { + + @Test + public void findOneofByName_notFound_throwsException() { + Descriptors.Descriptor descriptor = Constant.getDescriptor(); + assertThrows( + IllegalArgumentException.class, + () -> CelExprKindAndIdAdorner.findOneofByName(descriptor, "unknown_oneof")); + } + + @Test + public void getContainedName_nestedEnum_returnsContainedName() { + assertThat( + CelExprKindAndIdAdorner.getContainedName( + SourceInfo.Extension.Component.getDescriptor())) + .isEqualTo("SourceInfo.Extension.Component"); + } + + @Test + public void adorn_constantNull() { + CelExprKindAndIdAdorner adorner = new CelExprKindAndIdAdorner(); + Expr expr = + Expr.newBuilder() + .setId(1L) + .setConstExpr(Constant.newBuilder().setNullValue(NullValue.NULL_VALUE)) + .build(); + + assertThat(adorner.adorn(expr)).isEqualTo("^#1:NullValue#"); + } + + @Test + public void adorn_constantPrimitive() { + CelExprKindAndIdAdorner adorner = new CelExprKindAndIdAdorner(); + Expr expr = + Expr.newBuilder() + .setId(2L) + .setConstExpr(Constant.newBuilder().setInt64Value(42L)) + .build(); + + assertThat(adorner.adorn(expr)).isEqualTo("^#2:int64#"); + } + + @Test + public void adorn_exprKind() { + CelExprKindAndIdAdorner adorner = new CelExprKindAndIdAdorner(); + Expr expr = + Expr.newBuilder() + .setId(3L) + .setIdentExpr(Expr.Ident.newBuilder().setName("foo")) + .build(); + + assertThat(adorner.adorn(expr)).isEqualTo("^#3:Expr.Ident#"); + } + + @Test + public void adorn_structEntry() { + CelExprKindAndIdAdorner adorner = new CelExprKindAndIdAdorner(); + Expr.CreateStruct.Entry entry = + Expr.CreateStruct.Entry.newBuilder().setId(4L).build(); + + assertThat(adorner.adorn(entry)).isEqualTo("^#4:Expr.CreateStruct.Entry#"); + } + + @Test + public void adorn_macroCall() { + SourceInfo sourceInfo = + SourceInfo.newBuilder() + .putMacroCalls( + 5L, + Expr.newBuilder() + .setCallExpr(Expr.Call.newBuilder().setFunction("has")) + .build()) + .build(); + CelExprKindAndIdAdorner adorner = new CelExprKindAndIdAdorner(sourceInfo); + Expr expr = Expr.newBuilder().setId(5L).build(); + + assertThat(adorner.adorn(expr)).isEqualTo("^#5:has#"); + } + + @Test + public void convertMacroCallsToString() { + SourceInfo sourceInfo = + SourceInfo.newBuilder() + .putMacroCalls( + 1L, + Expr.newBuilder() + .setId(1L) + .setCallExpr(Expr.Call.newBuilder().setFunction("has")) + .build()) + .build(); + + assertThat(CelExprKindAndIdAdorner.convertMacroCallsToString(sourceInfo)) + .contains("^#1:has#"); + } +} diff --git a/testing/src/test/java/dev/cel/testing/CelLocationAdornerTest.java b/testing/src/test/java/dev/cel/testing/CelLocationAdornerTest.java new file mode 100644 index 000000000..203324068 --- /dev/null +++ b/testing/src/test/java/dev/cel/testing/CelLocationAdornerTest.java @@ -0,0 +1,72 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.testing; + +import static com.google.common.truth.Truth.assertThat; + +import dev.cel.expr.Expr; +import dev.cel.expr.SourceInfo; +import dev.cel.common.CelSourceLocation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class CelLocationAdornerTest { + + @Test + public void getLocation_missingPosition_returnsEmpty() { + SourceInfo sourceInfo = SourceInfo.getDefaultInstance(); + CelLocationAdorner adorner = new CelLocationAdorner(sourceInfo); + + assertThat(adorner.getLocation(1L)).isEmpty(); + assertThat(adorner.adorn(Expr.newBuilder().setId(1L).build())).isEqualTo("^#1[NO_POS]#"); + } + + @Test + public void getLocation_singleLineExpression() { + SourceInfo sourceInfo = + SourceInfo.newBuilder().putPositions(1L, 0).putPositions(2L, 4).build(); + CelLocationAdorner adorner = new CelLocationAdorner(sourceInfo); + + assertThat(adorner.getLocation(1L)).hasValue(CelSourceLocation.of(1, 0)); + assertThat(adorner.getLocation(2L)).hasValue(CelSourceLocation.of(1, 4)); + assertThat(adorner.adorn(Expr.newBuilder().setId(1L).build())).isEqualTo("^#1[1,0]#"); + assertThat(adorner.adorn(Expr.newBuilder().setId(2L).build())).isEqualTo("^#2[1,4]#"); + } + + @Test + public void getLocation_multiLineExpression() { + // Expression: + // Line 1: "foo\n" -> line offset 4 (first character after newline is index 4) + // Line 2: " + bar\n" -> line offset 12 (4 + 8 = 12) + // Line 3: " + baz" -> end + SourceInfo sourceInfo = + SourceInfo.newBuilder() + .addLineOffsets(4) + .addLineOffsets(12) + .putPositions(1L, 0) // 'foo' on line 1, col 0 + .putPositions(2L, 8) // 'bar' on line 2, col (8 - 4) = 4 + .putPositions(3L, 16) // 'baz' on line 3, col (16 - 12) = 4 + .build(); + CelLocationAdorner adorner = new CelLocationAdorner(sourceInfo); + + assertThat(adorner.getLocation(1L)).hasValue(CelSourceLocation.of(1, 0)); + assertThat(adorner.getLocation(2L)).hasValue(CelSourceLocation.of(2, 4)); + assertThat(adorner.getLocation(3L)).hasValue(CelSourceLocation.of(3, 4)); + assertThat(adorner.adorn(Expr.newBuilder().setId(2L).build())).isEqualTo("^#2[2,4]#"); + assertThat(adorner.adorn(Expr.newBuilder().setId(3L).build())).isEqualTo("^#3[3,4]#"); + } +} From 2bb86d0ee27994c8abdcd105d4647085aba40470 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Wed, 9 Sep 2026 12:09:37 -0700 Subject: [PATCH 188/204] Implement field selection optimizer PiperOrigin-RevId: 978680353 --- optimizer/optimizers/BUILD.bazel | 5 + .../dev/cel/optimizer/optimizers/BUILD.bazel | 36 + .../optimizer/optimizers/SelectOptimizer.java | 470 +++++++++ .../dev/cel/optimizer/optimizers/BUILD.bazel | 4 + .../optimizers/SelectOptimizerTest.java | 934 ++++++++++++++++++ 5 files changed, 1449 insertions(+) create mode 100644 optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java create mode 100644 optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java diff --git a/optimizer/optimizers/BUILD.bazel b/optimizer/optimizers/BUILD.bazel index 26d98c574..e95d48728 100644 --- a/optimizer/optimizers/BUILD.bazel +++ b/optimizer/optimizers/BUILD.bazel @@ -19,3 +19,8 @@ java_library( name = "inlining", exports = ["//optimizer/src/main/java/dev/cel/optimizer/optimizers:inlining"], ) + +java_library( + name = "select_optimizer", + exports = ["//optimizer/src/main/java/dev/cel/optimizer/optimizers:select_optimizer"], +) diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel index 0e6509c44..8219753fd 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -111,6 +111,42 @@ java_library( ], ) +java_library( + name = "select_optimizer", + srcs = [ + "SelectOptimizer.java", + ], + tags = [ + ], + deps = [ + "//:auto_value", + "//bundle:cel", + "//checker:standard_decl", + "//common:cel_ast", + "//common:cel_descriptor_util", + "//common:cel_descriptors", + "//common:cel_source", + "//common:compiler_common", + "//common:mutable_ast", + "//common/ast", + "//common/ast:expr_factory", + "//common/ast:mutable_expr", + "//common/internal:cel_descriptor_pools", + "//common/navigation:common", + "//common/navigation:mutable_navigation", + "//common/types", + "//common/types:cel_types", + "//common/types:type_providers", + "//common/values:cel_byte_string", + "//optimizer:ast_optimizer", + "//optimizer:mutable_ast", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:org_jspecify_jspecify", + ], +) + java_library( name = "default_optimizer_constants", srcs = [ diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java new file mode 100644 index 000000000..3c6097180 --- /dev/null +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java @@ -0,0 +1,470 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.optimizer.optimizers; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.UnsignedLong; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.protobuf.ByteString; +import com.google.protobuf.Descriptors.EnumValueDescriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Descriptors.FileDescriptor; +import dev.cel.bundle.Cel; +import dev.cel.checker.CelStandardDeclarations.StandardFunction; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelDescriptorUtil; +import dev.cel.common.CelDescriptors; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelMutableAst; +import dev.cel.common.CelOverloadDecl; +import dev.cel.common.CelSource; +import dev.cel.common.CelSource.Extension; +import dev.cel.common.CelSource.Extension.Component; +import dev.cel.common.CelSource.Extension.Version; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import dev.cel.common.ast.CelExprIdGeneratorFactory; +import dev.cel.common.ast.CelExprIdGeneratorFactory.MonotonicIdGenerator; +import dev.cel.common.ast.CelMutableExpr; +import dev.cel.common.ast.CelMutableExpr.CelMutableCall; +import dev.cel.common.ast.CelMutableExpr.CelMutableList; +import dev.cel.common.ast.CelMutableExpr.CelMutableMap; +import dev.cel.common.ast.CelMutableExpr.CelMutableSelect; +import dev.cel.common.internal.CelDescriptorPool; +import dev.cel.common.internal.CombinedDescriptorPool; +import dev.cel.common.internal.DefaultDescriptorPool; +// CEL-Internal-1 +import dev.cel.common.navigation.CelNavigableMutableAst; +import dev.cel.common.navigation.CelNavigableMutableExpr; +import dev.cel.common.navigation.TraversalOrder; +import dev.cel.common.types.CelKind; +import dev.cel.common.types.CelTypes; +import dev.cel.common.types.ListType; +import dev.cel.common.types.SimpleType; +import dev.cel.common.values.CelByteString; +import dev.cel.optimizer.AstMutator; +import dev.cel.optimizer.CelAstOptimizer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * Performs field selection optimization on protobuf message select chains. + * + *

Embeds protobuf field metadata directly into qualification paths ({@code cel.@attribute}) and + * field presence paths ({@code cel.@hasField}). This accelerates nested field evaluation, enables + * reflection-free field traversal in resource-constrained runtimes without descriptor tables, and + * provides resilience against protobuf field renames. + * + *

WARNING: Evaluating optimized ASTs requires explicit runtime support for {@code + * cel.@attribute} and {@code cel.@hasField}. Ensure that the target evaluation environment (in + * Java, C++, Go, or other language runtimes) supports these select optimization functions before + * applying this optimizer. Evaluating an optimized AST in an unsupported runtime will result in an + * evaluation error due to missing function overloads. + * + *

Metadata Tuples: In {@code cel.@attribute}, each step in the qualification path is + * represented as a metadata tuple: + * + *

    + *
  • Scalar fields, repeated fields, maps, and well-known types (timestamp, duration) include + * their default value as a 4-tuple: {@code [field_num, field_name, type_code, default_val]}. + *
  • User-defined message fields omit the default value and are represented as a 3-tuple: {@code + * [field_num, field_name, type_code]}. Unset messages default to empty message instances + * rather than null in protobuf and CEL; omitting the default avoids synthesizing unnecessary + * message construction expressions in the AST. + *
+ * + *

Field presence paths ({@code cel.@hasField}) represent each step as a 2-tuple: {@code + * [field_num, field_name]}. + * + *

Trade-off: Modestly increases serialized AST size over the wire due to the embedded + * metadata tuples. + * + *

Expressions are rewritten into the following forms: + * + *

+ *   // Selection chains (user message is 3-tuple, leaf scalar is 4-tuple)
+ *   request.user.age -> cel.@attribute(request,
+ *       [[user_num, "user", type_code], [age_num, "age", type_code, default_val]])
+ *
+ *   // Presence tests (2-tuples)
+ *   has(request.user.age) -> cel.@hasField(request,
+ *       [[user_num, "user"], [age_num, "age"]])
+ * 
+ * + *

Map indexing and non-protobuf selects pass through untouched. + */ +public final class SelectOptimizer implements CelAstOptimizer { + + /** + * CEL select optimization type code for protobuf maps. + * + *

Protobuf wire format encodes maps as repeated message entries ({@code MapEntry}). To avoid + * wire-decoding ambiguities with singular submessages, maps use this dedicated type code. + */ + private static final long CEL_MAP_TYPE_CODE = 20L; + + private static final String CEL_ATTRIBUTE_FUNCTION_NAME = "cel.@attribute"; + private static final String CEL_HAS_FIELD_FUNCTION_NAME = "cel.@hasField"; + + @VisibleForTesting + static final CelFunctionDecl CEL_ATTRIBUTE_FUNCTION_DECL = + CelFunctionDecl.newFunctionDeclaration( + CEL_ATTRIBUTE_FUNCTION_NAME, + CelOverloadDecl.newGlobalOverload( + "cel_attribute_list", + SimpleType.DYN, + SimpleType.DYN, + ListType.create(SimpleType.DYN))); + + @VisibleForTesting + static final CelFunctionDecl CEL_HAS_FIELD_FUNCTION_DECL = + CelFunctionDecl.newFunctionDeclaration( + CEL_HAS_FIELD_FUNCTION_NAME, + CelOverloadDecl.newGlobalOverload( + "cel_has_field_list", + SimpleType.BOOL, + SimpleType.DYN, + ListType.create(SimpleType.DYN))); + + @VisibleForTesting + static final Extension SELECT_OPTIMIZATION_AST_EXTENSION_TAG = + Extension.create("select_optimization", Version.of(1L, 0L), Component.COMPONENT_RUNTIME); + + private final SelectOptimizerOptions options; + private final AstMutator astMutator; + private final CelDescriptorPool descriptorPool; + + /** Returns a new select optimizer configured with the provided file descriptors. */ + public static SelectOptimizer newInstance(FileDescriptor... fileDescriptors) { + return newInstance(SelectOptimizerOptions.newBuilder().build(), fileDescriptors); + } + + /** Returns a new select optimizer configured with the provided file descriptors. */ + public static SelectOptimizer newInstance(Iterable fileDescriptors) { + return newInstance(SelectOptimizerOptions.newBuilder().build(), fileDescriptors); + } + + /** Returns a new select optimizer configured with the provided options and file descriptors. */ + public static SelectOptimizer newInstance( + SelectOptimizerOptions options, FileDescriptor... fileDescriptors) { + return newInstance(options, Arrays.asList(checkNotNull(fileDescriptors))); + } + + /** Returns a new select optimizer configured with the provided options and file descriptors. */ + public static SelectOptimizer newInstance( + SelectOptimizerOptions options, Iterable fileDescriptors) { + checkNotNull(options); + checkNotNull(fileDescriptors); + return new SelectOptimizer(options, fileDescriptors); + } + + @Override + public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) { + checkArgument(ast.isChecked(), "AST must be type-checked."); + + CelMutableAst astToModify = CelMutableAst.fromCelAst(ast); + CelNavigableMutableAst navAst = CelNavigableMutableAst.fromAst(astToModify); + ImmutableList topOfChainSelects = + navAst + .getRoot() + .allNodes(TraversalOrder.POST_ORDER) + .filter(node -> isTopOfSelectChain(navAst, node)) + .collect(toImmutableList()); + + if (topOfChainSelects.isEmpty()) { + return OptimizationResult.create(ast); + } + + MonotonicIdGenerator idGenerator = + CelExprIdGeneratorFactory.newMonotonicIdGenerator(navAst.getRoot().maxId()); + + int iterationCount = 0; + for (CelNavigableMutableExpr topNode : topOfChainSelects) { + if (++iterationCount > options.iterationLimit()) { + throw new IllegalStateException("Max iteration count reached."); + } + rewriteSelectChain(astToModify, navAst, topNode, idGenerator); + } + + astToModify = astMutator.renumberIdsConsecutively(astToModify); + CelAbstractSyntaxTree optimizedAst = tagAstExtension(astToModify.toParsedAst()); + + return OptimizationResult.create( + optimizedAst, + ImmutableList.of(), + ImmutableList.of(CEL_ATTRIBUTE_FUNCTION_DECL, CEL_HAS_FIELD_FUNCTION_DECL)); + } + + private void rewriteSelectChain( + CelMutableAst astToModify, + CelNavigableMutableAst navAst, + CelNavigableMutableExpr topNode, + MonotonicIdGenerator idGenerator) { + boolean isHasField = topNode.expr().select().testOnly(); + astToModify.source().getMacroCalls().remove(topNode.expr().id()); + + List fields = new ArrayList<>(); + FieldDescriptor topField = + getOptimizableField(navAst, topNode) + .orElseThrow( + () -> new IllegalStateException("Expected optimizable field on select node")); + fields.add(topField); + + // TODO: Support optional field selection (_?._) once integrated with lite runtime. + CelMutableExpr currentExpr = topNode.expr().select().operand(); + while (currentExpr.getKind() == Kind.SELECT) { + CelMutableSelect select = currentExpr.select(); + FieldDescriptor field = getOptimizableFieldForExpr(navAst, select).orElse(null); + if (field == null) { + break; + } + fields.add(field); + currentExpr = select.operand(); + } + + Collections.reverse(fields); + + List qualifierLists = new ArrayList<>(fields.size()); + for (FieldDescriptor field : fields) { + if (field.getType() == FieldDescriptor.Type.GROUP) { + throw new UnsupportedOperationException( + "Optimization of Group fields is unsupported: " + field.getFullName()); + } + if (field.getType() == FieldDescriptor.Type.MESSAGE) { + String messageFullName = field.getMessageType().getFullName(); + if (messageFullName.equals(CelTypes.STRUCT_MESSAGE)) { + throw new UnsupportedOperationException( + "Optimization of Struct fields is currently unimplemented: " + field.getFullName()); + } + if (messageFullName.equals(CelTypes.LIST_VALUE_MESSAGE)) { + throw new UnsupportedOperationException( + "Optimization of ListValue fields is currently unimplemented: " + + field.getFullName()); + } + if (messageFullName.equals(CelTypes.VALUE_MESSAGE)) { + throw new UnsupportedOperationException( + "Optimization of Value fields is currently unimplemented: " + field.getFullName()); + } + if (messageFullName.equals(CelTypes.ANY_MESSAGE)) { + throw new UnsupportedOperationException( + "Optimization of Any fields is currently unimplemented: " + field.getFullName()); + } + if (CelTypes.isWrapperType(messageFullName)) { + throw new UnsupportedOperationException( + "Optimization of wrapper fields is currently unimplemented: " + field.getFullName()); + } + } + + CelMutableList qualifierElements = + CelMutableList.create( + CelMutableExpr.ofConstant( + idGenerator.nextExprId(), CelConstant.ofValue((long) field.getNumber())), + CelMutableExpr.ofConstant( + idGenerator.nextExprId(), CelConstant.ofValue(field.getName()))); + if (!isHasField) { + qualifierElements + .elements() + .add( + CelMutableExpr.ofConstant( + idGenerator.nextExprId(), CelConstant.ofValue(resolveTypeCode(field)))); + resolveDefaultValue(field, idGenerator).ifPresent(qualifierElements.elements()::add); + } + qualifierLists.add(CelMutableExpr.ofList(idGenerator.nextExprId(), qualifierElements)); + } + + CelMutableExpr qualifiersExpr = + CelMutableExpr.ofList(idGenerator.nextExprId(), CelMutableList.create(qualifierLists)); + String functionName = isHasField ? CEL_HAS_FIELD_FUNCTION_NAME : CEL_ATTRIBUTE_FUNCTION_NAME; + topNode.expr().setCall(CelMutableCall.create(functionName, currentExpr, qualifiersExpr)); + } + + private static long resolveTypeCode(FieldDescriptor field) { + if (field.isMapField()) { + return CEL_MAP_TYPE_CODE; + } + return field.getType().toProto().getNumber(); + } + + private boolean isTopOfSelectChain(CelNavigableMutableAst navAst, CelNavigableMutableExpr node) { + return getOptimizableField(navAst, node).isPresent() + && !node.parent().flatMap(parent -> getOptimizableField(navAst, parent)).isPresent(); + } + + private Optional getOptimizableField( + CelNavigableMutableAst navAst, CelNavigableMutableExpr node) { + if (node.getKind() != Kind.SELECT) { + return Optional.empty(); + } + return getOptimizableFieldForExpr(navAst, node.expr().select()); + } + + private Optional getOptimizableFieldForExpr( + CelNavigableMutableAst navAst, CelMutableSelect select) { + return navAst + .getType(select.operand().id()) + .filter(type -> type.kind() == CelKind.STRUCT) + .flatMap(type -> descriptorPool.findDescriptor(type.name())) + .map(desc -> desc.findFieldByName(select.field())); + } + + private static Optional resolveDefaultValue( + FieldDescriptor field, MonotonicIdGenerator idGenerator) { + if (field.isMapField()) { + return Optional.of( + CelMutableExpr.ofMap(idGenerator.nextExprId(), CelMutableMap.create(ImmutableList.of()))); + } + if (field.isRepeated()) { + return Optional.of(CelMutableExpr.ofList(idGenerator.nextExprId(), CelMutableList.create())); + } + if (field.getType() == FieldDescriptor.Type.MESSAGE) { + String messageFullName = field.getMessageType().getFullName(); + switch (messageFullName) { + case CelTypes.DURATION_MESSAGE: + return Optional.of( + CelMutableExpr.ofCall( + idGenerator.nextExprId(), + CelMutableCall.create( + StandardFunction.DURATION.functionName(), + CelMutableExpr.ofConstant( + idGenerator.nextExprId(), CelConstant.ofValue("0s"))))); + case CelTypes.TIMESTAMP_MESSAGE: + return Optional.of( + CelMutableExpr.ofCall( + idGenerator.nextExprId(), + CelMutableCall.create( + StandardFunction.TIMESTAMP.functionName(), + CelMutableExpr.ofConstant( + idGenerator.nextExprId(), CelConstant.ofValue(0L))))); + // TODO: Support STRUCT_MESSAGE, LIST_VALUE_MESSAGE, VALUE_MESSAGE, + // ANY_MESSAGE, + // and wrapper types. + default: + // User-defined message fields omit default values (encoded as 3-tuples). + return Optional.empty(); + } + } + + return Optional.of( + CelMutableExpr.ofConstant(idGenerator.nextExprId(), resolveConstantDefaultValue(field))); + } + + private static CelConstant resolveConstantDefaultValue(FieldDescriptor field) { + Object def = field.getDefaultValue(); + switch (field.getType()) { + case DOUBLE: + return CelConstant.ofValue((Double) def); + case FLOAT: + return CelConstant.ofValue(((Float) def).doubleValue()); + case INT64: + case SINT64: + case SFIXED64: + return CelConstant.ofValue((Long) def); + case UINT64: + case FIXED64: + return CelConstant.ofValue(UnsignedLong.fromLongBits((Long) def)); + case INT32: + case SINT32: + case SFIXED32: + return CelConstant.ofValue(((Integer) def).longValue()); + case UINT32: + case FIXED32: + return CelConstant.ofValue( + UnsignedLong.fromLongBits(Integer.toUnsignedLong((Integer) def))); + case BOOL: + return CelConstant.ofValue((Boolean) def); + case STRING: + return CelConstant.ofValue((String) def); + case BYTES: + ByteString byteString = (ByteString) def; + return byteString.isEmpty() + ? CelConstant.ofValue(CelByteString.EMPTY) + : CelConstant.ofValue(CelByteString.of(byteString.toByteArray())); + case ENUM: + EnumValueDescriptor enumValue = (EnumValueDescriptor) def; + return CelConstant.ofValue((long) enumValue.getNumber()); + default: + throw new IllegalArgumentException("Unsupported protobuf field type: " + field.getType()); + } + } + + private static CelAbstractSyntaxTree tagAstExtension(CelAbstractSyntaxTree ast) { + CelSource.Builder celSourceBuilder = + ast.getSource().toBuilder().addAllExtensions(SELECT_OPTIMIZATION_AST_EXTENSION_TAG); + return CelAbstractSyntaxTree.newParsedAst(ast.getExpr(), celSourceBuilder.build()); + } + + private SelectOptimizer( + SelectOptimizerOptions options, Iterable fileDescriptors) { + this.options = checkNotNull(options); + this.astMutator = AstMutator.newInstance(options.iterationLimit()); + this.descriptorPool = newDescriptorPool(options, checkNotNull(fileDescriptors)); + } + + private static CelDescriptorPool newDescriptorPool( + SelectOptimizerOptions options, Iterable fileDescriptors) { + CelDescriptors celDescriptors = + CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(checkNotNull(fileDescriptors)); + ImmutableList.Builder descriptorPools = ImmutableList.builder(); + + descriptorPools.add(DefaultDescriptorPool.create(celDescriptors)); + + return CombinedDescriptorPool.create(descriptorPools.build()); + } + + /** Options configuring the behavior of {@link SelectOptimizer}. */ + @AutoValue + public abstract static class SelectOptimizerOptions { + + public abstract int iterationLimit(); + + public abstract boolean enableLinkedMessageTypes(); + + /** Builder for configuring {@link SelectOptimizerOptions}. */ + @AutoValue.Builder + public abstract static class Builder { + + @CanIgnoreReturnValue + public abstract Builder iterationLimit(int value); + + @CanIgnoreReturnValue + public abstract Builder enableLinkedMessageTypes(boolean enable); + + public abstract SelectOptimizerOptions build(); + + Builder() {} + } + + public abstract Builder toBuilder(); + + /** Returns a new options builder with recommended defaults. */ + public static Builder newBuilder() { + return new AutoValue_SelectOptimizer_SelectOptimizerOptions.Builder() + .iterationLimit(500) + .enableLinkedMessageTypes(true); + } + + // Package-private constructor to prevent external extension, required by @AutoValue. + SelectOptimizerOptions() {} + } +} diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel index c912d9570..787012466 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -18,6 +18,7 @@ java_library( "//common:container", "//common:mutable_ast", "//common:options", + "//common:proto_ast", "//common/ast", "//common/navigation:mutable_navigation", "//common/types", @@ -30,6 +31,7 @@ java_library( "//optimizer/optimizers:common_subexpression_elimination", "//optimizer/optimizers:constant_folding", "//optimizer/optimizers:inlining", + "//optimizer/optimizers:select_optimizer", "//parser:macro", "//parser:unparser", "//runtime", @@ -42,6 +44,8 @@ java_library( "@maven//:junit_junit", "@maven//:com_google_testparameterinjector_test_parameter_injector", "//:java_truth", + "@maven//:com_google_truth_extensions_truth_proto_extension", + "@cel_spec//proto/cel/expr:syntax_java_proto", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@maven//:com_google_guava_guava", diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java new file mode 100644 index 000000000..7740319fe --- /dev/null +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java @@ -0,0 +1,934 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.optimizer.optimizers; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static org.junit.Assert.assertThrows; + +import dev.cel.expr.ParsedExpr; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.TextFormat; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelBuilder; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelMutableAst; +import dev.cel.common.CelOptions; +import dev.cel.common.CelProtoAbstractSyntaxTree; +import dev.cel.common.CelValidationException; +import dev.cel.common.navigation.CelNavigableMutableAst; +import dev.cel.common.types.MapType; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.expr.conformance.proto2.NestedTestAllTypes; +import dev.cel.expr.conformance.proto2.TestAllTypesProto; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.optimizer.CelOptimizer; +import dev.cel.optimizer.CelOptimizerFactory; +import dev.cel.optimizer.optimizers.SelectOptimizer.SelectOptimizerOptions; +import dev.cel.parser.CelStandardMacro; +import dev.cel.parser.CelUnparser; +import dev.cel.parser.CelUnparserFactory; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelFunctionBinding; +import dev.cel.runtime.CelRuntime.Program; +import dev.cel.testing.CelRuntimeFlavor; +import java.util.List; +import java.util.stream.LongStream; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class SelectOptimizerTest { + + private static final CelOptions CEL_OPTIONS = + CelOptions.current() + .populateMacroCalls(true) + .enableHeterogeneousNumericComparisons(true) + .build(); + + private static final CelUnparser CEL_UNPARSER = CelUnparserFactory.newUnparser(); + + private static final Descriptor PROTO2_TEST_ALL_TYPES_DESCRIPTOR = + checkNotNull(TestAllTypesProto.getDescriptor().findMessageTypeByName("TestAllTypes")); + + @TestParameter CelRuntimeFlavor runtimeFlavor; + + private Cel cel; + private CelOptimizer celOptimizer; + + @Before + public void setUp() { + cel = setupEnv(runtimeFlavor.builder()); + celOptimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(cel) + .addAstOptimizers( + SelectOptimizer.newInstance( + SelectOptimizerOptions.newBuilder().build(), + TestAllTypes.getDescriptor().getFile(), + PROTO2_TEST_ALL_TYPES_DESCRIPTOR.getFile(), + NestedTestAllTypes.getDescriptor().getFile())) + .build(); + } + + private static Cel setupEnv(CelBuilder celBuilder) { + return celBuilder + .setOptions(CEL_OPTIONS) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addMessageTypes(TestAllTypes.getDescriptor()) + .addMessageTypes(PROTO2_TEST_ALL_TYPES_DESCRIPTOR) + .addMessageTypes(NestedTestAllTypes.getDescriptor()) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .addVar( + "proto2_msg", + StructTypeReference.create(PROTO2_TEST_ALL_TYPES_DESCRIPTOR.getFullName())) + .addVar( + "nested_msg", + StructTypeReference.create(NestedTestAllTypes.getDescriptor().getFullName())) + .addVar("map_var", MapType.create(SimpleType.STRING, SimpleType.INT)) + .addVar( + "map_var_msg", + MapType.create( + SimpleType.STRING, + StructTypeReference.create(TestAllTypes.getDescriptor().getFullName()))) + .addVar("x", SimpleType.INT) + .build(); + } + + private enum RewriteTestCase { + // === Selection & Traversal === + PROTO3_SINGLE_FIELD_SELECT( + "msg.single_int64", "cel.@attribute(msg, [[2, \"single_int64\", 3, 0]])"), + PROTO3_SINGLE_MESSAGE_FIELD_SELECT( + "msg.single_nested_message", "cel.@attribute(msg, [[21, \"single_nested_message\", 11]])"), + PROTO3_CHAINED_FIELD_SELECT( + "msg.single_nested_message.bb", + "cel.@attribute(msg, [[21, \"single_nested_message\", 11], [1, \"bb\", 5, 0]])"), + PROTO2_SINGLE_MESSAGE_FIELD_SELECT( + "proto2_msg.single_nested_message", + "cel.@attribute(proto2_msg, [[21, \"single_nested_message\", 11]])"), + PROTO2_CHAINED_FIELD_SELECT( + "proto2_msg.single_nested_message.bb", + "cel.@attribute(proto2_msg, [[21, \"single_nested_message\", 11], [1, \"bb\", 5, 0]])"), + PROTO2_TRIPLE_CHAINED_FIELD_SELECT( + "nested_msg.child.payload.single_int64", + "cel.@attribute(nested_msg, " + + "[[1, \"child\", 11], " + + "[2, \"payload\", 11], " + + "[2, \"single_int64\", 3, -64]])"), + PROTO2_CHAINED_MESSAGE_FIELD_SELECT( + "nested_msg.child.payload", + "cel.@attribute(nested_msg, [[1, \"child\", 11], [2, \"payload\", 11]])"), + + // === Presence Tests: Proto2 (Explicit Presence) vs Proto3 (Implicit/Explicit Presence) === + // In proto2, scalar fields have explicit presence (has-bit). + PROTO2_HAS_SCALAR_INT32( + "has(proto2_msg.single_int32)", "cel.@hasField(proto2_msg, [[1, \"single_int32\"]])"), + PROTO2_HAS_SCALAR_INT64( + "has(proto2_msg.single_int64)", "cel.@hasField(proto2_msg, [[2, \"single_int64\"]])"), + // In proto3, non-optional scalar fields have implicit presence (evaluated as != default). + PROTO3_HAS_SCALAR_INT32("has(msg.single_int32)", "cel.@hasField(msg, [[1, \"single_int32\"]])"), + PROTO3_HAS_SCALAR_INT64("has(msg.single_int64)", "cel.@hasField(msg, [[2, \"single_int64\"]])"), + // In proto3, explicit optional scalars have presence (has-bit). + PROTO3_HAS_OPTIONAL_BOOL( + "has(msg.optional_bool)", "cel.@hasField(msg, [[16, \"optional_bool\"]])"), + PROTO3_HAS_OPTIONAL_STRING( + "has(msg.optional_string)", "cel.@hasField(msg, [[17, \"optional_string\"]])"), + // Messages in both proto2 and proto3 have explicit presence. + PROTO2_HAS_MESSAGE( + "has(proto2_msg.single_nested_message)", + "cel.@hasField(proto2_msg, [[21, \"single_nested_message\"]])"), + PROTO3_HAS_MESSAGE( + "has(msg.single_nested_message)", "cel.@hasField(msg, [[21, \"single_nested_message\"]])"), + PROTO3_HAS_STANDALONE_MESSAGE( + "has(msg.standalone_message)", "cel.@hasField(msg, [[23, \"standalone_message\"]])"), + PROTO3_HAS_ONEOF_ENUM( + "has(msg.single_nested_enum)", "cel.@hasField(msg, [[22, \"single_nested_enum\"]])"), + PROTO2_HAS_CHAINED_MESSAGE( + "has(proto2_msg.single_nested_message.bb)", + "cel.@hasField(proto2_msg, [[21, \"single_nested_message\"], [1, \"bb\"]])"), + PROTO3_HAS_CHAINED_MESSAGE( + "has(msg.single_nested_message.bb)", + "cel.@hasField(msg, [[21, \"single_nested_message\"], [1, \"bb\"]])"), + PROTO2_HAS_TRIPLE_CHAINED_MESSAGE( + "has(nested_msg.child.payload.single_int64)", + "cel.@hasField(nested_msg, [[1, \"child\"], [2, \"payload\"], [2, \"single_int64\"]])"), + + // === Default Value Divergence: Proto2 Custom Defaults vs Proto3 Zero Defaults === + // Int32: proto2 has custom default -32, proto3 has 0 + PROTO2_CUSTOM_INT32( + "proto2_msg.single_int32", "cel.@attribute(proto2_msg, [[1, \"single_int32\", 5, -32]])"), + PROTO3_ZERO_INT32("msg.single_int32", "cel.@attribute(msg, [[1, \"single_int32\", 5, 0]])"), + + // Int64: proto2 has custom default -64, proto3 has 0 + PROTO2_CUSTOM_INT64( + "proto2_msg.single_int64", "cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]])"), + PROTO3_ZERO_INT64("msg.single_int64", "cel.@attribute(msg, [[2, \"single_int64\", 3, 0]])"), + + // Uint32: proto2 has custom default 32, proto3 has 0 + PROTO2_CUSTOM_UINT32( + "proto2_msg.single_uint32", + "cel.@attribute(proto2_msg, [[3, \"single_uint32\", 13, 32u]])"), + PROTO3_ZERO_UINT32( + "msg.single_uint32", "cel.@attribute(msg, [[3, \"single_uint32\", 13, 0u]])"), + + // Uint64: proto2 has custom default 64, proto3 has 0 + PROTO2_CUSTOM_UINT64( + "proto2_msg.single_uint64", "cel.@attribute(proto2_msg, [[4, \"single_uint64\", 4, 64u]])"), + PROTO3_ZERO_UINT64("msg.single_uint64", "cel.@attribute(msg, [[4, \"single_uint64\", 4, 0u]])"), + + // String: proto2 has custom default "empty", proto3 has "" + PROTO2_CUSTOM_STRING( + "proto2_msg.single_string", + "cel.@attribute(proto2_msg, [[14, \"single_string\", 9, \"empty\"]])"), + PROTO3_ZERO_STRING( + "msg.single_string", "cel.@attribute(msg, [[14, \"single_string\", 9, \"\"]])"), + + // Bool: proto2 has custom default true, proto3 has false + PROTO2_CUSTOM_BOOL( + "proto2_msg.single_bool", "cel.@attribute(proto2_msg, [[13, \"single_bool\", 8, true]])"), + PROTO3_ZERO_BOOL("msg.single_bool", "cel.@attribute(msg, [[13, \"single_bool\", 8, false]])"), + + // Float: proto2 has custom default 3.0, proto3 has 0.0 + PROTO2_CUSTOM_FLOAT( + "proto2_msg.single_float", "cel.@attribute(proto2_msg, [[11, \"single_float\", 2, 3.0]])"), + PROTO3_ZERO_FLOAT("msg.single_float", "cel.@attribute(msg, [[11, \"single_float\", 2, 0.0]])"), + + // Double: proto2 has custom default 6.4, proto3 has 0.0 + PROTO2_CUSTOM_DOUBLE( + "proto2_msg.single_double", + "cel.@attribute(proto2_msg, [[12, \"single_double\", 1, 6.4]])"), + PROTO3_ZERO_DOUBLE( + "msg.single_double", "cel.@attribute(msg, [[12, \"single_double\", 1, 0.0]])"), + + // Bytes: proto2 has custom default "none", proto3 has "" + PROTO2_CUSTOM_BYTES( + "proto2_msg.single_bytes", + "cel.@attribute(proto2_msg, [[15, \"single_bytes\", 12, b\"\\156\\157\\156\\145\"]])"), + PROTO3_ZERO_BYTES( + "msg.single_bytes", "cel.@attribute(msg, [[15, \"single_bytes\", 12, b\"\"]])"), + + // Enum: proto2 has custom default 1 (BAR), proto3 has 0 (FOO) + PROTO2_CUSTOM_ENUM( + "proto2_msg.single_nested_enum", + "cel.@attribute(proto2_msg, [[22, \"single_nested_enum\", 14, 1]])"), + PROTO3_ZERO_ENUM( + "msg.single_nested_enum", "cel.@attribute(msg, [[22, \"single_nested_enum\", 14, 0]])"), + + // Fixed / sfixed fields + PROTO3_SFIXED32( + "msg.single_sfixed32", "cel.@attribute(msg, [[9, \"single_sfixed32\", 15, 0]])"), + PROTO3_SFIXED64( + "msg.single_sfixed64", "cel.@attribute(msg, [[10, \"single_sfixed64\", 16, 0]])"), + + // Repeated fields: empty list default + PROTO2_REPEATED_PRIMITIVE( + "proto2_msg.repeated_int64", + "cel.@attribute(proto2_msg, [[32, \"repeated_int64\", 3, []]])"), + PROTO3_REPEATED_PRIMITIVE( + "msg.repeated_int64", "cel.@attribute(msg, [[32, \"repeated_int64\", 3, []]])"), + PROTO3_REPEATED_MESSAGE( + "msg.repeated_nested_message", + "cel.@attribute(msg, [[51, \"repeated_nested_message\", 11, []]])"), + + // Well-known types + PROTO3_TIMESTAMP( + "msg.single_timestamp", + "cel.@attribute(msg, [[102, \"single_timestamp\", 11, timestamp(0)]])"), + PROTO3_DURATION( + "msg.single_duration", + "cel.@attribute(msg, [[101, \"single_duration\", 11, duration(\"0s\")]])"), + + // Map selects + MAP_FIELD_INDEXING( + "msg.map_int64_message[1].bb", + "cel.@attribute(" + + "cel.@attribute(msg, [[95, \"map_int64_message\", 20, {}]])[1], " + + "[[1, \"bb\", 5, 0]])"), + MAP_FIELD_SELECT_CHAIN_STOPS_AT_MAP_BOUNDARY( + "map_var_msg.key.single_nested_message.bb", + "cel.@attribute(map_var_msg.key, " + + "[[21, \"single_nested_message\", 11], " + + "[1, \"bb\", 5, 0]])"), + MAP_FIELD_SELECT_STOPS_AT_MAP_BOUNDARY( + "map_var_msg.key.single_int64", + "cel.@attribute(map_var_msg.key, [[2, \"single_int64\", 3, 0]])"), + MAP_FIELD_HAS_STOPS_AT_MAP_BOUNDARY( + "has(map_var_msg.key.single_nested_message)", + "cel.@hasField(map_var_msg.key, [[21, \"single_nested_message\"]])"), + MAP_FIELD_HAS_CHAIN_STOPS_AT_MAP_BOUNDARY( + "has(map_var_msg.key.single_nested_message.bb)", + "cel.@hasField(map_var_msg.key, [[21, \"single_nested_message\"], [1, \"bb\"]])"), + PROTO_MAP_FIELD_SELECT_STOPS_AT_MAP_BOUNDARY( + "msg.map_string_message.key.bb", + "cel.@attribute(" + + "cel.@attribute(msg, [[227, \"map_string_message\", 20, {}]]).key, " + + "[[1, \"bb\", 5, 0]])"), + PROTO_MAP_FIELD_HAS_STOPS_AT_MAP_BOUNDARY( + "has(msg.map_string_message.key.bb)", + "cel.@hasField(" + + "cel.@attribute(msg, [[227, \"map_string_message\", 20, {}]]).key, " + + "[[1, \"bb\"]])"), + + MIXED_BOOLEAN_EXPRESSION( + "msg.single_int64 > 0 && has(msg.single_nested_message)", + "cel.@attribute(msg, [[2, \"single_int64\", 3, 0]]) > 0 " + + "&& cel.@hasField(msg, [[21, \"single_nested_message\"]])"); + + private final String expression; + private final String expectedUnparsed; + + RewriteTestCase(String expression, String expectedUnparsed) { + this.expression = expression; + this.expectedUnparsed = expectedUnparsed; + } + } + + @Test + public void optimize_rewritesSelectExpressions(@TestParameter RewriteTestCase testCase) + throws Exception { + CelAbstractSyntaxTree ast = cel.compile(testCase.expression).getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo(testCase.expectedUnparsed); + assertThat(optimizedAst.getSource().getExtensions()) + .contains(SelectOptimizer.SELECT_OPTIMIZATION_AST_EXTENSION_TAG); + } + + @Test + public void optimize_unoptimizableMapFieldSelect_leavesAstUntouched() throws Exception { + CelAbstractSyntaxTree ast = cel.compile("map_var.key").getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo("map_var.key"); + assertThat(optimizedAst.getSource().getExtensions()) + .doesNotContain(SelectOptimizer.SELECT_OPTIMIZATION_AST_EXTENSION_TAG); + } + + @Test + public void optimize_unoptimizableMapHasField_leavesAstUntouched() throws Exception { + CelAbstractSyntaxTree ast = cel.compile("has(map_var.key)").getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)).isEqualTo("has(map_var.key)"); + assertThat(optimizedAst.getSource().getExtensions()) + .doesNotContain(SelectOptimizer.SELECT_OPTIMIZATION_AST_EXTENSION_TAG); + } + + @Test + public void optimize_noSelects_returnsOriginalAst() throws Exception { + CelAbstractSyntaxTree ast = cel.compile("1 + 2 == 3").getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(optimizedAst).isEqualTo(ast); + assertThat(optimizedAst.getSource().getExtensions()) + .doesNotContain(SelectOptimizer.SELECT_OPTIMIZATION_AST_EXTENSION_TAG); + } + + @Test + public void optimize_notCheckedAst_throwsIllegalArgumentException() throws Exception { + CelAbstractSyntaxTree parsedAst = cel.parse("msg.single_int64").getAst(); + SelectOptimizer optimizer = + SelectOptimizer.newInstance(SelectOptimizerOptions.newBuilder().build()); + + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, () -> optimizer.optimize(parsedAst, cel)); + + assertThat(exception).hasMessageThat().contains("AST must be type-checked."); + } + + @Test + public void optimize_withFileDescriptors_success() throws Exception { + FileDescriptor fd = TestAllTypes.getDescriptor().getFile(); + SelectOptimizer customOptimizer = + SelectOptimizer.newInstance(SelectOptimizerOptions.newBuilder().build(), fd); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(cel) + .addAstOptimizers(customOptimizer) + .build(); + CelAbstractSyntaxTree ast = cel.compile("msg.single_int64").getAst(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo("cel.@attribute(msg, [[2, \"single_int64\", 3, 0]])"); + } + + @Test + public void optimize_withFileDescriptorsIterable_success() throws Exception { + FileDescriptor fd = TestAllTypes.getDescriptor().getFile(); + SelectOptimizer customOptimizer = + SelectOptimizer.newInstance( + SelectOptimizerOptions.newBuilder().build(), ImmutableList.of(fd)); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(cel) + .addAstOptimizers(customOptimizer) + .build(); + CelAbstractSyntaxTree ast = cel.compile("msg.single_int64").getAst(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo("cel.@attribute(msg, [[2, \"single_int64\", 3, 0]])"); + } + + @Test + public void newInstance_withOptionsAndFileDescriptors_preservesAddedDescriptors() + throws Exception { + FileDescriptor fd = PROTO2_TEST_ALL_TYPES_DESCRIPTOR.getFile(); + SelectOptimizerOptions baseOptions = + SelectOptimizerOptions.newBuilder().enableLinkedMessageTypes(false).build(); + SelectOptimizer optimizer = SelectOptimizer.newInstance(baseOptions, fd); + CelAbstractSyntaxTree ast = cel.compile("proto2_msg.single_int64").getAst(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast, cel).optimizedAst(); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]])"); + } + + @Test + public void optimize_defaultOptions_populatesMacroCalls() throws Exception { + CelAbstractSyntaxTree ast = + cel.compile("[1].exists(x, x > 0) && msg.single_int64 > 0").getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(optimizedAst.getSource().getMacroCalls()).isNotEmpty(); + } + + @Test + public void optimize_hasFieldMacroCall_removesHasMacroCallFromSource() throws Exception { + CelAbstractSyntaxTree ast = cel.compile("has(msg.single_int64)").getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(optimizedAst.getSource().getMacroCalls()).isEmpty(); + } + + @Test + public void optimize_renumbersIdsConsecutively() throws Exception { + CelAbstractSyntaxTree ast = cel.compile("msg.single_nested_message.bb").getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + CelNavigableMutableAst navAst = + CelNavigableMutableAst.fromAst(CelMutableAst.fromCelAst(optimizedAst)); + ImmutableList ids = + navAst + .getRoot() + .allNodes() + .map(node -> node.expr().id()) + .sorted() + .collect(toImmutableList()); + ImmutableList expectedIds = + LongStream.rangeClosed(1, ids.size()).boxed().collect(toImmutableList()); + assertThat(ids).containsExactlyElementsIn(expectedIds).inOrder(); + } + + @Test + public void optimizeAndEvaluate_withAttributeFunctionBinding_evaluatesSuccessfully() + throws Exception { + Cel celWithBinding = + cel.toCelBuilder() + .addFunctionDeclarations(SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL) + .addFunctionBindings( + CelFunctionBinding.from( + "cel_attribute_list", Object.class, List.class, (target, path) -> 42L)) + .build(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(celWithBinding) + .addAstOptimizers( + SelectOptimizer.newInstance( + SelectOptimizerOptions.newBuilder().build(), + TestAllTypes.getDescriptor().getFile())) + .build(); + CelAbstractSyntaxTree ast = celWithBinding.compile("msg.single_int64").getAst(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + Object result = + celWithBinding + .createProgram(optimizedAst) + .eval(ImmutableMap.of("msg", TestAllTypes.getDefaultInstance())); + + assertThat(result).isEqualTo(42L); + } + + @Test + public void optimizeAndEvaluate_withChainedMessageSelect_unpacksTuplesSuccessfully() + throws Exception { + Cel celWithBinding = + cel.toCelBuilder() + .addFunctionDeclarations(SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL) + .addFunctionBindings( + CelFunctionBinding.from( + "cel_attribute_list", Object.class, List.class, (target, path) -> path)) + .build(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(celWithBinding) + .addAstOptimizers( + SelectOptimizer.newInstance( + SelectOptimizerOptions.newBuilder().build(), + TestAllTypes.getDescriptor().getFile())) + .build(); + CelAbstractSyntaxTree ast = celWithBinding.compile("msg.single_nested_message.bb").getAst(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + Object result = + celWithBinding + .createProgram(optimizedAst) + .eval(ImmutableMap.of("msg", TestAllTypes.getDefaultInstance())); + + assertThat((List) result) + .containsExactly( + ImmutableList.of(21L, "single_nested_message", 11L), ImmutableList.of(1L, "bb", 5L, 0L)) + .inOrder(); + } + + @Test + public void optimizeAndEvaluate_withHasFieldFunctionBinding_evaluatesSuccessfully() + throws Exception { + Cel celWithBinding = + cel.toCelBuilder() + .addFunctionDeclarations(SelectOptimizer.CEL_HAS_FIELD_FUNCTION_DECL) + .addFunctionBindings( + CelFunctionBinding.from( + "cel_has_field_list", Object.class, List.class, (target, path) -> true)) + .build(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(celWithBinding) + .addAstOptimizers( + SelectOptimizer.newInstance( + SelectOptimizerOptions.newBuilder().build(), + TestAllTypes.getDescriptor().getFile())) + .build(); + CelAbstractSyntaxTree ast = celWithBinding.compile("has(msg.single_int64)").getAst(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + Object result = + celWithBinding + .createProgram(optimizedAst) + .eval(ImmutableMap.of("msg", TestAllTypes.getDefaultInstance())); + + assertThat(result).isEqualTo(true); + } + + @Test + public void optimizeAndEvaluate_withSelectOnMapValue_evaluatesSuccessfully() throws Exception { + Cel celWithBinding = + cel.toCelBuilder() + .addFunctionDeclarations(SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL) + .addFunctionBindings( + CelFunctionBinding.from( + "cel_attribute_list", Object.class, List.class, (target, path) -> 42L)) + .build(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(celWithBinding) + .addAstOptimizers( + SelectOptimizer.newInstance( + SelectOptimizerOptions.newBuilder().build(), + TestAllTypes.getDescriptor().getFile())) + .build(); + CelAbstractSyntaxTree ast = celWithBinding.compile("map_var_msg.key.single_int64").getAst(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + Object result = + celWithBinding + .createProgram(optimizedAst) + .eval( + ImmutableMap.of( + "map_var_msg", ImmutableMap.of("key", TestAllTypes.getDefaultInstance()))); + + assertThat(result).isEqualTo(42L); + assertThat(optimizedAst.getSource().getExtensions()) + .contains(SelectOptimizer.SELECT_OPTIMIZATION_AST_EXTENSION_TAG); + } + + @Test + public void optimizeAndEvaluate_withHasOnMapValue_evaluatesSuccessfully() throws Exception { + Cel celWithBinding = + cel.toCelBuilder() + .addFunctionDeclarations(SelectOptimizer.CEL_HAS_FIELD_FUNCTION_DECL) + .addFunctionBindings( + CelFunctionBinding.from( + "cel_has_field_list", Object.class, List.class, (target, path) -> true)) + .build(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(celWithBinding) + .addAstOptimizers( + SelectOptimizer.newInstance( + SelectOptimizerOptions.newBuilder().build(), + TestAllTypes.getDescriptor().getFile())) + .build(); + CelAbstractSyntaxTree ast = + celWithBinding.compile("has(map_var_msg.key.single_nested_message)").getAst(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + Object result = + celWithBinding + .createProgram(optimizedAst) + .eval( + ImmutableMap.of( + "map_var_msg", ImmutableMap.of("key", TestAllTypes.getDefaultInstance()))); + + assertThat(result).isEqualTo(true); + assertThat(optimizedAst.getSource().getExtensions()) + .contains(SelectOptimizer.SELECT_OPTIMIZATION_AST_EXTENSION_TAG); + } + + @Test + public void optimizeAndEvaluate_withMissingMapKey_throwsEvaluationException() throws Exception { + Cel celWithBinding = + cel.toCelBuilder() + .addFunctionDeclarations(SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL) + .addFunctionBindings( + CelFunctionBinding.from( + "cel_attribute_list", Object.class, List.class, (target, path) -> 42L)) + .build(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(celWithBinding) + .addAstOptimizers( + SelectOptimizer.newInstance( + SelectOptimizerOptions.newBuilder().build(), + TestAllTypes.getDescriptor().getFile())) + .build(); + CelAbstractSyntaxTree ast = celWithBinding.compile("map_var_msg.key.single_int64").getAst(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + Program program = celWithBinding.createProgram(optimizedAst); + ImmutableMap input = ImmutableMap.of("map_var_msg", ImmutableMap.of()); + + CelEvaluationException exception = + assertThrows(CelEvaluationException.class, () -> program.eval(input)); + + assertThat(exception).hasMessageThat().contains("key 'key' is not present in map"); + } + + @Test + public void options_toBuilder_preservesValues() { + SelectOptimizerOptions options = + SelectOptimizerOptions.newBuilder() + .iterationLimit(100) + .enableLinkedMessageTypes(false) + .build(); + + SelectOptimizerOptions copiedOptions = options.toBuilder().iterationLimit(200).build(); + + assertThat(copiedOptions.iterationLimit()).isEqualTo(200); + assertThat(copiedOptions.enableLinkedMessageTypes()).isFalse(); + } + + @Test + public void optimize_iterationLimitReached_throws() throws Exception { + CelAbstractSyntaxTree ast = + cel.compile("msg.single_int64 + msg.single_int64 + msg.single_int64").getAst(); + SelectOptimizer optimizer = + SelectOptimizer.newInstance( + SelectOptimizerOptions.newBuilder().iterationLimit(2).build(), + TestAllTypes.getDescriptor().getFile()); + + IllegalStateException e = + assertThrows(IllegalStateException.class, () -> optimizer.optimize(ast, cel)); + + assertThat(e).hasMessageThat().isEqualTo("Max iteration count reached."); + } + + @Test + public void optimize_structField_throwsUnsupportedOperationException() throws Exception { + CelAbstractSyntaxTree ast = cel.compile("msg.single_struct").getAst(); + SelectOptimizer optimizer = SelectOptimizer.newInstance(TestAllTypes.getDescriptor().getFile()); + + UnsupportedOperationException e = + assertThrows(UnsupportedOperationException.class, () -> optimizer.optimize(ast, cel)); + + assertThat(e) + .hasMessageThat() + .contains("Optimization of Struct fields is currently unimplemented"); + } + + @Test + public void optimize_listValueField_throwsUnsupportedOperationException() throws Exception { + CelAbstractSyntaxTree ast = cel.compile("msg.list_value").getAst(); + SelectOptimizer optimizer = SelectOptimizer.newInstance(TestAllTypes.getDescriptor().getFile()); + + UnsupportedOperationException e = + assertThrows(UnsupportedOperationException.class, () -> optimizer.optimize(ast, cel)); + + assertThat(e) + .hasMessageThat() + .contains("Optimization of ListValue fields is currently unimplemented"); + } + + @Test + public void optimize_valueField_throwsUnsupportedOperationException() throws Exception { + CelAbstractSyntaxTree ast = cel.compile("msg.single_value").getAst(); + SelectOptimizer optimizer = SelectOptimizer.newInstance(TestAllTypes.getDescriptor().getFile()); + + UnsupportedOperationException e = + assertThrows(UnsupportedOperationException.class, () -> optimizer.optimize(ast, cel)); + + assertThat(e) + .hasMessageThat() + .contains("Optimization of Value fields is currently unimplemented"); + } + + @Test + public void optimize_anyField_throwsUnsupportedOperationException() throws Exception { + CelAbstractSyntaxTree ast = cel.compile("msg.single_any").getAst(); + SelectOptimizer optimizer = SelectOptimizer.newInstance(TestAllTypes.getDescriptor().getFile()); + + UnsupportedOperationException e = + assertThrows(UnsupportedOperationException.class, () -> optimizer.optimize(ast, cel)); + + assertThat(e) + .hasMessageThat() + .contains("Optimization of Any fields is currently unimplemented"); + } + + @Test + public void optimize_wrapperField_throwsUnsupportedOperationException() throws Exception { + CelAbstractSyntaxTree ast = cel.compile("msg.single_int64_wrapper").getAst(); + SelectOptimizer optimizer = SelectOptimizer.newInstance(TestAllTypes.getDescriptor().getFile()); + + UnsupportedOperationException e = + assertThrows(UnsupportedOperationException.class, () -> optimizer.optimize(ast, cel)); + + assertThat(e) + .hasMessageThat() + .contains("Optimization of wrapper fields is currently unimplemented"); + } + + @Test + public void optimize_groupField_throwsUnsupportedOperationException() throws Exception { + CelAbstractSyntaxTree ast = cel.compile("proto2_msg.nestedgroup.single_id").getAst(); + SelectOptimizer optimizer = + SelectOptimizer.newInstance(PROTO2_TEST_ALL_TYPES_DESCRIPTOR.getFile()); + + UnsupportedOperationException e = + assertThrows(UnsupportedOperationException.class, () -> optimizer.optimize(ast, cel)); + + assertThat(e).hasMessageThat().contains("Optimization of Group fields is unsupported"); + } + + @Test + public void newInstance_fileDescriptorsVarargs_defaultOptions_success() throws Exception { + FileDescriptor fd = PROTO2_TEST_ALL_TYPES_DESCRIPTOR.getFile(); + SelectOptimizer optimizer = SelectOptimizer.newInstance(fd); + CelAbstractSyntaxTree ast = cel.compile("proto2_msg.single_int64").getAst(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast, cel).optimizedAst(); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]])"); + } + + @Test + public void newInstance_fileDescriptorsIterable_defaultOptions_success() throws Exception { + FileDescriptor fd = PROTO2_TEST_ALL_TYPES_DESCRIPTOR.getFile(); + SelectOptimizer optimizer = SelectOptimizer.newInstance(ImmutableList.of(fd)); + CelAbstractSyntaxTree ast = cel.compile("proto2_msg.single_int64").getAst(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast, cel).optimizedAst(); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]])"); + } + + @Test + public void + newInstance_addFileDescriptorsVarargs_withLinkedDescriptorsDisabled_doesNotOptimizeUnprovidedDescriptors() + throws Exception { + FileDescriptor fd = PROTO2_TEST_ALL_TYPES_DESCRIPTOR.getFile(); + SelectOptimizer optimizer = + SelectOptimizer.newInstance( + SelectOptimizerOptions.newBuilder().enableLinkedMessageTypes(false).build(), fd); + CelAbstractSyntaxTree proto2Ast = cel.compile("proto2_msg.single_int64").getAst(); + CelAbstractSyntaxTree proto3Ast = cel.compile("msg.single_int64").getAst(); + + CelAbstractSyntaxTree proto2Optimized = optimizer.optimize(proto2Ast, cel).optimizedAst(); + CelAbstractSyntaxTree proto3Optimized = optimizer.optimize(proto3Ast, cel).optimizedAst(); + + assertThat(CEL_UNPARSER.unparse(proto2Optimized)) + .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]])"); + assertThat(CEL_UNPARSER.unparse(proto3Optimized)).isEqualTo("msg.single_int64"); + } + + @Test + public void + newInstance_addFileDescriptorsIterable_withLinkedDescriptorsDisabled_doesNotOptimizeUnprovidedDescriptors() + throws Exception { + FileDescriptor fd = PROTO2_TEST_ALL_TYPES_DESCRIPTOR.getFile(); + SelectOptimizer optimizer = + SelectOptimizer.newInstance( + SelectOptimizerOptions.newBuilder().enableLinkedMessageTypes(false).build(), + ImmutableList.of(fd)); + CelAbstractSyntaxTree proto2Ast = cel.compile("proto2_msg.single_int64").getAst(); + CelAbstractSyntaxTree proto3Ast = cel.compile("msg.single_int64").getAst(); + + CelAbstractSyntaxTree proto2Optimized = optimizer.optimize(proto2Ast, cel).optimizedAst(); + CelAbstractSyntaxTree proto3Optimized = optimizer.optimize(proto3Ast, cel).optimizedAst(); + + assertThat(CEL_UNPARSER.unparse(proto2Optimized)) + .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]])"); + assertThat(CEL_UNPARSER.unparse(proto3Optimized)).isEqualTo("msg.single_int64"); + } + + private enum CompilerRejectionTestCase { + ATTRIBUTE_AT_SIGN( + SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL, + "cel.@attribute(msg, [])", + "token recognition error at: '@'"), + ATTRIBUTE_OVERLOAD( + SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL, + "cel_attribute_list(msg, [])", + "undeclared reference to 'cel_attribute_list'"), + HAS_FIELD_AT_SIGN( + SelectOptimizer.CEL_HAS_FIELD_FUNCTION_DECL, + "cel.@hasField(msg, [])", + "token recognition error at: '@'"), + HAS_FIELD_OVERLOAD( + SelectOptimizer.CEL_HAS_FIELD_FUNCTION_DECL, + "cel_has_field_list(msg, [])", + "undeclared reference to 'cel_has_field_list'"); + + private final CelFunctionDecl functionDecl; + private final String expression; + private final String expectedErrorMessage; + + CompilerRejectionTestCase( + CelFunctionDecl functionDecl, String expression, String expectedErrorMessage) { + this.functionDecl = functionDecl; + this.expression = expression; + this.expectedErrorMessage = expectedErrorMessage; + } + } + + @Test + public void compile_sourceWithInternalFunctionCall_failsCompilation( + @TestParameter CompilerRejectionTestCase testCase) { + Cel celWithDecl = cel.toCelBuilder().addFunctionDeclarations(testCase.functionDecl).build(); + + CelValidationException e = + assertThrows( + CelValidationException.class, () -> celWithDecl.compile(testCase.expression).getAst()); + + assertThat(e).hasMessageThat().contains(testCase.expectedErrorMessage); + } + + @Test + public void optimize_toParsedExpr_matchesExpectedSerializedProto() throws Exception { + CelAbstractSyntaxTree ast = cel.compile("msg.single_nested_message.bb").getAst(); + ParsedExpr expectedParsedExpr = + TextFormat.parse( + "expr {\n" + + " id: 1\n" + + " call_expr {\n" + + " function: \"cel.@attribute\"\n" + + " args {\n" + + " id: 2\n" + + " ident_expr {\n" + + " name: \"msg\"\n" + + " }\n" + + " }\n" + + " args {\n" + + " id: 3\n" + + " list_expr {\n" + + " elements {\n" + + " id: 4\n" + + " list_expr {\n" + + " elements {\n" + + " id: 5\n" + + " const_expr {\n" + + " int64_value: 21\n" + + " }\n" + + " }\n" + + " elements {\n" + + " id: 6\n" + + " const_expr {\n" + + " string_value: \"single_nested_message\"\n" + + " }\n" + + " }\n" + + " elements {\n" + + " id: 7\n" + + " const_expr {\n" + + " int64_value: 11\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " elements {\n" + + " id: 8\n" + + " list_expr {\n" + + " elements {\n" + + " id: 9\n" + + " const_expr {\n" + + " int64_value: 1\n" + + " }\n" + + " }\n" + + " elements {\n" + + " id: 10\n" + + " const_expr {\n" + + " string_value: \"bb\"\n" + + " }\n" + + " }\n" + + " elements {\n" + + " id: 11\n" + + " const_expr {\n" + + " int64_value: 5\n" + + " }\n" + + " }\n" + + " elements {\n" + + " id: 12\n" + + " const_expr {\n" + + " int64_value: 0\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}\n" + + "source_info {\n" + + " location: \"\"\n" + + " extensions {\n" + + " id: \"select_optimization\"\n" + + " affected_components: COMPONENT_RUNTIME\n" + + " version {\n" + + " major: 1\n" + + " }\n" + + " }\n" + + "}\n", + ParsedExpr.class); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + ParsedExpr parsedExpr = CelProtoAbstractSyntaxTree.fromCelAst(optimizedAst).toParsedExpr(); + + assertThat(parsedExpr).isEqualTo(expectedParsedExpr); + } +} From fbe7f97307ca08dd0e9f16bb1e8b2eb87b70b6a9 Mon Sep 17 00:00:00 2001 From: Cristina Borza Date: Thu, 10 Sep 2026 01:54:05 -0700 Subject: [PATCH 189/204] Fix a bug in the `assertAstIdCorrectness` method and avoid re-checking the AST in `CelOptimizer` if it was not modified. PiperOrigin-RevId: 979038969 --- .../dev/cel/optimizer/CelOptimizerImpl.java | 51 ++++++++++------ .../SubexpressionOptimizerTest.java | 60 +++++++++++++++++++ 2 files changed, 92 insertions(+), 19 deletions(-) diff --git a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java index 0d2f151c5..7e14a8dfc 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java +++ b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java @@ -15,6 +15,7 @@ package dev.cel.optimizer; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import com.google.common.collect.ImmutableSet; import dev.cel.bundle.Cel; @@ -44,6 +45,9 @@ final class CelOptimizerImpl implements CelOptimizer { } @Override + // AstOptimizers return the same AST instance if no changes are made. Using != avoids deep + // .equals() comparison. + @SuppressWarnings("ReferenceEquality") public CelAbstractSyntaxTree optimize(CelAbstractSyntaxTree ast) throws CelOptimizationException { if (!ast.isChecked()) { throw new IllegalArgumentException("AST must be type-checked."); @@ -64,16 +68,18 @@ public CelAbstractSyntaxTree optimize(CelAbstractSyntaxTree ast) throws CelOptim OptimizationResult result = optimizer.optimize(optimizedAst, celOptimizerEnv); - if (!result.newFunctionDecls().isEmpty() || !result.newVarDecls().isEmpty()) { - celOptimizerEnv = - celOptimizerEnv - .toCelBuilder() - .addVarDeclarations(result.newVarDecls()) - .addFunctionDeclarations(result.newFunctionDecls()) - .build(); + if (result.optimizedAst() != optimizedAst) { + if (!result.newFunctionDecls().isEmpty() || !result.newVarDecls().isEmpty()) { + celOptimizerEnv = + celOptimizerEnv + .toCelBuilder() + .addVarDeclarations(result.newVarDecls()) + .addFunctionDeclarations(result.newFunctionDecls()) + .build(); + } + optimizedAst = celOptimizerEnv.check(result.optimizedAst()).getAst(); + assertAstIdCorrectness(optimizedAst); } - optimizedAst = celOptimizerEnv.check(result.optimizedAst()).getAst(); - assertAstIdCorrectness(optimizedAst); for (CelOptimizerListener listener : listeners) { listener.onPassEnd(optimizer, preAst, optimizedAst); @@ -131,19 +137,26 @@ private static void assertAstIdCorrectness(CelAbstractSyntaxTree ast) { return; } - if (astExpr.exprKind().getKind().equals(Kind.COMPREHENSION)) { - if (!macroExpr.exprKind().getKind().equals(Kind.NOT_SET)) { - throw new IllegalStateException( - String.format( - "Expected macro call node %d to be NOT_SET for comprehension, but" - + " was %s.", - macroExpr.id(), macroExpr.exprKind().getKind())); - } + if (macroExpr.exprKind().getKind().equals(Kind.NOT_SET)) { + // If a macro node is NOT_SET, its ID must be present in the main AST. + checkState( + ast.getSource().getMacroCalls().containsKey(macroExpr.id()), + "Expected macro call node %s to be present in macro calls map, but was not.", + macroExpr.id()); + } else if (astExpr.exprKind().getKind().equals(Kind.COMPREHENSION)) { + // We encountered something other than NOT_SET in macro source for comprehension + // node. This is an error. + throw new IllegalStateException( + String.format( + "Expected macro call node %d to be NOT_SET for comprehension, but was" + + " %s.", + macroExpr.id(), macroExpr.exprKind().getKind())); } else if (!macroExpr.exprKind().getKind().equals(astExpr.exprKind().getKind())) { + // Otherwise for all cases, the AST node should match exactly. throw new IllegalStateException( String.format( - "Macro call node %d kind mismatch: expected %s (from AST), but was %s" - + " (in macro call).", + "Macro call node %d kind mismatch: expected %s (from AST), but was %s (in" + + " macro call).", macroExpr.id(), astExpr.exprKind().getKind(), macroExpr.exprKind().getKind())); diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java index 1a36bd16b..209dba3a5 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SubexpressionOptimizerTest.java @@ -702,6 +702,66 @@ public void block_lazyEvaluationContainsError_cleansUpCycleState() throws Except assertThat(e).hasMessageThat().doesNotContain("Cycle detected"); } + @Test + public void cse_nestedMacro_noOp_assertAstIdCorrectness() throws Exception { + Cel cel = + runtimeFlavor + .builder() + .addVar("x", SimpleType.DYN) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions(CelOptions.current().populateMacroCalls(true).build()) + .addCompilerLibraries(CelExtensions.comprehensions()) + .addRuntimeLibraries(CelExtensions.comprehensions()) + .build(); + CelOptimizer celOptimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(cel) + .addAstOptimizers(SubexpressionOptimizer.getInstance()) + .build(); + CelAbstractSyntaxTree ast = + cel.compile("[{}, {\"a\": 1}, {\"b\": 2}].filter(m, has(x.a))").getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo("[{}, {\"a\": 1}, {\"b\": 2}].filter(m, has(x.a))"); + assertThat(optimizedAst).isSameInstanceAs(ast); + } + + @Test + public void cse_nestedMacro_withOptimization_assertAstIdCorrectness() throws Exception { + Cel cel = + runtimeFlavor + .builder() + .addVar("x", SimpleType.DYN) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions( + CelOptions.current() + .populateMacroCalls(true) + .enableHeterogeneousNumericComparisons(true) + .build()) + .addCompilerLibraries(CelExtensions.comprehensions()) + .addRuntimeLibraries(CelExtensions.comprehensions()) + .build(); + CelOptimizer celOptimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(cel) + .addAstOptimizers( + SubexpressionOptimizer.newInstance( + SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build())) + .build(); + CelAbstractSyntaxTree ast = + cel.compile( + "[{}, {\"a\": 1}, {\"b\": 2}].filter(m, has(x.a)) == [{}, {\"a\": 1}, {\"b\":" + + " 2}].filter(m, has(x.a))") + .getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo( + "cel.@block([[{}, {\"a\": 1}, {\"b\": 2}].filter(@it:0:0, has(x.a))], @index0 ==" + + " @index0)"); + } + /** * Converts AST containing cel.block related test functions to internal functions (e.g: cel.block * -> cel.@block) From e955f8e2f4c9ce7478ba22879dd4e85831c621e0 Mon Sep 17 00:00:00 2001 From: Cristina Borza Date: Thu, 10 Sep 2026 04:50:25 -0700 Subject: [PATCH 190/204] Avoid unnecessary AST renumbering in CEL optimizers when no changes are made. PiperOrigin-RevId: 979113076 --- .../optimizers/ConstantFoldingOptimizer.java | 29 +++++++++++++++---- .../optimizers/InliningOptimizer.java | 10 ++++++- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java index 181cc4f75..aa724fdb6 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java @@ -121,6 +121,9 @@ private static CelMutableExpr newOptionalNoneExpr() { } @Override + // Internal optimization steps preserve the initial mutable AST instance if no mutations occur. + // Using == avoids deep .equals() comparison. + @SuppressWarnings("ReferenceEquality") public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) throws CelOptimizationException { CelBuilder builder = cel.toCelBuilder(); @@ -134,12 +137,17 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) // Override the environment's expected type to generally allow all subtrees to be folded. Cel optimizerEnv = builder.setResultType(SimpleType.DYN).build(); - CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); - ImmutableMap identTypes = precomputeIdentTypes(mutableAst); + CelMutableAst initialMutableAst = CelMutableAst.fromCelAst(ast); + ImmutableMap identTypes = precomputeIdentTypes(initialMutableAst); - mutableAst = foldConstants(optimizerEnv, valueProvider, identTypes, mutableAst); + CelMutableAst mutableAst = + foldConstants(optimizerEnv, valueProvider, identTypes, initialMutableAst); mutableAst = pruneOptionalElements(mutableAst); + if (mutableAst == initialMutableAst) { + return OptimizationResult.create(ast); + } + return OptimizationResult.create(astMutator.renumberIdsConsecutively(mutableAst).toParsedAst()); } @@ -735,10 +743,21 @@ private CelMutableAst pruneOptionalListElements(CelMutableAst mutableAst, CelMut updatedIndicesBuilder.add(newOptIndex); } + // An optional list is modified if: + // 1. An optional.none() was dropped - it this case, the updatedElements.size() decreases. + // 2. An optional.of(literal) was unwrapped into a regular element - in this case, + // updatedIndices.size() decreases. + // If both counts are unchanged, neither case occurred, and we can return the original AST. + ImmutableList updatedElements = updatedElemBuilder.build(); + ImmutableList updatedIndices = updatedIndicesBuilder.build(); + if (updatedElements.size() == list.elements().size() + && updatedIndices.size() == list.optionalIndices().size()) { + return mutableAst; + } + return astMutator.replaceSubtree( mutableAst, - CelMutableExpr.ofList( - CelMutableList.create(updatedElemBuilder.build(), updatedIndicesBuilder.build())), + CelMutableExpr.ofList(CelMutableList.create(updatedElements, updatedIndices)), expr.id()); } diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/InliningOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/InliningOptimizer.java index 61fd19347..696b6749b 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/InliningOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/InliningOptimizer.java @@ -101,8 +101,12 @@ public static InliningOptimizer newInstance( } @Override + // Internal optimization steps preserve the initial mutable AST instance if no mutations occur. + // Using == avoids deep .equals() comparison. + @SuppressWarnings("ReferenceEquality") public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) { - CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); + CelMutableAst initialMutableAst = CelMutableAst.fromCelAst(ast); + CelMutableAst mutableAst = initialMutableAst; for (InlineVariable inlineVariable : inlineVariables) { mutableAst = astMutator.mutateUntilFixedPoint( @@ -125,6 +129,10 @@ public OptimizationResult optimize(CelAbstractSyntaxTree ast, Cel cel) { }); } + if (mutableAst == initialMutableAst) { + return OptimizationResult.create(ast); + } + return OptimizationResult.create(astMutator.renumberIdsConsecutively(mutableAst).toParsedAst()); } From d88ea568ab403b7c93e3a51e668d9c293f91a677 Mon Sep 17 00:00:00 2001 From: Kat Lai Date: Thu, 10 Sep 2026 06:02:00 -0700 Subject: [PATCH 191/204] Fix CEL Java sortBy macro expansion to preserve element type and avoid heterogeneous list literals. PiperOrigin-RevId: 979138470 --- .../main/java/dev/cel/extensions/BUILD.bazel | 2 + .../cel/extensions/CelListsExtensions.java | 198 ++++++++++++++---- .../test/java/dev/cel/extensions/BUILD.bazel | 4 +- .../dev/cel/extensions/CelExtensionsTest.java | 2 +- .../extensions/CelListsExtensionsTest.java | 66 ++++-- 5 files changed, 211 insertions(+), 61 deletions(-) diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel index 8b7991cc0..554aadbde 100644 --- a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel @@ -274,6 +274,8 @@ java_library( "//common/ast", "//common/internal:comparison_functions", "//common/types", + "//common/types:type_providers", + "//common/values:cel_byte_string", "//compiler:compiler_builder", "//extensions:extension_library", "//parser:macro", diff --git a/extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java index 79539b008..91e6f8dc8 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.ImmutableSet.toImmutableSet; +import com.google.common.base.Ascii; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -31,9 +32,11 @@ import dev.cel.common.Operator; import dev.cel.common.ast.CelExpr; import dev.cel.common.internal.ComparisonFunctions; +import dev.cel.common.types.CelType; import dev.cel.common.types.ListType; import dev.cel.common.types.SimpleType; import dev.cel.common.types.TypeParamType; +import dev.cel.common.values.CelByteString; import dev.cel.compiler.CelCompilerLibrary; import dev.cel.parser.CelMacro; import dev.cel.parser.CelMacroExprFactory; @@ -54,6 +57,10 @@ public final class CelListsExtensions implements CelCompilerLibrary, CelInternalRuntimeLibrary, CelExtensionLibrary.FeatureSet { + private static final CelObjectComparator OBJECT_COMPARATOR = new CelObjectComparator(); + private static final String UNUSED_ITER_VAR = "#unused"; + private static final String SORT_BY_INPUT_VAR = "@__sortBy_input__"; + /** Supported functions for Lists extension library. */ @SuppressWarnings({"unchecked"}) // Unchecked: Type-checker guarantees casting safety. public enum Function { @@ -131,17 +138,50 @@ public enum Function { ListType.create(TypeParamType.create("T")))), CelFunctionBinding.from("list_sort", Collection.class, CelListsExtensions::sort)), SORT_BY( - CelFunctionDecl.newFunctionDeclaration( - "lists.@sortByAssociatedKeys", - CelOverloadDecl.newGlobalOverload( - "list_sortByAssociatedKeys", - "Sorts a list by a key value. Used by the 'sortBy' macro", + createSortByFunctionDecl(comparableSortKeyTypes()), + createSortByFunctionBindings(comparableSortKeyTypes())); + + private static ImmutableList comparableSortKeyTypes() { + return ImmutableList.of( + SimpleType.INT, + SimpleType.UINT, + SimpleType.DOUBLE, + SimpleType.BOOL, + SimpleType.STRING, + SimpleType.BYTES, + SimpleType.DURATION, + SimpleType.TIMESTAMP); + } + + private static CelFunctionDecl createSortByFunctionDecl(ImmutableList keyTypes) { + ImmutableList.Builder overloads = ImmutableList.builder(); + for (CelType type : keyTypes) { + String typeName = Ascii.toLowerCase(type.kind().name()); + overloads.add( + CelOverloadDecl.newMemberOverload( + String.format("list_%s_sortByAssociatedKeys", typeName), + "Sorts a list by an associated list of keys. Used by the 'sortBy' macro", ListType.create(TypeParamType.create("T")), - ListType.create(TypeParamType.create("T")))), - CelFunctionBinding.from( - "list_sortByAssociatedKeys", - Collection.class, - CelListsExtensions::sortByAssociatedKeys)); + ListType.create(TypeParamType.create("T")), + ListType.create(type))); + } + return CelFunctionDecl.newFunctionDeclaration("@sortByAssociatedKeys", overloads.build()); + } + + private static CelFunctionBinding[] createSortByFunctionBindings( + ImmutableList keyTypes) { + return keyTypes.stream() + .map( + type -> { + String typeName = Ascii.toLowerCase(type.kind().name()); + return CelFunctionBinding.from( + String.format("list_%s_sortByAssociatedKeys", typeName), + Collection.class, + Collection.class, + CelListsExtensions::sortByAssociatedKeys); + }) + .toArray(CelFunctionBinding[]::new); + } private final CelFunctionDecl functionDecl; private final ImmutableSet functionBindings; @@ -359,7 +399,15 @@ private static List reverse(Collection list) { } private static ImmutableList sort(Collection objects) { - return ImmutableList.sortedCopyOf(new CelObjectComparator(), objects); + if (objects.isEmpty()) { + return ImmutableList.of(); + } + if (objects.size() == 1) { + Object single = objects.iterator().next(); + OBJECT_COMPARATOR.compare(single, single); + return ImmutableList.of(single); + } + return ImmutableList.sortedCopyOf(OBJECT_COMPARATOR, objects); } private static class CelObjectComparator implements Comparator { @@ -372,6 +420,10 @@ public int compare(Object o1, Object o2) { if (o1 instanceof Number && o2 instanceof Number) { return ComparisonFunctions.numericCompare((Number) o1, (Number) o2); } + if (o1 instanceof CelByteString && o2 instanceof CelByteString) { + return CelByteString.unsignedLexicographicalComparator() + .compare((CelByteString) o1, (CelByteString) o2); + } if (!(o1 instanceof Comparable)) { throw new IllegalArgumentException("List elements must be comparable"); @@ -383,6 +435,34 @@ public int compare(Object o1, Object o2) { } } + /** + * Expands the {@code list.sortBy(var, expr)} receiver macro into a binding expression that sorts + * the target list using keys evaluated by mapping {@code expr} over each element. + * + *

For example, given: + * + *

{@code
+   * myList.sortBy(item, -item.field)
+   * }
+ * + *

The macro expands into: + * + *

{@code
+   * cel.bind(@__sortBy_input__, myList,
+   *     @__sortBy_input__.@sortByAssociatedKeys(
+   *         @__sortBy_input__.map(item, -item.field)
+   *     )
+   * )
+   * }
+ * + *

Where: + * + *

    + *
  • {@code @__sortBy_input__.map(item, -item.field)} evaluates the sort key for each element. + *
  • {@code @sortByAssociatedKeys} stably sorts the input list elements based on their + * corresponding sort keys. + *
+ */ private static Optional sortByMacro( CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) { checkNotNull(exprFactory); @@ -400,56 +480,86 @@ private static Optional sortByMacro( String varName = varIdent.ident().name(); CelExpr sortKeyExpr = checkNotNull(arguments.get(1)); - // Compute the key using the second argument of the `sortBy(e, key)` macro. - // Combine the key and the value in a two-element list - CelExpr step = exprFactory.newList(sortKeyExpr, varIdent); - // Wrap the pair in another list in order to be able to use the `list+list` operator - step = exprFactory.newList(step); - // Append the key-value pair to the i - step = + // Build map comprehension: @__sortBy_input__.map(varName, sortKeyExpr) + CelExpr targetIdent = exprFactory.newIdentifier(SORT_BY_INPUT_VAR); + CelExpr mapStep = exprFactory.newGlobalCall( Operator.ADD.getFunction(), exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()), - step); - // Create an intermediate list and populate it with key-value pairs - step = + exprFactory.newList(sortKeyExpr)); + CelExpr mapCompr = exprFactory.fold( varName, - target, + targetIdent, exprFactory.getAccumulatorVarName(), exprFactory.newList(), - exprFactory.newBoolLiteral(true), // Include all elements - step, + exprFactory.newBoolLiteral(true), + mapStep, exprFactory.newIdentifier(exprFactory.getAccumulatorVarName())); - // Finally, sort the list of key-value pairs and map it to a list of values - step = exprFactory.newGlobalCall(Function.SORT_BY.getFunction(), step); - return Optional.of(step); + // Build call: @__sortBy_input__.@sortByAssociatedKeys(mapCompr) + CelExpr callExpr = + exprFactory.newReceiverCall( + Function.SORT_BY.getFunction(), exprFactory.newIdentifier(SORT_BY_INPUT_VAR), mapCompr); + + // Build bind: cel.bind(@__sortBy_input__, target, callExpr) + CelExpr bindExpr = + exprFactory.fold( + UNUSED_ITER_VAR, + exprFactory.newList(), + SORT_BY_INPUT_VAR, + target, + exprFactory.newBoolLiteral(false), + exprFactory.newIdentifier(SORT_BY_INPUT_VAR), + callExpr); + + return Optional.of(bindExpr); } - @SuppressWarnings({"unchecked", "rawtypes"}) + /** + * Sorts elements of {@code list} based on the natural order of corresponding elements in {@code + * keys}. + * + *

Both {@code list} and {@code keys} must have the exact same size. The sorting is stable + * (i.e., preserves the relative order of elements with equal keys). + * + * @param list The input list to sort + * @param keys The associated keys evaluated for each element in {@code list} + * @return A new {@link ImmutableList} containing the elements of {@code list} sorted by {@code + * keys} + */ private static ImmutableList sortByAssociatedKeys( - Collection> keyValuePairs) { - List[] array = keyValuePairs.toArray(new List[0]); - Arrays.sort(array, new CelObjectByKeyComparator(new CelObjectComparator())); - ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(array.length); - for (List pair : array) { - builder.add(pair.get(1)); + Collection list, Collection keys) { + checkArgument( + list.size() == keys.size(), + "@sortByAssociatedKeys() expected a list of the same size as the associated keys" + + " list, but got %s in list and %s in keys", + list.size(), + keys.size()); + + int listSize = list.size(); + if (listSize == 0) { + return ImmutableList.of(); } - return builder.build(); - } - private static class CelObjectByKeyComparator implements Comparator { - private final CelObjectComparator keyComparator; + Object[] listArray = list.toArray(); + Object[] keysArray = keys.toArray(); + if (listSize == 1) { + OBJECT_COMPARATOR.compare(keysArray[0], keysArray[0]); + return ImmutableList.of(listArray[0]); + } - CelObjectByKeyComparator(CelObjectComparator keyComparator) { - this.keyComparator = keyComparator; + Integer[] indices = new Integer[listSize]; + for (int i = 0; i < listSize; i++) { + indices[i] = i; } - @SuppressWarnings({"unchecked"}) - @Override - public int compare(Object o1, Object o2) { - return keyComparator.compare(((List) o1).get(0), ((List) o2).get(0)); + Arrays.sort(indices, (i1, i2) -> OBJECT_COMPARATOR.compare(keysArray[i1], keysArray[i2])); + + ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(listSize); + for (int index : indices) { + builder.add(listArray[index]); } + return builder.build(); } } diff --git a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel index 920ba537b..f7b996610 100644 --- a/extensions/src/test/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel @@ -42,12 +42,14 @@ java_library( "//parser:unparser", "//runtime", "//runtime:function_binding", - "//runtime:interpreter_util", "//runtime:lite_runtime", "//runtime:lite_runtime_factory", "//runtime:partial_vars", "//runtime:unknown_attributes", "//testing:cel_runtime_flavor", + "//validator", + "//validator:validator_builder", + "//validator/validators:homogeneous_literal", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/test:simple_java_proto", diff --git a/extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java index 31c7d65c8..279ad7013 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java @@ -185,7 +185,7 @@ public void getAllFunctionNames() { "distinct", "reverse", "sort", - "lists.@sortByAssociatedKeys", + "@sortByAssociatedKeys", "regex.replace", "regex.extract", "regex.extractAll", diff --git a/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java index 4520f81ba..1f893c7ba 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java @@ -22,6 +22,7 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.bundle.Cel; +import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelValidationException; import dev.cel.common.CelValidationResult; @@ -30,6 +31,9 @@ import dev.cel.parser.CelStandardMacro; import dev.cel.runtime.CelEvaluationException; import dev.cel.testing.CelRuntimeFlavor; +import dev.cel.validator.CelValidator; +import dev.cel.validator.CelValidatorFactory; +import dev.cel.validator.validators.HomogeneousLiteralValidator; import org.junit.Assume; import org.junit.Test; import org.junit.runner.RunWith; @@ -64,7 +68,7 @@ public void functionList_byVersion() { "distinct", "reverse", "sort", - "lists.@sortByAssociatedKeys"); + "@sortByAssociatedKeys"); } @Test @@ -234,6 +238,14 @@ public void reverse_success(String expression, String expected) throws Exception @TestParameters( "{expression: '[\"d\", \"a\", \"b\", \"c\"].sort()', " + "expected: '[\"a\", \"b\", \"c\", \"d\"]'}") + @TestParameters("{expression: '[b\"b\", b\"a\"].sort()', " + "expected: '[b\"a\", b\"b\"]'}") + @TestParameters( + "{expression: '[duration(\"2s\"), duration(\"1s\")].sort()', " + + "expected: '[duration(\"1s\"), duration(\"2s\")]'}") + @TestParameters( + "{expression: '[timestamp(\"2026-01-01T00:00:00Z\")," + + " timestamp(\"2025-01-01T00:00:00Z\")].sort()', expected:" + + " '[timestamp(\"2025-01-01T00:00:00Z\"), timestamp(\"2026-01-01T00:00:00Z\")]'}") public void sort_success(String expression, String expected) throws Exception { Object result = eval(cel, expression); @@ -257,6 +269,9 @@ public void sort_success_heterogeneousNumbers(String expression, String expected @TestParameters( "{expression: '[SimpleTest{name: \"a\"}, SimpleTest{name: \"b\"}].sort()', " + "expectedError: 'List elements must be comparable'}") + @TestParameters( + "{expression: '[SimpleTest{name: \"a\"}].sort()', " + + "expectedError: 'List elements must be comparable'}") public void sort_throws(String expression, String expectedError) throws Exception { assertThat(assertThrows(CelEvaluationException.class, () -> eval(cel, expression))) .hasCauseThat() @@ -276,6 +291,15 @@ public void sort_throws(String expression, String expectedError) throws Exceptio @TestParameters( "{expression: '[\"a\", \"c\", \"b\", \"first\"].sortBy(e, e == \"first\" ? \"\" : e)', " + "expected: '[\"first\", \"a\", \"b\", \"c\"]'}") + @TestParameters( + "{expression: '[b\"b\", b\"a\"].sortBy(e, e)', " + "expected: '[b\"a\", b\"b\"]'}") + @TestParameters( + "{expression: '[duration(\"2s\"), duration(\"1s\")].sortBy(e, e)', " + + "expected: '[duration(\"1s\"), duration(\"2s\")]'}") + @TestParameters( + "{expression: '[timestamp(\"2026-01-01T00:00:00Z\")," + + " timestamp(\"2025-01-01T00:00:00Z\")].sortBy(e, e)', expected:" + + " '[timestamp(\"2025-01-01T00:00:00Z\"), timestamp(\"2026-01-01T00:00:00Z\")]'}") @TestParameters( "{expression: '[SimpleTest{name: \"baz\"}," + " SimpleTest{name: \"foo\"}," @@ -283,6 +307,11 @@ public void sort_throws(String expression, String expectedError) throws Exceptio + "expected: '[SimpleTest{name: \"bar\"}," + " SimpleTest{name: \"baz\"}," + " SimpleTest{name: \"foo\"}]'}") + @TestParameters( + "{expression: '[SimpleTest{name: \"baz\"}," + + " SimpleTest{name: \"foo\"}," + + " SimpleTest{name: \"bar\"}].sortBy(e, e.name)[0].name', " + + "expected: '\"bar\"'}") public void sortBy_success(String expression, String expected) throws Exception { Object result = eval(cel, expression); @@ -296,6 +325,12 @@ public void sortBy_success(String expression, String expected) throws Exception @TestParameters( "{expression: 'lists.range(3).sortBy(e.foo, e)', " + "expectedError: 'variable name must be a simple identifier'}") + @TestParameters( + "{expression: '[SimpleTest{name: \"a\"}].sortBy(e, e)', " + + "expectedError: 'found no matching overload for ''@sortByAssociatedKeys'''}") + @TestParameters( + "{expression: '[SimpleTest{name: \"a\"}, SimpleTest{name: \"b\"}].sortBy(e, e)', " + + "expectedError: 'found no matching overload for ''@sortByAssociatedKeys'''}") public void sortBy_throws_validationException(String expression, String expectedError) throws Exception { CelValidationResult result = cel.compile(expression); @@ -305,19 +340,20 @@ public void sortBy_throws_validationException(String expression, String expected } @Test - @TestParameters( - "{expression: '[[1, 2], [\"a\", \"b\"]].sortBy(e, e[0])', " - + "expectedError: 'List elements must have the same type'}") - @TestParameters( - "{expression: '[SimpleTest{name: \"a\"}, SimpleTest{name: \"b\"}].sortBy(e, e)', " - + "expectedError: 'List elements must be comparable'}") - public void sortBy_throws_evaluationException(String expression, String expectedError) - throws Exception { - assertThat(assertThrows(CelEvaluationException.class, () -> eval(cel, expression))) - .hasCauseThat() - .hasMessageThat() - .contains(expectedError); + public void sortBy_withHomogeneousLiteralValidator_success() throws Exception { + CelValidator validator = + CelValidatorFactory.standardCelValidatorBuilder(cel) + .addAstValidators(HomogeneousLiteralValidator.newInstance()) + .build(); + + CelAbstractSyntaxTree ast = + cel.compile( + "[SimpleTest{name: 'baz'}, SimpleTest{name: 'foo'}, SimpleTest{name: 'bar'}]" + + ".sortBy(e, e.name)[0].name") + .getAst(); + CelValidationResult result = validator.validate(ast); + + assertThat(result.hasError()).isFalse(); + assertThat(cel.createProgram(ast).eval()).isEqualTo("bar"); } - - } From 2db56dd94c4106207a906b67786af6a8d907e8d8 Mon Sep 17 00:00:00 2001 From: Muhammad Askri Date: Thu, 10 Sep 2026 11:37:46 -0700 Subject: [PATCH 192/204] Introduce cel_verifier_test Bazel macro for CEL verification. PiperOrigin-RevId: 979301965 --- verifier/tools/BUILD.bazel | 62 ++++++++ verifier/tools/cel_verifier.bzl | 154 +++++++++++++++++++ verifier/tools/run_verifier.sh | 35 +++++ verifier/tools/testdata/simple_policy.yaml | 11 ++ verifier/tools/testdata/violated_policy.yaml | 11 ++ 5 files changed, 273 insertions(+) create mode 100644 verifier/tools/cel_verifier.bzl create mode 100755 verifier/tools/run_verifier.sh create mode 100644 verifier/tools/testdata/simple_policy.yaml create mode 100644 verifier/tools/testdata/violated_policy.yaml diff --git a/verifier/tools/BUILD.bazel b/verifier/tools/BUILD.bazel index a547c15b2..c3fe3fc32 100644 --- a/verifier/tools/BUILD.bazel +++ b/verifier/tools/BUILD.bazel @@ -1,8 +1,13 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load(":cel_verifier.bzl", "cel_verifier_test") + package( default_applicable_licenses = ["//:license"], default_visibility = ["//verifier:verifier_internal"], ) +exports_files(["run_verifier.sh"]) + alias( name = "tools", actual = "//verifier/src/main/java/dev/cel/verifier/tools:tools_lib", @@ -17,3 +22,60 @@ alias( name = "cel_verifier_tool", actual = "//verifier/src/main/java/dev/cel/verifier/tools:cel_verifier_tool", ) + +cel_verifier_test( + name = "test_sat_simple_test", + command = "check-sat", + expected_status = [ + "VERIFIED", + "INCONCLUSIVE", + ], # Pass if verified OR inconclusive + expression = "x > 10", + variables = {"x": "int"}, +) + +cel_verifier_test( + name = "test_sat_unsatisfiable_test", + command = "check-sat", + expected_status = ["VIOLATED"], + expression = "x > 10 && x < 5", + variables = {"x": "int"}, +) + +cel_verifier_test( + name = "test_equiv_simple_test", + command = "verify-equiv", + expression = "x > 10", + expression_b = "10 < x", + variables = {"x": "int"}, +) + +cel_verifier_test( + name = "test_policy_simple_test", + command = "verify-policy", + policy_file = "testdata/simple_policy.yaml", + variables = {"role": "string"}, +) + +cel_verifier_test( + name = "test_equiv_negative_test", + command = "verify-equiv", + expected_status = ["VIOLATED"], + expression = "x > 10", + expression_b = "x < 5", + variables = {"x": "int"}, +) + +cel_verifier_test( + name = "test_policy_violated_test", + command = "verify-policy", + expected_status = ["VIOLATED"], + policy_file = "testdata/violated_policy.yaml", + variables = {"role": "string"}, +) + +bzl_library( + name = "cel_verifier_bzl", + srcs = ["cel_verifier.bzl"], + visibility = ["//visibility:private"], +) diff --git a/verifier/tools/cel_verifier.bzl b/verifier/tools/cel_verifier.bzl new file mode 100644 index 000000000..1b6b9e6c6 --- /dev/null +++ b/verifier/tools/cel_verifier.bzl @@ -0,0 +1,154 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Macros for CEL Formal Verifier.""" + +load("@rules_shell//shell:sh_test.bzl", "sh_test") +load("@bazel_skylib//lib:shell.bzl", "shell") + +# Mapping of friendly status names to CelVerifierTool exit codes +_STATUS_TO_EXIT_CODE = { + "VERIFIED": "0", + "VIOLATED": "1", + "INCONCLUSIVE": "2", + "ERROR": "3", +} + +def _make_verifier_args( + command, + expression = None, + expression_b = None, + policy_file = None, + variables = {}, + unknowns = [], + timeout = 10, + unroll_limit = 5): + args = [] + srcs = [] + + if command not in ["check-sat", "check-valid", "verify-equiv", "verify-policy"]: + fail("Unsupported command: " + command) + + args.append(command) + + if command in ["check-sat", "check-valid"]: + if not expression: + fail("expression is required for " + command) + args.append("--expr") + args.append(expression) + elif command == "verify-equiv": + if not expression or not expression_b: + fail("expression and expression_b are required for verify-equiv") + args.append("--expr1") + args.append(expression) + args.append("--expr2") + args.append(expression_b) + elif command == "verify-policy": + if not policy_file: + fail("policy_file is required for verify-policy") + args.append("--file") + args.append("$(rootpath %s)" % policy_file) + srcs.append(policy_file) + + for var_name, var_type in variables.items(): + args.append("--var") + args.append("%s:%s" % (var_name, var_type)) + + for unknown in unknowns: + args.append("--unknown") + args.append(unknown) + + args.append("--timeout") + args.append(str(timeout)) + + args.append("--unroll-limit") + args.append(str(unroll_limit)) + + args.append("--output_format") + args.append("TEXT") + + return args, srcs + +def cel_verifier_test( + name, + command, + expression = None, + expression_b = None, + policy_file = None, + variables = {}, + unknowns = [], + timeout = 10, + unroll_limit = 5, + expected_status = ["VERIFIED"], + **kwargs): + """Verifies a CEL expression or policy via a test rule. + + See //verifier/tools/README.md for more details on CLI commands and options. + + Args: + name: str name for the test + command: str verification command. Supported commands: + - 'check-sat': Verify satisfiability of a CEL expression & generate witness model. + - 'check-valid': Verify validity (isAlwaysTrue) of a CEL expression & generate counterexample. + - 'verify-equiv': Prove logical equivalence between two CEL expressions. + - 'verify-policy': Verify policy invariants defined in a YAML policy file. + expression: str CEL expression to verify (required for check-sat, check-valid, verify-equiv) + expression_b: str second CEL expression for equivalence check (required for verify-equiv) + policy_file: label of a YAML policy file to verify (required for verify-policy) + variables: dict of var_name -> type_string (e.g., {"port": "int"}) + unknowns: list of str unknown identifiers + timeout: int solver timeout in seconds (default 10) + unroll_limit: int comprehension unroll limit (default 5) + expected_status: list of str expected verification statuses (default ["VERIFIED"]). + Supported statuses: 'VERIFIED', 'VIOLATED', 'INCONCLUSIVE', 'ERROR'. + **kwargs: other standard Bazel attributes + """ + + args, srcs = _make_verifier_args( + command = command, + expression = expression, + expression_b = expression_b, + policy_file = policy_file, + variables = variables, + unknowns = unknowns, + timeout = timeout, + unroll_limit = unroll_limit, + ) + + tags = kwargs.pop("tags", []) + if "nomsan" not in tags: + tags = tags + ["nomsan"] + + allowed_codes = [] + for status in expected_status: + if status not in _STATUS_TO_EXIT_CODE: + fail("Unsupported status in expected_status: " + status) + allowed_codes.append(_STATUS_TO_EXIT_CODE[status]) + + allowed_codes_str = ",".join(allowed_codes) + + env = kwargs.pop("env", {}) + env["ALLOWED_EXIT_CODES"] = allowed_codes_str + + sh_test( + name = name, + srcs = ["//verifier/tools:run_verifier.sh"], + args = [shell.quote("$(rootpath //verifier/src/main/java/dev/cel/verifier/tools:cel_verifier_tool)")] + [shell.quote(a) for a in args], + data = [ + "//verifier/src/main/java/dev/cel/verifier/tools:cel_verifier_tool", + ] + srcs, + tags = tags, + env = env, + **kwargs + ) diff --git a/verifier/tools/run_verifier.sh b/verifier/tools/run_verifier.sh new file mode 100755 index 000000000..f8d0a85ff --- /dev/null +++ b/verifier/tools/run_verifier.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Path: //third_party/java/cel/verifier/tools/run_verifier.sh +# +# This script is used by the cel_verifier_test macro to invoke the +# underlying Java binary (CelVerifierTool) with the arguments passed to the macro. +# It uses 'find' to locate the binary in the runfiles directory, which works +# in both Google3 and Bazel environments. + +die() { + echo "ERROR: $*" >&2 + exit 1 +} + +if [ -z "$1" ]; then + die "Verifier binary path not provided as first argument." +fi + +VERIFIER_BINARY="$1" +shift + +# Execute the verifier binary with all passed arguments. +"$VERIFIER_BINARY" "$@" +ACTUAL_EXIT_CODE=$? + +# Default to expecting 0 (VERIFIED) if env is not set +ALLOWED_CODES=${ALLOWED_EXIT_CODES:-"0"} + +# Check if the actual exit code is in the allowed list +if [[ ",$ALLOWED_CODES," =~ ",$ACTUAL_EXIT_CODE," ]]; then + echo "Verification finished with status code $ACTUAL_EXIT_CODE (Allowed: $ALLOWED_CODES)" + exit 0 +else + echo "ERROR: Verification finished with unexpected status code $ACTUAL_EXIT_CODE (Allowed: $ALLOWED_CODES)" >&2 + exit 1 +fi diff --git a/verifier/tools/testdata/simple_policy.yaml b/verifier/tools/testdata/simple_policy.yaml new file mode 100644 index 000000000..6103fc518 --- /dev/null +++ b/verifier/tools/testdata/simple_policy.yaml @@ -0,0 +1,11 @@ +name: simple_policy +rule: + match: + - condition: role == 'admin' + output: 'true' + - output: 'false' +verification: + invariants: + - id: admin_always_true + assume: role == 'admin' + assert: rule.result == true diff --git a/verifier/tools/testdata/violated_policy.yaml b/verifier/tools/testdata/violated_policy.yaml new file mode 100644 index 000000000..2babda216 --- /dev/null +++ b/verifier/tools/testdata/violated_policy.yaml @@ -0,0 +1,11 @@ +name: violated_policy +rule: + match: + - condition: role == 'admin' + output: 'true' + - output: 'false' +verification: + invariants: + - id: viewer_always_true + assume: role == 'viewer' + assert: rule.result == true From 226484d5da7bb2ec665fa75726d1922c6b2dd7b2 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Thu, 10 Sep 2026 12:54:16 -0700 Subject: [PATCH 193/204] Fix type unification for type parameters PiperOrigin-RevId: 979344413 --- .../src/main/java/dev/cel/checker/Types.java | 30 +- .../test/java/dev/cel/checker/TypesTest.java | 318 ++++++++++++++++++ 2 files changed, 346 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/dev/cel/checker/Types.java b/checker/src/main/java/dev/cel/checker/Types.java index 4cc502cdf..f9b82ecb7 100644 --- a/checker/src/main/java/dev/cel/checker/Types.java +++ b/checker/src/main/java/dev/cel/checker/Types.java @@ -205,6 +205,19 @@ private static boolean isTypeParam(CelType type) { return type.kind().equals(CelKind.TYPE_PARAM); } + /** Tests whether the {@code type} contains any type params directly or transitively. */ + private static boolean hasTypeParam(CelType type) { + if (isTypeParam(type)) { + return true; + } + for (CelType param : type.parameters()) { + if (hasTypeParam(param)) { + return true; + } + } + return false; + } + /** Returns the more general of two types which are known to unify. */ public static CelType mostGeneral(CelType type1, CelType type2) { return isEqualOrLessSpecific(type1, type2) ? type1 : type2; @@ -332,8 +345,21 @@ private static boolean internalIsAssignable( switch (type1.kind()) { case TYPE: - // A type is a type is a type, any additional parameterization of the type cannot affect - // method resolution or assignability. + if (!(type1 instanceof TypeType) || !(type2 instanceof TypeType)) { + return type2.isAssignableFrom(type1); + } + TypeType fromType = (TypeType) type1; + TypeType toType = (TypeType) type2; + // If either type contains a type parameter (e.g., type(T) in foo(data, type(T)) -> T), + // delegate to inner type unification to bind or validate type parameter substitutions. + // Returns true if the inner types structurally match, unify with an unbound type param, + // or conform to an existing binding in 'subs'. Returns false on structural/kind mismatches + // (e.g., int vs list(T)), occurs-check cycles, or conflicting type param bindings. + + if (hasTypeParam(fromType.type()) || hasTypeParam(toType.type())) { + return internalIsAssignable(subs, fromType.type(), toType.type()); + } + // Concrete types are coassignable in CEL (e.g., type(1) == type("a"), type([1]) == list). return true; case OPAQUE: case LIST: diff --git a/checker/src/test/java/dev/cel/checker/TypesTest.java b/checker/src/test/java/dev/cel/checker/TypesTest.java index 960ebec3f..a8ca2167e 100644 --- a/checker/src/test/java/dev/cel/checker/TypesTest.java +++ b/checker/src/test/java/dev/cel/checker/TypesTest.java @@ -18,10 +18,21 @@ import dev.cel.expr.Type; import dev.cel.expr.Type.PrimitiveType; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOverloadDecl; import dev.cel.common.types.CelKind; import dev.cel.common.types.CelProtoTypes; import dev.cel.common.types.CelType; +import dev.cel.common.types.ListType; +import dev.cel.common.types.MapType; +import dev.cel.common.types.NullableType; +import dev.cel.common.types.OptionalType; import dev.cel.common.types.SimpleType; +import dev.cel.common.types.TypeParamType; +import dev.cel.common.types.TypeType; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; import java.util.HashMap; import java.util.Map; import org.junit.Test; @@ -54,6 +65,313 @@ public void isAssignable_usingCustomTypes() { assertThat(Types.isAssignable(subs, customType, intType)).isNull(); } + @Test + public void isAssignable_typeType_concreteTypes_legacyCoassignability() { + Map subs = new HashMap<>(); + CelType intType = TypeType.create(SimpleType.INT); + CelType stringType = TypeType.create(SimpleType.STRING); + + Map result1 = Types.isAssignable(subs, intType, stringType); + Map result2 = Types.isAssignable(subs, stringType, intType); + + // Concrete types are coassignable in CEL (e.g. for equality comparison type(1) == type("a")) + assertThat(result1).isEmpty(); + assertThat(result2).isEmpty(); + } + + @Test + public void isAssignable_typeType_mapContainerErasure() { + Map subs = new HashMap<>(); + CelType mapIntUint = TypeType.create(MapType.create(SimpleType.INT, SimpleType.UINT)); + CelType mapDynDyn = TypeType.create(MapType.create(SimpleType.DYN, SimpleType.DYN)); + + Map result = Types.isAssignable(subs, mapIntUint, mapDynDyn); + + // type({1: 2u}) == map + assertThat(result).isEmpty(); + } + + @Test + public void isAssignable_typeType_listContainerErasure() { + Map subs = new HashMap<>(); + CelType listInt = TypeType.create(ListType.create(SimpleType.INT)); + CelType listDyn = TypeType.create(ListType.create(SimpleType.DYN)); + + Map result = Types.isAssignable(subs, listInt, listDyn); + + // type([1]) == list + assertThat(result).isEmpty(); + } + + @Test + public void isAssignable_typeType_typeParamTarget_bindsConcreteType() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(SimpleType.INT); + CelType toType = TypeType.create(typeParamT); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).containsExactly(typeParamT, SimpleType.INT); + } + + @Test + public void isAssignable_typeType_typeParamSource_bindsConcreteType() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(typeParamT); + CelType toType = TypeType.create(SimpleType.INT); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).containsExactly(typeParamT, SimpleType.INT); + } + + @Test + public void isAssignable_typeType_nestedTypeParam_unifies() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + TypeParamType typeParamR = TypeParamType.create("R"); + CelType fromType = TypeType.create(typeParamT); + CelType toType = TypeType.create(TypeType.create(typeParamR)); + + // type(T) == type(type(R)) + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).containsExactly(typeParamT, TypeType.create(typeParamR)); + } + + @Test + public void isAssignable_typeType_deeplyNestedTypeParam_bindsConcreteType() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(TypeType.create(SimpleType.INT)); + CelType toType = TypeType.create(TypeType.create(typeParamT)); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).containsExactly(typeParamT, SimpleType.INT); + } + + @Test + public void isAssignable_typeType_compositeListTypeParam_bindsConcreteType() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(ListType.create(SimpleType.INT)); + CelType toType = TypeType.create(ListType.create(typeParamT)); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).containsExactly(typeParamT, SimpleType.INT); + } + + @Test + public void isAssignable_typeType_compositeMapTypeParam_bindsConcreteTypes() { + Map subs = new HashMap<>(); + TypeParamType typeParamK = TypeParamType.create("K"); + TypeParamType typeParamV = TypeParamType.create("V"); + CelType fromType = TypeType.create(MapType.create(SimpleType.STRING, SimpleType.INT)); + CelType toType = TypeType.create(MapType.create(typeParamK, typeParamV)); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).containsExactly(typeParamK, SimpleType.STRING, typeParamV, SimpleType.INT); + } + + @Test + public void isAssignable_typeType_nullableTypeParam_unifies() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(NullableType.create(SimpleType.INT)); + CelType toType = TypeType.create(NullableType.create(typeParamT)); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result) + .containsExactly(NullableType.create(typeParamT), NullableType.create(SimpleType.INT)); + } + + @Test + public void isAssignable_typeType_optionalTypeParam_unifies() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(OptionalType.create(SimpleType.INT)); + CelType toType = TypeType.create(OptionalType.create(typeParamT)); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).containsExactly(typeParamT, SimpleType.INT); + } + + @Test + public void isAssignable_typeType_incompatibleTypeParams_returnsNull() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(ListType.create(typeParamT)); + CelType toType = TypeType.create(SimpleType.INT); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).isNull(); + } + + @Test + public void isAssignable_typeType_conflictingBoundTypeParam_returnsNull() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + subs.put(typeParamT, SimpleType.STRING); + CelType fromType = TypeType.create(typeParamT); + CelType toType = TypeType.create(SimpleType.INT); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).isNull(); + } + + @Test + public void isAssignable_typeType_occursCheck_failsOnSelfReference() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(typeParamT); + CelType toType = TypeType.create(TypeType.create(typeParamT)); + + // Occurs check: T = type(T) is cyclic and must fail + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).isNull(); + } + + @Test + public void isAssignable_typeType_occursCheck_failsOnTransitiveCycle() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + TypeParamType typeParamR = TypeParamType.create("R"); + subs.put(typeParamT, TypeType.create(typeParamR)); + // Trying to assign type(R) to type(T) would produce R = type(R) transitively through T + CelType fromType = TypeType.create(typeParamR); + CelType toType = TypeType.create(TypeType.create(typeParamT)); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).isNull(); + } + + @Test + public void compiler_typeParamInTypeType_resolvesReturnTypeInt() throws Exception { + TypeParamType typeParamT = TypeParamType.create("T"); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "cast", + CelOverloadDecl.newGlobalOverload( + "cast_t", typeParamT, SimpleType.DYN, TypeType.create(typeParamT)))) + .build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("cast('hello', int)").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.INT); + } + + @Test + public void compiler_typeParamInTypeType_resolvesReturnTypeString() throws Exception { + TypeParamType typeParamT = TypeParamType.create("T"); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "cast", + CelOverloadDecl.newGlobalOverload( + "cast_t", typeParamT, SimpleType.DYN, TypeType.create(typeParamT)))) + .build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("cast(123, string)").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.STRING); + } + + @Test + public void compiler_typeParamInCompositeTypeType_resolvesReturnType() throws Exception { + TypeParamType typeParamT = TypeParamType.create("T"); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "first_elem_type", + CelOverloadDecl.newGlobalOverload( + "first_elem_type_overload", + typeParamT, + SimpleType.DYN, + TypeType.create(ListType.create(typeParamT))))) + .build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("first_elem_type('data', type([1]))").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.INT); + } + + @Test + public void compiler_typeComparison_mapType_succeeds() throws Exception { + CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("type({}) == map").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); + } + + @Test + public void compiler_typeComparison_compositeTypes_succeeds() throws Exception { + CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + + CelAbstractSyntaxTree ast = + celCompiler.compile("list == type([1]) && map == type({1:2u})").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); + } + + @Test + public void compiler_typeComparison_differentTypesEqual_succeeds() throws Exception { + CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("type(1) == type('a')").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); + } + + @Test + public void compiler_typeComparison_differentTypesNotEqual_succeeds() throws Exception { + CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("type(1) != uint").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); + } + + @Test + public void compiler_typeComparison_type1NotEqualsType1u_succeeds() throws Exception { + CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("type(1) != type(1u)").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); + } + + @Test + public void compiler_typeParamEquality_unifiesTypeParams() throws Exception { + TypeParamType typeParamT = TypeParamType.create("T"); + TypeParamType typeParamR = TypeParamType.create("R"); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("x", TypeType.create(typeParamT)) + .addVar("y", TypeType.create(TypeType.create(typeParamR))) + .build(); + + // type(T) == type(type(R)) + CelAbstractSyntaxTree ast = celCompiler.compile("x == y").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); + } + private static final class CustomCelType extends CelType { @Override From 6a8a056ea93d0a9b04371b3dd7263db3f4255062 Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Thu, 10 Sep 2026 17:45:34 -0700 Subject: [PATCH 194/204] Add enablePrattParser option and split Parser into AntlrParser and dispatcher Split Parser.java into AntlrParser.java (containing all ANTLR dependencies) and Parser.java (acting as a simple dispatcher between AntlrParser and PrattParser). Refactor CelParserParameterizedTest and CelParserImplTest to run tests against both ANTLR and Pratt parsers to ensure exact AST equivalence. PiperOrigin-RevId: 979492678 --- .../main/java/dev/cel/common/CelOptions.java | 11 + .../java/dev/cel/common/CelOptionsTest.java | 1 + .../cel/extensions/CelMathExtensionsTest.java | 4 +- .../main/java/dev/cel/parser/AntlrParser.java | 1411 ++++++++++ .../src/main/java/dev/cel/parser/BUILD.bazel | 28 +- .../java/dev/cel/parser/CelParserImpl.java | 4 + .../src/main/java/dev/cel/parser/Parser.java | 1386 +--------- .../main/java/dev/cel/parser/PrattParser.java | 46 +- .../dev/cel/parser/CelParserImplTest.java | 83 +- .../parser/CelParserParameterizedTest.java | 978 +++++-- .../java/dev/cel/parser/PrattParserTest.java | 15 +- ...r.baseline => parser_core_syntax.baseline} | 2291 +++++++---------- .../src/test/resources/parser_errors.baseline | 1199 +++++++-- .../test/resources/parser_literals.baseline | 649 +++++ .../src/test/resources/parser_macros.baseline | 981 +++++++ .../pratt_parser_core_syntax.baseline | 292 ++- .../resources/pratt_parser_errors.baseline | 30 +- .../resources/pratt_parser_literals.baseline | 17 +- .../resources/pratt_parser_macros.baseline | 64 +- .../src/test/resources/source_info.baseline | 2 +- 20 files changed, 6110 insertions(+), 3382 deletions(-) create mode 100644 parser/src/main/java/dev/cel/parser/AntlrParser.java rename parser/src/test/resources/{parser.baseline => parser_core_syntax.baseline} (53%) create mode 100644 parser/src/test/resources/parser_literals.baseline create mode 100644 parser/src/test/resources/parser_macros.baseline diff --git a/common/src/main/java/dev/cel/common/CelOptions.java b/common/src/main/java/dev/cel/common/CelOptions.java index 417d4dc9d..c4b868bf2 100644 --- a/common/src/main/java/dev/cel/common/CelOptions.java +++ b/common/src/main/java/dev/cel/common/CelOptions.java @@ -72,6 +72,8 @@ public enum ProtoUnsetFieldOptions { public abstract boolean enableQuotedIdentifierSyntax(); + public abstract boolean enablePrattParser(); + // Type-Checker related options public abstract boolean enableCompileTimeOverloadResolution(); @@ -144,6 +146,7 @@ public static Builder newBuilder() { .retainUnbalancedLogicalExpressions(false) .enableHiddenAccumulatorVar(true) .enableQuotedIdentifierSyntax(true) + .enablePrattParser(false) // Type-Checker options .enableCompileTimeOverloadResolution(false) .enableHomogeneousLiterals(false) @@ -279,6 +282,14 @@ public abstract static class Builder { */ public abstract Builder enableQuotedIdentifierSyntax(boolean value); + /** + * Enables Pratt parser implementation over ANTLR parser. + * + *

The Pratt parser provides improved parsing performance (typically 4x–11x speedup over + * ANTLR) and lower memory overhead while producing an equivalent abstract syntax tree. + */ + public abstract Builder enablePrattParser(boolean value); + // Type-Checker related options /** diff --git a/common/src/test/java/dev/cel/common/CelOptionsTest.java b/common/src/test/java/dev/cel/common/CelOptionsTest.java index 751d85ed8..cc5203a25 100644 --- a/common/src/test/java/dev/cel/common/CelOptionsTest.java +++ b/common/src/test/java/dev/cel/common/CelOptionsTest.java @@ -35,5 +35,6 @@ public void current_defaults() { // Defaults that aren't represented in deprecated CelOptions assertThat(CelOptions.current().build().enableUnknownTracking()).isFalse(); assertThat(CelOptions.current().build().resolveTypeDependencies()).isTrue(); + assertThat(CelOptions.current().build().enablePrattParser()).isFalse(); } } diff --git a/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java index 68c80dedb..5b57f1fb2 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java @@ -827,7 +827,7 @@ public void abs_overflow_throwsException() { assertThat(e) .hasMessageThat() - .contains("ERROR: :1:10: For input string: \"-9223372036854775809\""); + .contains("ERROR: :1:10: invalid int literal: -9223372036854775809"); } @Test @@ -917,7 +917,7 @@ public void bitAnd_maxValArg_throwsException() { assertThat(e) .hasMessageThat() - .contains("ERROR: :1:33: For input string: \"9223372036854775809\""); + .contains("ERROR: :1:33: invalid int literal: 9223372036854775809"); } @Test diff --git a/parser/src/main/java/dev/cel/parser/AntlrParser.java b/parser/src/main/java/dev/cel/parser/AntlrParser.java new file mode 100644 index 000000000..155fb0843 --- /dev/null +++ b/parser/src/main/java/dev/cel/parser/AntlrParser.java @@ -0,0 +1,1411 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.parser; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.primitives.Ints.min; + +import cel.parser.internal.CELBaseVisitor; +import cel.parser.internal.CELLexer; +import cel.parser.internal.CELParser; +import cel.parser.internal.CELParser.BoolFalseContext; +import cel.parser.internal.CELParser.BoolTrueContext; +import cel.parser.internal.CELParser.BytesContext; +import cel.parser.internal.CELParser.CalcContext; +import cel.parser.internal.CELParser.ConditionalAndContext; +import cel.parser.internal.CELParser.ConditionalOrContext; +import cel.parser.internal.CELParser.ConstantLiteralContext; +import cel.parser.internal.CELParser.CreateListContext; +import cel.parser.internal.CELParser.CreateMapContext; +import cel.parser.internal.CELParser.CreateMessageContext; +import cel.parser.internal.CELParser.DoubleContext; +import cel.parser.internal.CELParser.EscapeIdentContext; +import cel.parser.internal.CELParser.EscapedIdentifierContext; +import cel.parser.internal.CELParser.ExprContext; +import cel.parser.internal.CELParser.ExprListContext; +import cel.parser.internal.CELParser.FieldInitializerListContext; +import cel.parser.internal.CELParser.GlobalCallContext; +import cel.parser.internal.CELParser.IdentContext; +import cel.parser.internal.CELParser.IndexContext; +import cel.parser.internal.CELParser.IntContext; +import cel.parser.internal.CELParser.ListInitContext; +import cel.parser.internal.CELParser.LogicalNotContext; +import cel.parser.internal.CELParser.MapInitializerListContext; +import cel.parser.internal.CELParser.MemberCallContext; +import cel.parser.internal.CELParser.MemberExprContext; +import cel.parser.internal.CELParser.NegateContext; +import cel.parser.internal.CELParser.NestedContext; +import cel.parser.internal.CELParser.NullContext; +import cel.parser.internal.CELParser.OptExprContext; +import cel.parser.internal.CELParser.OptFieldContext; +import cel.parser.internal.CELParser.PrimaryExprContext; +import cel.parser.internal.CELParser.RelationContext; +import cel.parser.internal.CELParser.SelectContext; +import cel.parser.internal.CELParser.SimpleIdentifierContext; +import cel.parser.internal.CELParser.StartContext; +import cel.parser.internal.CELParser.StringContext; +import cel.parser.internal.CELParser.UintContext; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.FormatMethod; +import com.google.errorprone.annotations.FormatString; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelIssue; +import dev.cel.common.CelOptions; +import dev.cel.common.CelSource; +import dev.cel.common.CelSourceLocation; +import dev.cel.common.CelValidationResult; +import dev.cel.common.Operator; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.internal.CodePointStream; +import dev.cel.common.internal.Constants; +import java.text.ParseException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; +import org.antlr.v4.runtime.ANTLRErrorListener; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.DefaultErrorStrategy; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.atn.ATNConfigSet; +import org.antlr.v4.runtime.dfa.DFA; +import org.antlr.v4.runtime.misc.ParseCancellationException; +import org.antlr.v4.runtime.tree.ErrorNode; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.ParseTreeListener; +import org.antlr.v4.runtime.tree.TerminalNode; + +/** ANTLR-based parser implementation for CEL. */ +final class AntlrParser extends CELBaseVisitor { + + private static final CelExpr ERROR = CelExpr.newBuilder().setConstant(Constants.ERROR).build(); + private static final ImmutableSet RESERVED_IDS = + ImmutableSet.of( + "as", + "break", + "const", + "continue", + "else", + "false", + "for", + "function", + "if", + "import", + "in", + "let", + "loop", + "package", + "namespace", + "null", + "return", + "true", + "var", + "void", + "while"); + private static final String ACCUMULATOR_NAME = "__result__"; + private static final String HIDDEN_ACCUMULATOR_NAME = "@result"; + + static CelValidationResult parse( + CelSource source, CelOptions options, Collection macros) { + if (source.getContent().size() > options.maxExpressionCodePointSize()) { + return new CelValidationResult( + source, + ImmutableList.of( + CelIssue.formatError( + CelSourceLocation.NONE, + String.format( + "expression code point size exceeds limit: size: %d, limit %d", + source.getContent().size(), options.maxExpressionCodePointSize())))); + } + CELLexer antlrLexer = + new CELLexer(new CodePointStream(source.getDescription(), source.getContent())); + CELParser antlrParser = new CELParser(new CommonTokenStream(antlrLexer)); + CelSource.Builder sourceInfo = source.toBuilder(); + sourceInfo.setDescription(source.getDescription()); + ExprFactory exprFactory = + new ExprFactory( + antlrParser, + sourceInfo, + options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME, + options.maxParseExpressionNodeCount()); + AntlrParser parserImpl = new AntlrParser(options, macros, sourceInfo, exprFactory); + ErrorListener errorListener = new ErrorListener(exprFactory); + antlrLexer.removeErrorListeners(); + antlrParser.removeErrorListeners(); + antlrLexer.addErrorListener(errorListener); + antlrParser.addErrorListener(errorListener); + antlrParser.addParseListener( + new PerRuleRecursionListener(exprFactory, options.maxParseRecursionDepth())); + antlrParser.setErrorHandler( + new RecoveryLimitErrorStrategy(options.maxParseErrorRecoveryLimit())); + CelExpr expr; + try { + StartContext context = checkNotNull(antlrParser.start()); + expr = checkNotNull(parserImpl.visit(context)); + } catch (ParseCancellationException parseFailure) { + return new CelValidationResult( + sourceInfo.build(), parseFailure, ImmutableList.copyOf(exprFactory.getIssuesList())); + } + return new CelValidationResult( + CelAbstractSyntaxTree.newParsedAst(expr, sourceInfo.build()), + ImmutableList.copyOf(exprFactory.getIssuesList())); + } + + private final CelOptions options; + private final ImmutableMap macros; + private final CelSource.Builder sourceInfo; + private final ExprFactory exprFactory; + + private int recursionDepth; + + private AntlrParser( + CelOptions options, + Collection macros, + CelSource.Builder sourceInfo, + ExprFactory exprFactory) { + this.options = options; + this.macros = macros.stream().collect(ImmutableMap.toImmutableMap(CelMacro::getKey, m -> m)); + this.sourceInfo = sourceInfo; + this.exprFactory = exprFactory; + } + + @Override + public CelExpr visit(ParseTree tree) { + ParseTree unnestedNode = unnest(tree); + boolean isLeftRecursiveNode = isLeftRecursiveForCountingDepths(unnestedNode); + if (isLeftRecursiveNode) { + checkAndIncrementRecursionDepth(); + CelExpr expr = super.visit(unnestedNode); + decrementRecursionDepth(); + return expr; + } + + return super.visit(unnestedNode); + } + + @Override + public CelExpr visitStart(StartContext context) { + checkNotNull(context); + if (context.e == null) { + return exprFactory.ensureErrorsExist(context); + } + return visit(context.e); + } + + @Override + public CelExpr visitExpr(ExprContext context) { + checkNotNull(context); + if (context.e == null) { + return exprFactory.ensureErrorsExist(context); + } + CelExpr condition = visit(context.e); + if (context.op != null) { + if (context.e1 == null || context.e2 == null) { + return exprFactory.ensureErrorsExist(context); + } + condition = + exprFactory + .newExprBuilder(context.op) + .setCall( + CelExpr.CelCall.newBuilder() + .setFunction(Operator.CONDITIONAL.getFunction()) + .addArgs(condition) + .addArgs(visit(context.e1)) + .addArgs(visit(context.e2)) + .build()) + .build(); + } + + return condition; + } + + @Override + public CelExpr visitConditionalOr(ConditionalOrContext context) { + checkNotNull(context); + if (context.e == null) { + return exprFactory.ensureErrorsExist(context); + } + CelExpr conditionalOr = visit(context.e); + if (context.ops == null || context.ops.isEmpty()) { + return conditionalOr; + } + ExpressionBalancer balancer = + new ExpressionBalancer(Operator.LOGICAL_OR.getFunction(), conditionalOr); + int index = 0; + for (Token token : context.ops) { + if (context.e1 == null || index >= context.e1.size()) { + return exprFactory.reportError(context, "unexpected character, wanted '||'"); + } + long operationId = exprFactory.newExprId(exprFactory.getPosition(token)); + CelExpr term = visit(context.e1.get(index)); + balancer.add(operationId, term); + index++; + } + return balancer.balance(); + } + + @Override + public CelExpr visitConditionalAnd(ConditionalAndContext context) { + checkNotNull(context); + if (context.e == null) { + return exprFactory.ensureErrorsExist(context); + } + CelExpr conditionalAnd = visit(context.e); + if (context.ops == null || context.ops.isEmpty()) { + return conditionalAnd; + } + ExpressionBalancer balancer = + new ExpressionBalancer(Operator.LOGICAL_AND.getFunction(), conditionalAnd); + int index = 0; + for (Token token : context.ops) { + if (context.e1 == null || index >= context.e1.size()) { + return exprFactory.reportError(context, "unexpected character, wanted '&&'"); + } + long operationId = exprFactory.newExprId(exprFactory.getPosition(token)); + CelExpr term = visit(context.e1.get(index)); + balancer.add(operationId, term); + index++; + } + return balancer.balance(); + } + + @Override + public CelExpr visitRelation(RelationContext context) { + checkNotNull(context); + if (context.calc() != null) { + return visit(context.calc()); + } + if (context.relation() == null || context.relation().isEmpty() || context.op == null) { + return exprFactory.ensureErrorsExist(context); + } + Optional operator = Operator.find(context.op.getText()); + if (!operator.isPresent()) { + return exprFactory.reportError(context, "operator not found"); + } + CelExpr left = visit(context.relation(0)); + CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.op); + CelExpr right = visit(context.relation(1)); + return exprBuilder + .setCall( + CelExpr.CelCall.newBuilder() + .setFunction(operator.get().getFunction()) + .addArgs(left) + .addArgs(right) + .build()) + .build(); + } + + @Override + public CelExpr visitCalc(CalcContext context) { + checkNotNull(context); + if (context.unary() != null) { + return visit(context.unary()); + } + if (context.calc() == null || context.calc().isEmpty() || context.op == null) { + return exprFactory.ensureErrorsExist(context); + } + Optional operator = Operator.find(context.op.getText()); + if (!operator.isPresent()) { + return exprFactory.reportError(context, "operator not found"); + } + CelExpr left = visit(context.calc(0)); + CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.op); + CelExpr right = visit(context.calc(1)); + return exprBuilder + .setCall( + CelExpr.CelCall.newBuilder() + .setFunction(operator.get().getFunction()) + .addArgs(left) + .addArgs(right) + .build()) + .build(); + } + + @Override + public CelExpr visitMemberExpr(MemberExprContext context) { + checkNotNull(context); + if (context.member() == null) { + return exprFactory.ensureErrorsExist(context); + } + return visit(context.member()); + } + + @Override + public CelExpr visitLogicalNot(LogicalNotContext context) { + checkNotNull(context); + if (context.member() == null) { + return exprFactory.ensureErrorsExist(context); + } + if (context.ops != null && options.retainRepeatedUnaryOperators()) { + CelExpr expr = visit(context.member()); + for (int index = context.ops.size(); index > 0; --index) { + expr = + exprFactory + .newExprBuilder(context.ops.get(index - 1)) + .setCall( + CelExpr.CelCall.newBuilder() + .setFunction(Operator.LOGICAL_NOT.getFunction()) + .addArgs(expr) + .build()) + .build(); + } + return expr; + } else if (context.ops == null || context.ops.size() % 2 == 0) { + return visit(context.member()); + } + CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.ops.get(0)); + CelExpr member = visit(context.member()); + return exprBuilder + .setCall( + CelExpr.CelCall.newBuilder() + .setFunction(Operator.LOGICAL_NOT.getFunction()) + .addArgs(member) + .build()) + .build(); + } + + @Override + public CelExpr visitNegate(NegateContext context) { + checkNotNull(context); + if (context.member() == null) { + return exprFactory.ensureErrorsExist(context); + } + if (context.ops != null && options.retainRepeatedUnaryOperators()) { + CelExpr expr = visit(context.member()); + for (int index = context.ops.size(); index > 0; --index) { + expr = + exprFactory + .newExprBuilder(context.ops.get(index - 1)) + .setCall( + CelExpr.CelCall.newBuilder() + .setFunction(Operator.NEGATE.getFunction()) + .addArgs(expr) + .build()) + .build(); + } + return expr; + } else if (context.ops == null || context.ops.size() % 2 == 0) { + return visit(context.member()); + } + CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.ops.get(0)); + CelExpr member = visit(context.member()); + return exprBuilder + .setCall( + CelExpr.CelCall.newBuilder() + .setFunction(Operator.NEGATE.getFunction()) + .addArgs(member) + .build()) + .build(); + } + + @Override + public CelExpr visitPrimaryExpr(PrimaryExprContext context) { + checkNotNull(context); + if (context.primary() == null) { + return exprFactory.ensureErrorsExist(context); + } + return visit(context.primary()); + } + + @Override + public CelExpr visitSelect(SelectContext context) { + checkNotNull(context); + if (context.member() == null) { + return exprFactory.ensureErrorsExist(context); + } + CelExpr member = visit(context.member()); + if (context.id == null) { + return exprFactory.newExprBuilder(context).build(); + } + String id = normalizeEscapedIdent(context.id); + + if (context.opt != null && context.opt.getText().equals("?")) { + if (!options.enableOptionalSyntax()) { + return exprFactory.reportError(context.op, "unsupported syntax '.?'"); + } + + CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(exprFactory.getPosition(context.op)); + CelExpr.CelCall callExpr = + CelExpr.CelCall.newBuilder() + .setFunction(Operator.OPTIONAL_SELECT.getFunction()) + .addArgs( + Arrays.asList( + member, + exprFactory + .newExprBuilder(context) + .setConstant(CelConstant.ofValue(id)) + .build())) + .build(); + + return exprBuilder.setCall(callExpr).build(); + } + + return exprFactory + .newExprBuilder(context.op) + .setSelect(CelExpr.CelSelect.newBuilder().setOperand(member).setField(id).build()) + .build(); + } + + @Override + public CelExpr visitMemberCall(MemberCallContext context) { + checkNotNull(context); + if (context.member() == null) { + return exprFactory.ensureErrorsExist(context); + } + CelExpr member = visit(context.member()); + if (context.id == null) { + return exprFactory.newExprBuilder(context).build(); + } + String id = context.id.getText(); + return receiverCallOrMacro(context, id, member); + } + + @Override + public CelExpr visitIndex(IndexContext context) { + checkNotNull(context); + if (context.member() == null || context.index == null) { + return exprFactory.ensureErrorsExist(context); + } + CelExpr member = visit(context.member()); + CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.op); + CelExpr index = visit(context.index); + Operator indexOperator = Operator.INDEX; + + if (context.opt != null && context.opt.getText().equals("?")) { + if (!options.enableOptionalSyntax()) { + return exprFactory.reportError(context.op, "unsupported syntax '[?'"); + } + indexOperator = Operator.OPTIONAL_INDEX; + } + + return exprBuilder + .setCall( + CelExpr.CelCall.newBuilder() + .setFunction(indexOperator.getFunction()) + .addArgs(member) + .addArgs(index) + .build()) + .build(); + } + + @Override + public CelExpr visitCreateMessage(CreateMessageContext context) { + checkNotNull(context); + StringBuilder msgNameBuilder = new StringBuilder(); + for (Token token : context.ids) { + if (msgNameBuilder.length() > 0) { + msgNameBuilder.append("."); + } + msgNameBuilder.append(token.getText()); + } + + if (context.leadingDot != null) { + msgNameBuilder.insert(0, "."); + } + + String messageName = msgNameBuilder.toString(); + if (messageName.isEmpty()) { + return exprFactory.ensureErrorsExist(context); + } + + CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.op); + CelExpr.CelStruct.Builder structExpr = visitStructFields(context.entries); + return exprBuilder.setStruct(structExpr.setMessageName(messageName).build()).build(); + } + + @Override + public CelExpr visitIdent(IdentContext context) { + checkNotNull(context); + if (context.id == null) { + return exprFactory.newExprBuilder(context).build(); + } + String id = context.id.getText(); + if (options.enableReservedIds() && RESERVED_IDS.contains(id)) { + return exprFactory.reportError(context, "reserved identifier: %s", id); + } + if (context.leadingDot != null) { + id = "." + id; + } + + return exprFactory + .newExprBuilder(context.id) + .setIdent(CelExpr.CelIdent.newBuilder().setName(id).build()) + .build(); + } + + @Override + public CelExpr visitGlobalCall(GlobalCallContext context) { + checkNotNull(context); + if (context.id == null) { + return exprFactory.newExprBuilder(context).build(); + } + String id = context.id.getText(); + if (options.enableReservedIds() && RESERVED_IDS.contains(id)) { + return exprFactory.reportError(context, "reserved identifier: %s", id); + } + if (context.leadingDot != null) { + id = "." + id; + } + + return globalCallOrMacro(context, id); + } + + @Override + public CelExpr visitNested(NestedContext context) { + checkNotNull(context); + if (context.e == null) { + return exprFactory.ensureErrorsExist(context); + } + return visit(context.e); + } + + @Override + public CelExpr visitCreateList(CreateListContext context) { + checkNotNull(context); + CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.op); + CelExpr.CelList createListExpr = visitListInitElements(context.listInit()); + + return exprBuilder.setList(createListExpr).build(); + } + + private CelExpr.CelList visitListInitElements(ListInitContext context) { + CelExpr.CelList.Builder listExpr = CelExpr.CelList.newBuilder(); + if (context == null) { + return listExpr.build(); + } + + for (int index = 0; index < context.elems.size(); index++) { + OptExprContext elem = context.elems.get(index); + listExpr.addElements(visit(elem.e)); + + if (elem.opt != null) { + if (!options.enableOptionalSyntax()) { + exprFactory.reportError(elem.opt, "unsupported syntax '?'"); + continue; + } + listExpr.addOptionalIndices(index); + } + } + + return listExpr.build(); + } + + @Override + public CelExpr visitCreateMap(CreateMapContext context) { + checkNotNull(context); + CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.op); + CelExpr.CelMap.Builder createMapExpr = visitMapEntries(context.entries); + return exprBuilder.setMap(createMapExpr.build()).build(); + } + + private CelExpr buildMacroCallArgs(CelExpr expr) { + CelExpr.Builder resultExpr = CelExpr.newBuilder().setId(expr.id()); + if (sourceInfo.containsMacroCalls(expr.id())) { + return resultExpr.build(); + } + // Call expression could have args or sub-args that are also macros found in macro calls + if (expr.exprKind().getKind() == CelExpr.ExprKind.Kind.CALL) { + CelExpr.CelCall.Builder callExpr = + CelExpr.CelCall.newBuilder().setFunction(expr.call().function()); + // Iterate the AST from `expr` recursively looking for macros. Because we are at most + // starting from the top level macro, this recursion is bounded by the size of the AST. This + // means that the depth check on the AST during parsing will catch recursion overflows + // before we get to here. + expr.call().args().forEach(arg -> callExpr.addArgs(buildMacroCallArgs(arg))); + expr.call().target().ifPresent(target -> callExpr.setTarget(buildMacroCallArgs(target))); + return resultExpr.setCall(callExpr.build()).build(); + } + return expr; + } + + /** + * Returns the expanded AST after visiting a macro. Optional.empty is returned instead if the + * implementation decides that an expansion should not be performed, in which case we should just + * default to call. + */ + private Optional visitMacro( + CelExpr.Builder expr, + String id, + ImmutableList args, + Optional target, + CelMacro macro) { + if (exprFactory.isNodeLimitExceeded()) { + return Optional.of( + exprFactory.reportError( + exprFactory.getPosition(expr.id()), + "could not expand macro: expression node limit exceeded")); + } + + Optional expandedMacro = + expandMacro( + exprFactory.getPosition(expr.id()), + macro, + target.orElse(CelExpr.newBuilder().build()), + args); + if (!expandedMacro.isPresent()) { + return Optional.empty(); + } + CelExpr.CelCall.Builder callExpr = CelExpr.CelCall.newBuilder().setFunction(id); + if (target.isPresent()) { + if (sourceInfo.containsMacroCalls(target.get().id())) { + callExpr.setTarget(CelExpr.newBuilder().setId(target.get().id()).build()); + } else { + callExpr.setTarget(target.get()); + } + } + for (CelExpr arg : args) { + callExpr.addArgs(buildMacroCallArgs(arg)); + } + + if (options.populateMacroCalls()) { + sourceInfo.addMacroCalls( + expandedMacro.get().id(), + // Note: A macro id MUST NOT be assigned to the call expr placed into the macro calls map. + // This can cause an infinite loop in some of the call chains that try to figure out + // whether the current expression is expanded to a macro. + CelExpr.newBuilder().setCall(callExpr.build()).build()); + } + + sourceInfo.removePositions(expr.id()); + return expandedMacro; + } + + private String normalizeEscapedIdent(EscapeIdentContext context) { + String identifier = context.getText(); + if (context instanceof SimpleIdentifierContext) { + return identifier; + } else if (context instanceof EscapedIdentifierContext) { + if (!options.enableQuotedIdentifierSyntax()) { + exprFactory.reportError(context, "unsupported syntax '`'"); + return identifier; + } + return identifier.substring(1, identifier.length() - 1); + } + + // This is normally unreachable, but might happen if the parser is in an error state or if the + // grammar is updated and not handled here. + exprFactory.reportError(context, "unsupported identifier"); + return identifier; + } + + private CelExpr.CelStruct.Builder visitStructFields(FieldInitializerListContext context) { + if (context == null + || context.cols == null + || context.fields == null + || context.values == null) { + return CelExpr.CelStruct.newBuilder(); + } + int entryCount = min(context.cols.size(), context.fields.size(), context.values.size()); + CelExpr.CelStruct.Builder structExpr = CelExpr.CelStruct.newBuilder(); + for (int index = 0; index < entryCount; index++) { + OptFieldContext fieldContext = context.fields.get(index); + boolean isOptionalEntry = false; + if (fieldContext.opt != null) { + if (!options.enableOptionalSyntax()) { + exprFactory.reportError(fieldContext.opt, "unsupported syntax '?'"); + } else { + isOptionalEntry = true; + } + } + + // The field may be empty due to a prior error. + if (fieldContext.escapeIdent() == null) { + return CelExpr.CelStruct.newBuilder(); + } + String fieldName = normalizeEscapedIdent(fieldContext.escapeIdent()); + + CelExpr.CelStruct.Entry.Builder exprBuilder = + CelExpr.CelStruct.Entry.newBuilder() + .setId(exprFactory.newExprId(exprFactory.getPosition(context.cols.get(index)))); + structExpr.addEntries( + exprBuilder + .setFieldKey(fieldName) + .setValue(visit(context.values.get(index))) + .setOptionalEntry(isOptionalEntry) + .build()); + } + return structExpr; + } + + private CelExpr.CelMap.Builder visitMapEntries(MapInitializerListContext context) { + if (context == null || context.cols == null || context.keys == null || context.values == null) { + return CelExpr.CelMap.newBuilder(); + } + int entryCount = min(context.cols.size(), context.keys.size(), context.values.size()); + CelExpr.CelMap.Builder mapExpr = CelExpr.CelMap.newBuilder(); + for (int index = 0; index < entryCount; index++) { + OptExprContext keyContext = context.keys.get(index); + boolean isOptionalEntry = false; + if (keyContext.opt != null) { + if (!options.enableOptionalSyntax()) { + exprFactory.reportError(keyContext.opt, "unsupported syntax '?'"); + } else { + isOptionalEntry = true; + } + } + CelExpr.CelMap.Entry.Builder exprBuilder = + CelExpr.CelMap.Entry.newBuilder() + .setId(exprFactory.newExprId(exprFactory.getPosition(context.cols.get(index)))); + mapExpr.addEntries( + exprBuilder + .setKey(visit(keyContext.e)) + .setValue(visit(context.values.get(index))) + .setOptionalEntry(isOptionalEntry) + .build()); + } + return mapExpr; + } + + @Override + protected CelExpr defaultResult() { + // visitTerminalNode and visitErrorNode call this method. + return exprFactory.ensureErrorsExist( + () -> "Abstract syntax tree in an unexpected state, this is likely a bug."); + } + + @Override + public CelExpr visitConstantLiteral(ConstantLiteralContext context) { + checkNotNull(context); + if (context.literal() == null) { + return exprFactory.ensureErrorsExist(context); + } + return visit(context.literal()); + } + + @Override + public CelExpr visitExprList(ExprListContext context) { + // We should never get here, as we do not directly visit expression lists. + return exprFactory.ensureErrorsExist(context); + } + + @Override + public CelExpr visitFieldInitializerList(FieldInitializerListContext context) { + // We should never get here, as we do not directly visit field initializer lists. + return exprFactory.ensureErrorsExist(context); + } + + @Override + public CelExpr visitMapInitializerList(MapInitializerListContext context) { + // We should never get here, as we do not directly visit map initializer lists. + return exprFactory.ensureErrorsExist(context); + } + + @Override + public CelExpr visitListInit(ListInitContext context) { + // We should never get here, as we do not directly visit list initializer. + return exprFactory.ensureErrorsExist(context); + } + + @Override + public CelExpr visitInt(IntContext context) { + checkNotNull(context); + CelConstant constExpr; + try { + constExpr = Constants.parseInt(context.getText()); + } catch (ParseException e) { + // Do not propagate e.getMessage(), which is JDK-specific. + return exprFactory.reportError( + context, "invalid int literal: %s", context.getText()); + } + + return exprFactory.newExprBuilder(context.tok).setConstant(constExpr).build(); + } + + @Override + public CelExpr visitUint(UintContext context) { + checkNotNull(context); + CelConstant constExpr; + try { + constExpr = Constants.parseUint(context.getText()); + } catch (ParseException e) { + // Do not propagate e.getMessage(), which is JDK-specific. + return exprFactory.reportError( + context, "invalid uint literal: %s", context.getText()); + } + return exprFactory.newExprBuilder(context).setConstant(constExpr).build(); + } + + @Override + public CelExpr visitDouble(DoubleContext context) { + checkNotNull(context); + CelConstant constExpr; + try { + constExpr = Constants.parseDouble(context.getText()); + } catch (ParseException e) { + // Do not propagate e.getMessage(), which is JDK-specific. + return exprFactory.reportError( + context, "invalid double literal: %s", context.getText()); + } + return exprFactory.newExprBuilder(context.tok).setConstant(constExpr).build(); + } + + @Override + public CelExpr visitString(StringContext context) { + checkNotNull(context); + CelConstant constExpr; + try { + constExpr = Constants.parseString(context.getText()); + } catch (ParseException e) { + return exprFactory.reportError(context, e.getMessage()); + } + return exprFactory.newExprBuilder(context).setConstant(constExpr).build(); + } + + @Override + public CelExpr visitBytes(BytesContext context) { + checkNotNull(context); + CelConstant constExpr; + try { + constExpr = Constants.parseBytes(context.getText()); + } catch (ParseException e) { + return exprFactory.reportError(context, e.getMessage()); + } + return exprFactory.newExprBuilder(context).setConstant(constExpr).build(); + } + + @Override + public CelExpr visitBoolTrue(BoolTrueContext context) { + checkNotNull(context); + return exprFactory.newExprBuilder(context).setConstant(Constants.TRUE).build(); + } + + @Override + public CelExpr visitBoolFalse(BoolFalseContext context) { + checkNotNull(context); + return exprFactory.newExprBuilder(context).setConstant(Constants.FALSE).build(); + } + + @Override + public CelExpr visitNull(NullContext context) { + checkNotNull(context); + return exprFactory.newExprBuilder(context).setConstant(Constants.NULL).build(); + } + + private Optional expandMacro( + int position, CelMacro macro, CelExpr target, ImmutableList arguments) { + exprFactory.pushPosition(position); + try { + return macro.getExpander().expandMacro(exprFactory, target, arguments); + } finally { + exprFactory.popPosition(); + } + } + + private CelExpr receiverCallOrMacro(MemberCallContext context, String id, CelExpr member) { + return macroOrCall(context.args, context.open, id, Optional.of(member), true); + } + + private CelExpr globalCallOrMacro(GlobalCallContext context, String id) { + return macroOrCall(context.args, context.op, id, Optional.empty(), false); + } + + private ImmutableList visitExprListContext(ExprListContext args) { + int argCount = args != null && args.e != null ? args.e.size() : 0; + if (argCount == 0) { + return ImmutableList.of(); + } + + ImmutableList.Builder argumentsBuilder = + ImmutableList.builderWithExpectedSize(argCount); + for (ExprContext argExprCtx : args.e) { + argumentsBuilder.add(visit(argExprCtx)); + } + return argumentsBuilder.build(); + } + + private CelExpr macroOrCall( + ExprListContext args, + Token open, + String id, + Optional member, + boolean isReceiverStyle) { + int argCount = args != null && args.e != null ? args.e.size() : 0; + Optional macro = lookupMacro(id, argCount, isReceiverStyle); + CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(open); + + ImmutableList arguments = visitExprListContext(args); + Optional errorArg = arguments.stream().filter(ERROR::equals).findAny(); + if (errorArg.isPresent()) { + sourceInfo.removePositions(exprBuilder.id()); + // Any arguments passed in to the macro may fail parsing. + // Stop the macro expansion in this case as the result of the macro will be a parse failure. + return ERROR; + } + + if (macro.isPresent()) { + Optional expandedMacro = visitMacro(exprBuilder, id, arguments, member, macro.get()); + if (expandedMacro.isPresent()) { + return expandedMacro.get(); + } + } + + CelExpr.CelCall.Builder callExpr = + CelExpr.CelCall.newBuilder().setFunction(id).addArgs(arguments); + member.ifPresent(callExpr::setTarget); + + return exprBuilder.setCall(callExpr.build()).build(); + } + + private Optional lookupMacro(String id, int argCount, boolean receiverStlye) { + String key = CelMacro.formatKey(id, argCount, receiverStlye); + CelMacro macro = macros.get(key); + if (macro != null) { + return Optional.of(macro); + } + key = CelMacro.formatVarArgKey(id, receiverStlye); + return Optional.ofNullable(macros.get(key)); + } + + /** + * Checks whether a given parse tree node is left recursive for the purposes of counting recursion + * depths. + */ + private boolean isLeftRecursiveForCountingDepths(ParseTree node) { + // There are certainly more left recursive nodes than what's shown below. + // We try to catch the specific node types that explodes the number of recursive visit calls and + // of those that cannot be caught by PerRuleRecursionListener. + return node instanceof ExprContext + || node instanceof CalcContext + || node instanceof RelationContext + || node instanceof SelectContext + || node instanceof MemberCallContext + || node instanceof IndexContext; + } + + private void checkAndIncrementRecursionDepth() { + recursionDepth++; + if (recursionDepth > options.maxParseRecursionDepth()) { + String errorMessage = + String.format( + "Expression recursion limit exceeded. limit: %d", options.maxParseRecursionDepth()); + exprFactory.reportError(CelIssue.formatError(CelSourceLocation.of(1, 0), errorMessage)); + throw new ParseCancellationException(errorMessage); + } + } + + private void decrementRecursionDepth() { + recursionDepth--; + } + + /** + * unnest traverses down the left-hand side of the parse graph until it encounters the first + * compound parse node or the first leaf in the parse graph. + */ + private ParseTree unnest(ParseTree tree) { + while (tree != null) { + if (tree instanceof ExprContext) { + // conditionalOr op='?' conditionalOr : expr + ExprContext context = (ExprContext) tree; + if (context.op != null) { + return tree; + } + // conditionalOr + tree = context.e; + } else if (tree instanceof ConditionalOrContext) { + // conditionalAnd (ops=|| conditionalAnd)* + ConditionalOrContext context = (ConditionalOrContext) tree; + if (context.ops != null && !context.ops.isEmpty()) { + return tree; + } + // conditionalAnd + tree = context.e; + } else if (tree instanceof ConditionalAndContext) { + // relation (ops=&& relation)* + ConditionalAndContext context = (ConditionalAndContext) tree; + if (context.ops != null && !context.ops.isEmpty()) { + return tree; + } + + // relation + tree = context.e; + } else if (tree instanceof RelationContext) { + // relation op relation + RelationContext context = (RelationContext) tree; + if (context.op != null) { + return tree; + } + // calc + tree = context.calc(); + } else if (tree instanceof CalcContext) { + // calc op calc + CalcContext context = (CalcContext) tree; + if (context.op != null) { + return tree; + } + + // unary + tree = context.unary(); + } else if (tree instanceof MemberExprContext) { + // member expands to one of: primary, select, index, or create message + tree = ((MemberExprContext) tree).member(); + } else if (tree instanceof PrimaryExprContext) { + // primary expands to one of identifier, nested, create list, create struct, literal + tree = ((PrimaryExprContext) tree).primary(); + } else if (tree instanceof NestedContext) { + // contains a nested 'expr' + tree = ((NestedContext) tree).e; + } else if (tree instanceof ConstantLiteralContext) { + // expands to a primitive literal + tree = ((ConstantLiteralContext) tree).literal(); + } else { + return tree; + } + } + + return tree; + } + + /** Implementation of {@link CelMacroExprFactory}. */ + private static final class ExprFactory extends CelMacroExprFactory { + + private final org.antlr.v4.runtime.Parser recognizer; + private final CelSource.Builder sourceInfo; + private final ArrayList issues; + private final ArrayDeque positions; + private final String accumulatorVarName; + private final int maxExpressionNodeCount; + private boolean nodeLimitExceeded; + + private ExprFactory( + org.antlr.v4.runtime.Parser recognizer, + CelSource.Builder sourceInfo, + String accumulatorVarName, + int maxExpressionNodeCount) { + this.recognizer = recognizer; + this.sourceInfo = sourceInfo; + this.issues = new ArrayList<>(); + this.positions = new ArrayDeque<>(1); // Currently this usually contains at most 1 position. + this.accumulatorVarName = accumulatorVarName; + this.maxExpressionNodeCount = maxExpressionNodeCount; + } + + // Implementation of CelExprFactory. + + @Override + protected CelSourceLocation getSourceLocation(long exprId) { + checkArgument(exprId > 0L); + return getLocation(getPosition(exprId)); + } + + @CanIgnoreReturnValue + @Override + public CelExpr reportError(CelIssue error) { + checkNotNull(error); + issues.add(error); + if (!CelSourceLocation.NONE.equals(error.getSourceLocation())) { + Optional offset = sourceInfo.getLocationOffset(error.getSourceLocation()); + checkState(offset.isPresent()); // A valid location should always return a valid offset. + return newExpr(offset.get()); + } + return ERROR; + } + + @FormatMethod + @CanIgnoreReturnValue + private CelExpr reportError( + ParserRuleContext context, @FormatString String format, Object... args) { + return reportError(context, String.format(format, args)); + } + + @CanIgnoreReturnValue + private CelExpr reportError(ParserRuleContext context, String message) { + return reportError(CelIssue.formatError(getLocation(context), message)); + } + + @CanIgnoreReturnValue + private CelExpr reportError(Token token, String message) { + return reportError(CelIssue.formatError(getLocation(token), message)); + } + + @CanIgnoreReturnValue + private CelExpr reportError(int position, String message) { + return reportError(CelIssue.formatError(getLocation(position), message)); + } + + // Implementation of CelExprFactory. + + @Override + public String getAccumulatorVarName() { + return accumulatorVarName; + } + + @Override + protected CelSourceLocation currentSourceLocationForMacro() { + checkState(!positions.isEmpty()); // Should only be called while expanding macros. + return getLocation(peekPosition()); + } + + // Internal methods used by the parser but not part of the public API. + + private boolean isNodeLimitExceeded() { + return nodeLimitExceeded; + } + + private void pushPosition(int position) { + positions.addLast(position); + } + + private void popPosition() { + checkState(!positions.isEmpty()); + positions.removeLast(); + } + + private int peekPosition() { + checkState(!positions.isEmpty()); + return positions.peekLast(); + } + + private long nextExprId(int position) { + long exprId = super.nextExprId(); + if (exprId > maxExpressionNodeCount && !nodeLimitExceeded) { + nodeLimitExceeded = true; + reportError( + position, String.format("expression node limit (%d) exceeded", maxExpressionNodeCount)); + } + if (position != -1) { + sourceInfo.addPositions(exprId, position); + } + return exprId; + } + + @Override + public long nextExprId() { + checkState(!positions.isEmpty()); // Should only be called while expanding macros. + // Do not call this method directly from within the parser, use nextExprId(int). + return nextExprId(peekPosition()); + } + + @Override + public long copyExprId(long id) { + return nextExprId(getPosition(id)); + } + + private List getIssuesList() { + return issues; + } + + private int getPosition(long exprId) { + return Optional.ofNullable(sourceInfo.getPositionsMap().get(exprId)).orElse(-1); + } + + private int getPosition(Token token) { + return sourceInfo + .getLocationOffset(token.getLine(), token.getCharPositionInLine()) + .orElse(-1); + } + + private int getPosition(ParserRuleContext context) { + return getPosition(context.getStart()); + } + + private CelSourceLocation getLocation(int position) { + return sourceInfo.getOffsetLocation(position).orElse(CelSourceLocation.NONE); + } + + private CelSourceLocation getLocation(Token token) { + return CelSourceLocation.of(token.getLine(), token.getCharPositionInLine()); + } + + private CelSourceLocation getLocation(ParserRuleContext context) { + return getLocation(context.getStart()); + } + + @CanIgnoreReturnValue + private long newExprId(int position) { + return nextExprId(position); + } + + private CelExpr.Builder newExprBuilder(int position) { + return CelExpr.newBuilder().setId(newExprId(position)); + } + + private CelExpr.Builder newExprBuilder(Token token) { + return newExprBuilder(getPosition(token)); + } + + private CelExpr.Builder newExprBuilder(ParserRuleContext context) { + return newExprBuilder(getPosition(context)); + } + + private CelExpr newExpr(int position) { + return newExprBuilder(position).build(); + } + + private CelExpr ensureErrorsExist(Supplier message) { + // Because we do not treat syntax errors as fatal during parsing, the parse tree is often in + // an abnormal state. We call this function to ensure we have recorded syntax errors. If we + // have we return the special error node otherwise we bail and mention that this is likely a + // bug. + if (issues.isEmpty()) { + // If we reach here, this is an unexpected error and highly likely to be a bug. At least one + // syntax error or another error should have occurred because the parse tree is in an + // unexpected state. + throw new ParseCancellationException( + String.format( + "Abstract syntax tree in an unexpected state, this is likely a bug: %s", + message.get())); + } + return ERROR; + } + + private CelExpr ensureErrorsExist(ParserRuleContext context) { + return ensureErrorsExist(() -> context.toInfoString(recognizer)); + } + } + + /** + * Listener that enforces a maximum recursion depth, to avoid accidental stack overflow issues + * when parsing large expressions. + */ + private static final class PerRuleRecursionListener implements ParseTreeListener { + + private final ExprFactory exprFactory; + private final int maxRecursionDepth; + private final Map ruleTypeDepth; + + private PerRuleRecursionListener(ExprFactory exprFactory, int maxRecursionDepth) { + this.exprFactory = exprFactory; + this.maxRecursionDepth = maxRecursionDepth; + this.ruleTypeDepth = new HashMap<>(); + } + + @Override + public void enterEveryRule(ParserRuleContext context) { + int ruleDepth = ruleTypeDepth.getOrDefault(context.getRuleIndex(), 0) + 1; + ruleTypeDepth.put(context.getRuleIndex(), ruleDepth); + if (ruleDepth > maxRecursionDepth) { + String errorMessage = + String.format("Expression recursion limit exceeded. limit: %d", maxRecursionDepth); + exprFactory.reportError(CelIssue.formatError(CelSourceLocation.of(1, 0), errorMessage)); + throw new ParseCancellationException(errorMessage); + } + } + + @Override + public void exitEveryRule(ParserRuleContext context) { + int ruleDepth = ruleTypeDepth.get(context.getRuleIndex()) - 1; + ruleTypeDepth.put(context.getRuleIndex(), ruleDepth); + } + + @Override + public void visitErrorNode(ErrorNode node) {} + + @Override + public void visitTerminal(TerminalNode node) {} + } + + /** Error strategy that limits the number of recovery attempts. */ + private static final class RecoveryLimitErrorStrategy extends DefaultErrorStrategy { + + private final int recoveryLimit; + private int recoveryAttempts; + + private RecoveryLimitErrorStrategy(int recoveryLimit) { + this.recoveryLimit = recoveryLimit; + recoveryAttempts = 0; + } + + @Override + public void recover(org.antlr.v4.runtime.Parser recognizer, RecognitionException e) { + checkRecoveryLimit(recognizer); + super.recover(recognizer, e); + } + + @Override + public Token recoverInline(org.antlr.v4.runtime.Parser recognizer) { + checkRecoveryLimit(recognizer); + return super.recoverInline(recognizer); + } + + private void checkRecoveryLimit(org.antlr.v4.runtime.Parser recognizer) { + if (recoveryAttempts++ >= recoveryLimit) { + String tooManyErrors = String.format("More than %d parse errors.", recoveryLimit); + recognizer.notifyErrorListeners(tooManyErrors); + throw new ParseCancellationException(tooManyErrors); + } + } + } + + private static final class ErrorListener implements ANTLRErrorListener { + + private final ExprFactory exprFactory; + + private ErrorListener(ExprFactory exprFactory) { + this.exprFactory = exprFactory; + } + + @Override + public void reportAmbiguity( + org.antlr.v4.runtime.Parser recognizer, + DFA dfa, + int startIndex, + int stopIndex, + boolean exact, + BitSet ambigAlts, + ATNConfigSet configs) { + // Intentional. + } + + @Override + public void reportAttemptingFullContext( + org.antlr.v4.runtime.Parser recognizer, + DFA dfa, + int startIndex, + int stopIndex, + BitSet ambigAlts, + ATNConfigSet configs) { + // Intentional. + } + + @Override + public void reportContextSensitivity( + org.antlr.v4.runtime.Parser recognizer, + DFA dfa, + int startIndex, + int stopIndex, + int prediction, + ATNConfigSet configs) { + // Intentional. + } + + @Override + public void syntaxError( + Recognizer recognizer, + Object offendingSymbol, + int line, + int charPositionInLine, + String msg, + RecognitionException e) { + msg = msg.replace("%", "%%"); + exprFactory.reportError( + CelIssue.formatError(CelSourceLocation.of(line, charPositionInLine), msg)); + } + } +} diff --git a/parser/src/main/java/dev/cel/parser/BUILD.bazel b/parser/src/main/java/dev/cel/parser/BUILD.bazel index 905bf298f..848209380 100644 --- a/parser/src/main/java/dev/cel/parser/BUILD.bazel +++ b/parser/src/main/java/dev/cel/parser/BUILD.bazel @@ -11,10 +11,15 @@ package( # keep sorted PARSER_SOURCES = [ "CelParserImpl.java", - "ExpressionBalancer.java", "Parser.java", ] +# keep sorted +ANTLR_PARSER_SOURCES = [ + "AntlrParser.java", + "ExpressionBalancer.java", +] + # keep sorted PRATT_PARSER_SOURCES = [ "Lexer.java", @@ -61,19 +66,36 @@ java_library( tags = [ ], deps = [ + ":antlr_parser", ":macro", ":parser_builder", + ":pratt_parser", + "//common:cel_source", + "//common:compiler_common", + "//common:options", + "//common/annotations", + "//common/internal:env_visitor", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "antlr_parser", + srcs = ANTLR_PARSER_SOURCES, + tags = [ + ], + deps = [ + ":macro", "//common:cel_ast", "//common:cel_source", "//common:compiler_common", "//common:operator", "//common:options", "//common:source_location", - "//common/annotations", "//common/ast", "//common/internal", "//common/internal:code_point_stream", - "//common/internal:env_visitor", "//parser:cel_g4_visitors", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", diff --git a/parser/src/main/java/dev/cel/parser/CelParserImpl.java b/parser/src/main/java/dev/cel/parser/CelParserImpl.java index 615b073ef..8ee9a3457 100644 --- a/parser/src/main/java/dev/cel/parser/CelParserImpl.java +++ b/parser/src/main/java/dev/cel/parser/CelParserImpl.java @@ -100,6 +100,10 @@ Optional findMacro(String key) { return Optional.ofNullable(macros.get(key)); } + ImmutableMap getMacros() { + return macros; + } + /** Return the options the {@link CelParser} was originally created with. */ public CelOptions getOptions() { return options; diff --git a/parser/src/main/java/dev/cel/parser/Parser.java b/parser/src/main/java/dev/cel/parser/Parser.java index 0e6849056..9b2a5aad8 100644 --- a/parser/src/main/java/dev/cel/parser/Parser.java +++ b/parser/src/main/java/dev/cel/parser/Parser.java @@ -14,1392 +14,24 @@ package dev.cel.parser; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.primitives.Ints.min; - -import cel.parser.internal.CELBaseVisitor; -import cel.parser.internal.CELLexer; -import cel.parser.internal.CELParser; -import cel.parser.internal.CELParser.BoolFalseContext; -import cel.parser.internal.CELParser.BoolTrueContext; -import cel.parser.internal.CELParser.BytesContext; -import cel.parser.internal.CELParser.CalcContext; -import cel.parser.internal.CELParser.ConditionalAndContext; -import cel.parser.internal.CELParser.ConditionalOrContext; -import cel.parser.internal.CELParser.ConstantLiteralContext; -import cel.parser.internal.CELParser.CreateListContext; -import cel.parser.internal.CELParser.CreateMapContext; -import cel.parser.internal.CELParser.CreateMessageContext; -import cel.parser.internal.CELParser.DoubleContext; -import cel.parser.internal.CELParser.EscapeIdentContext; -import cel.parser.internal.CELParser.EscapedIdentifierContext; -import cel.parser.internal.CELParser.ExprContext; -import cel.parser.internal.CELParser.ExprListContext; -import cel.parser.internal.CELParser.FieldInitializerListContext; -import cel.parser.internal.CELParser.GlobalCallContext; -import cel.parser.internal.CELParser.IdentContext; -import cel.parser.internal.CELParser.IndexContext; -import cel.parser.internal.CELParser.IntContext; -import cel.parser.internal.CELParser.ListInitContext; -import cel.parser.internal.CELParser.LogicalNotContext; -import cel.parser.internal.CELParser.MapInitializerListContext; -import cel.parser.internal.CELParser.MemberCallContext; -import cel.parser.internal.CELParser.MemberExprContext; -import cel.parser.internal.CELParser.NegateContext; -import cel.parser.internal.CELParser.NestedContext; -import cel.parser.internal.CELParser.NullContext; -import cel.parser.internal.CELParser.OptExprContext; -import cel.parser.internal.CELParser.OptFieldContext; -import cel.parser.internal.CELParser.PrimaryExprContext; -import cel.parser.internal.CELParser.RelationContext; -import cel.parser.internal.CELParser.SelectContext; -import cel.parser.internal.CELParser.SimpleIdentifierContext; -import cel.parser.internal.CELParser.StartContext; -import cel.parser.internal.CELParser.StringContext; -import cel.parser.internal.CELParser.UintContext; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import com.google.errorprone.annotations.FormatMethod; -import com.google.errorprone.annotations.FormatString; -import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelIssue; import dev.cel.common.CelOptions; import dev.cel.common.CelSource; -import dev.cel.common.CelSourceLocation; import dev.cel.common.CelValidationResult; -import dev.cel.common.Operator; -import dev.cel.common.ast.CelConstant; -import dev.cel.common.ast.CelExpr; -import dev.cel.common.internal.CodePointStream; -import dev.cel.common.internal.Constants; -import java.text.ParseException; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.BitSet; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.Supplier; -import org.antlr.v4.runtime.ANTLRErrorListener; -import org.antlr.v4.runtime.CommonTokenStream; -import org.antlr.v4.runtime.DefaultErrorStrategy; -import org.antlr.v4.runtime.ParserRuleContext; -import org.antlr.v4.runtime.RecognitionException; -import org.antlr.v4.runtime.Recognizer; -import org.antlr.v4.runtime.Token; -import org.antlr.v4.runtime.atn.ATNConfigSet; -import org.antlr.v4.runtime.dfa.DFA; -import org.antlr.v4.runtime.misc.ParseCancellationException; -import org.antlr.v4.runtime.tree.ErrorNode; -import org.antlr.v4.runtime.tree.ParseTree; -import org.antlr.v4.runtime.tree.ParseTreeListener; -import org.antlr.v4.runtime.tree.TerminalNode; /** - * Parses a CEL expression and returns an abstraction syntax tree in the form of - * google.api.expr.ParsedExpr. Currently this uses ANTLRv4 for lexing and parsing. + * Parses a CEL expression and returns an abstract syntax tree. + * + *

Dispatches to {@link AntlrParser} or {@link PrattParser} based on {@link + * CelOptions#enablePrattParser()}. */ -final class Parser extends CELBaseVisitor { - - private static final CelExpr ERROR = CelExpr.newBuilder().setConstant(Constants.ERROR).build(); - private static final ImmutableSet RESERVED_IDS = - ImmutableSet.of( - "as", - "break", - "const", - "continue", - "else", - "false", - "for", - "function", - "if", - "import", - "in", - "let", - "loop", - "package", - "namespace", - "null", - "return", - "true", - "var", - "void", - "while"); - private static final String ACCUMULATOR_NAME = "__result__"; - private static final String HIDDEN_ACCUMULATOR_NAME = "@result"; +final class Parser { static CelValidationResult parse(CelParserImpl parser, CelSource source, CelOptions options) { - if (source.getContent().size() > options.maxExpressionCodePointSize()) { - return new CelValidationResult( - source, - ImmutableList.of( - CelIssue.formatError( - CelSourceLocation.NONE, - String.format( - "expression code point size exceeds limit: size: %d, limit %d", - source.getContent().size(), options.maxExpressionCodePointSize())))); - } - CELLexer antlrLexer = - new CELLexer(new CodePointStream(source.getDescription(), source.getContent())); - CELParser antlrParser = new CELParser(new CommonTokenStream(antlrLexer)); - CelSource.Builder sourceInfo = source.toBuilder(); - sourceInfo.setDescription(source.getDescription()); - ExprFactory exprFactory = - new ExprFactory( - antlrParser, - sourceInfo, - options.enableHiddenAccumulatorVar() ? HIDDEN_ACCUMULATOR_NAME : ACCUMULATOR_NAME, - options.maxParseExpressionNodeCount()); - Parser parserImpl = new Parser(parser, options, sourceInfo, exprFactory); - ErrorListener errorListener = new ErrorListener(exprFactory); - antlrLexer.removeErrorListeners(); - antlrParser.removeErrorListeners(); - antlrLexer.addErrorListener(errorListener); - antlrParser.addErrorListener(errorListener); - antlrParser.addParseListener( - new PerRuleRecursionListener(exprFactory, options.maxParseRecursionDepth())); - antlrParser.setErrorHandler( - new RecoveryLimitErrorStrategy(options.maxParseErrorRecoveryLimit())); - CelExpr expr; - try { - StartContext context = checkNotNull(antlrParser.start()); - expr = checkNotNull(parserImpl.visit(context)); - } catch (ParseCancellationException parseFailure) { - return new CelValidationResult( - sourceInfo.build(), parseFailure, ImmutableList.copyOf(exprFactory.getIssuesList())); - } - return new CelValidationResult( - CelAbstractSyntaxTree.newParsedAst(expr, sourceInfo.build()), - ImmutableList.copyOf(exprFactory.getIssuesList())); - } - - private final CelParserImpl parser; - private final CelOptions options; - private final CelSource.Builder sourceInfo; - private final ExprFactory exprFactory; - - private int recursionDepth; - - private Parser( - CelParserImpl parser, - CelOptions options, - CelSource.Builder sourceInfo, - ExprFactory exprFactory) { - this.parser = parser; - this.options = options; - this.sourceInfo = sourceInfo; - this.exprFactory = exprFactory; - } - - @Override - public CelExpr visit(ParseTree tree) { - ParseTree unnestedNode = unnest(tree); - boolean isLeftRecursiveNode = isLeftRecursiveForCountingDepths(unnestedNode); - if (isLeftRecursiveNode) { - checkAndIncrementRecursionDepth(); - CelExpr expr = super.visit(unnestedNode); - decrementRecursionDepth(); - return expr; - } - - return super.visit(unnestedNode); - } - - @Override - public CelExpr visitStart(StartContext context) { - checkNotNull(context); - if (context.e == null) { - return exprFactory.ensureErrorsExist(context); - } - return visit(context.e); - } - - @Override - public CelExpr visitExpr(ExprContext context) { - checkNotNull(context); - if (context.e == null) { - return exprFactory.ensureErrorsExist(context); - } - CelExpr condition = visit(context.e); - if (context.op != null) { - if (context.e1 == null || context.e2 == null) { - return exprFactory.ensureErrorsExist(context); - } - condition = - exprFactory - .newExprBuilder(context.op) - .setCall( - CelExpr.CelCall.newBuilder() - .setFunction(Operator.CONDITIONAL.getFunction()) - .addArgs(condition) - .addArgs(visit(context.e1)) - .addArgs(visit(context.e2)) - .build()) - .build(); - } - - return condition; - } - - @Override - public CelExpr visitConditionalOr(ConditionalOrContext context) { - checkNotNull(context); - if (context.e == null) { - return exprFactory.ensureErrorsExist(context); - } - CelExpr conditionalOr = visit(context.e); - if (context.ops == null || context.ops.isEmpty()) { - return conditionalOr; - } - ExpressionBalancer balancer = - new ExpressionBalancer(Operator.LOGICAL_OR.getFunction(), conditionalOr); - int index = 0; - for (Token token : context.ops) { - if (context.e1 == null || index >= context.e1.size()) { - return exprFactory.reportError(context, "unexpected character, wanted '||'"); - } - long operationId = exprFactory.newExprId(exprFactory.getPosition(token)); - CelExpr term = visit(context.e1.get(index)); - balancer.add(operationId, term); - index++; - } - return balancer.balance(); - } - - @Override - public CelExpr visitConditionalAnd(ConditionalAndContext context) { - checkNotNull(context); - if (context.e == null) { - return exprFactory.ensureErrorsExist(context); - } - CelExpr conditionalAnd = visit(context.e); - if (context.ops == null || context.ops.isEmpty()) { - return conditionalAnd; - } - ExpressionBalancer balancer = - new ExpressionBalancer(Operator.LOGICAL_AND.getFunction(), conditionalAnd); - int index = 0; - for (Token token : context.ops) { - if (context.e1 == null || index >= context.e1.size()) { - return exprFactory.reportError(context, "unexpected character, wanted '&&'"); - } - long operationId = exprFactory.newExprId(exprFactory.getPosition(token)); - CelExpr term = visit(context.e1.get(index)); - balancer.add(operationId, term); - index++; - } - return balancer.balance(); - } - - @Override - public CelExpr visitRelation(RelationContext context) { - checkNotNull(context); - if (context.calc() != null) { - return visit(context.calc()); - } - if (context.relation() == null || context.relation().isEmpty() || context.op == null) { - return exprFactory.ensureErrorsExist(context); - } - Optional operator = Operator.find(context.op.getText()); - if (!operator.isPresent()) { - return exprFactory.reportError(context, "operator not found"); - } - CelExpr left = visit(context.relation(0)); - CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.op); - CelExpr right = visit(context.relation(1)); - return exprBuilder - .setCall( - CelExpr.CelCall.newBuilder() - .setFunction(operator.get().getFunction()) - .addArgs(left) - .addArgs(right) - .build()) - .build(); - } - - @Override - public CelExpr visitCalc(CalcContext context) { - checkNotNull(context); - if (context.unary() != null) { - return visit(context.unary()); - } - if (context.calc() == null || context.calc().isEmpty() || context.op == null) { - return exprFactory.ensureErrorsExist(context); - } - Optional operator = Operator.find(context.op.getText()); - if (!operator.isPresent()) { - return exprFactory.reportError(context, "operator not found"); - } - CelExpr left = visit(context.calc(0)); - CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.op); - CelExpr right = visit(context.calc(1)); - return exprBuilder - .setCall( - CelExpr.CelCall.newBuilder() - .setFunction(operator.get().getFunction()) - .addArgs(left) - .addArgs(right) - .build()) - .build(); - } - - @Override - public CelExpr visitMemberExpr(MemberExprContext context) { - checkNotNull(context); - if (context.member() == null) { - return exprFactory.ensureErrorsExist(context); - } - return visit(context.member()); - } - - @Override - public CelExpr visitLogicalNot(LogicalNotContext context) { - checkNotNull(context); - if (context.member() == null) { - return exprFactory.ensureErrorsExist(context); - } - if (context.ops != null && options.retainRepeatedUnaryOperators()) { - CelExpr expr = visit(context.member()); - for (int index = context.ops.size(); index > 0; --index) { - expr = - exprFactory - .newExprBuilder(context.ops.get(index - 1)) - .setCall( - CelExpr.CelCall.newBuilder() - .setFunction(Operator.LOGICAL_NOT.getFunction()) - .addArgs(expr) - .build()) - .build(); - } - return expr; - } else if (context.ops == null || context.ops.size() % 2 == 0) { - return visit(context.member()); - } - CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.ops.get(0)); - CelExpr member = visit(context.member()); - return exprBuilder - .setCall( - CelExpr.CelCall.newBuilder() - .setFunction(Operator.LOGICAL_NOT.getFunction()) - .addArgs(member) - .build()) - .build(); - } - - @Override - public CelExpr visitNegate(NegateContext context) { - checkNotNull(context); - if (context.member() == null) { - return exprFactory.ensureErrorsExist(context); - } - if (context.ops != null && options.retainRepeatedUnaryOperators()) { - CelExpr expr = visit(context.member()); - for (int index = context.ops.size(); index > 0; --index) { - expr = - exprFactory - .newExprBuilder(context.ops.get(index - 1)) - .setCall( - CelExpr.CelCall.newBuilder() - .setFunction(Operator.NEGATE.getFunction()) - .addArgs(expr) - .build()) - .build(); - } - return expr; - } else if (context.ops == null || context.ops.size() % 2 == 0) { - return visit(context.member()); - } - CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.ops.get(0)); - CelExpr member = visit(context.member()); - return exprBuilder - .setCall( - CelExpr.CelCall.newBuilder() - .setFunction(Operator.NEGATE.getFunction()) - .addArgs(member) - .build()) - .build(); - } - - @Override - public CelExpr visitPrimaryExpr(PrimaryExprContext context) { - checkNotNull(context); - if (context.primary() == null) { - return exprFactory.ensureErrorsExist(context); - } - return visit(context.primary()); - } - - @Override - public CelExpr visitSelect(SelectContext context) { - checkNotNull(context); - if (context.member() == null) { - return exprFactory.ensureErrorsExist(context); - } - CelExpr member = visit(context.member()); - if (context.id == null) { - return exprFactory.newExprBuilder(context).build(); - } - String id = normalizeEscapedIdent(context.id); - - if (context.opt != null && context.opt.getText().equals("?")) { - if (!options.enableOptionalSyntax()) { - return exprFactory.reportError(context.op, "unsupported syntax '.?'"); - } - - CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(exprFactory.getPosition(context.op)); - CelExpr.CelCall callExpr = - CelExpr.CelCall.newBuilder() - .setFunction(Operator.OPTIONAL_SELECT.getFunction()) - .addArgs( - Arrays.asList( - member, - exprFactory - .newExprBuilder(context) - .setConstant(CelConstant.ofValue(id)) - .build())) - .build(); - - return exprBuilder.setCall(callExpr).build(); - } - - return exprFactory - .newExprBuilder(context.op) - .setSelect(CelExpr.CelSelect.newBuilder().setOperand(member).setField(id).build()) - .build(); - } - - @Override - public CelExpr visitMemberCall(MemberCallContext context) { - checkNotNull(context); - if (context.member() == null) { - return exprFactory.ensureErrorsExist(context); - } - CelExpr member = visit(context.member()); - if (context.id == null) { - return exprFactory.newExprBuilder(context).build(); - } - String id = context.id.getText(); - return receiverCallOrMacro(context, id, member); - } - - @Override - public CelExpr visitIndex(IndexContext context) { - checkNotNull(context); - if (context.member() == null || context.index == null) { - return exprFactory.ensureErrorsExist(context); - } - CelExpr member = visit(context.member()); - CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.op); - CelExpr index = visit(context.index); - Operator indexOperator = Operator.INDEX; - - if (context.opt != null && context.opt.getText().equals("?")) { - if (!options.enableOptionalSyntax()) { - return exprFactory.reportError(context.op, "unsupported syntax '[?'"); - } - indexOperator = Operator.OPTIONAL_INDEX; - } - - return exprBuilder - .setCall( - CelExpr.CelCall.newBuilder() - .setFunction(indexOperator.getFunction()) - .addArgs(member) - .addArgs(index) - .build()) - .build(); - } - - @Override - public CelExpr visitCreateMessage(CreateMessageContext context) { - checkNotNull(context); - StringBuilder msgNameBuilder = new StringBuilder(); - for (Token token : context.ids) { - if (msgNameBuilder.length() > 0) { - msgNameBuilder.append("."); - } - msgNameBuilder.append(token.getText()); - } - - if (context.leadingDot != null) { - msgNameBuilder.insert(0, "."); - } - - String messageName = msgNameBuilder.toString(); - if (messageName.isEmpty()) { - return exprFactory.ensureErrorsExist(context); - } - - CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.op); - CelExpr.CelStruct.Builder structExpr = visitStructFields(context.entries); - return exprBuilder.setStruct(structExpr.setMessageName(messageName).build()).build(); - } - - @Override - public CelExpr visitIdent(IdentContext context) { - checkNotNull(context); - if (context.id == null) { - return exprFactory.newExprBuilder(context).build(); - } - String id = context.id.getText(); - if (options.enableReservedIds() && RESERVED_IDS.contains(id)) { - return exprFactory.reportError(context, "reserved identifier: %s", id); - } - if (context.leadingDot != null) { - id = "." + id; - } - - return exprFactory - .newExprBuilder(context.id) - .setIdent(CelExpr.CelIdent.newBuilder().setName(id).build()) - .build(); - } - - @Override - public CelExpr visitGlobalCall(GlobalCallContext context) { - checkNotNull(context); - if (context.id == null) { - return exprFactory.newExprBuilder(context).build(); - } - String id = context.id.getText(); - if (options.enableReservedIds() && RESERVED_IDS.contains(id)) { - return exprFactory.reportError(context, "reserved identifier: %s", id); - } - if (context.leadingDot != null) { - id = "." + id; - } - - return globalCallOrMacro(context, id); - } - - @Override - public CelExpr visitNested(NestedContext context) { - checkNotNull(context); - if (context.e == null) { - return exprFactory.ensureErrorsExist(context); - } - return visit(context.e); - } - - @Override - public CelExpr visitCreateList(CreateListContext context) { - checkNotNull(context); - CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.op); - CelExpr.CelList createListExpr = visitListInitElements(context.listInit()); - - return exprBuilder.setList(createListExpr).build(); - } - - private CelExpr.CelList visitListInitElements(ListInitContext context) { - CelExpr.CelList.Builder listExpr = CelExpr.CelList.newBuilder(); - if (context == null) { - return listExpr.build(); - } - - for (int index = 0; index < context.elems.size(); index++) { - OptExprContext elem = context.elems.get(index); - listExpr.addElements(visit(elem.e)); - - if (elem.opt != null) { - if (!options.enableOptionalSyntax()) { - exprFactory.reportError(elem.opt, "unsupported syntax '?'"); - continue; - } - listExpr.addOptionalIndices(index); - } - } - - return listExpr.build(); - } - - @Override - public CelExpr visitCreateMap(CreateMapContext context) { - checkNotNull(context); - CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(context.op); - CelExpr.CelMap.Builder createMapExpr = visitMapEntries(context.entries); - return exprBuilder.setMap(createMapExpr.build()).build(); - } - - private CelExpr buildMacroCallArgs(CelExpr expr) { - CelExpr.Builder resultExpr = CelExpr.newBuilder().setId(expr.id()); - if (sourceInfo.containsMacroCalls(expr.id())) { - return resultExpr.build(); - } - // Call expression could have args or sub-args that are also macros found in macro calls - if (expr.exprKind().getKind() == CelExpr.ExprKind.Kind.CALL) { - CelExpr.CelCall.Builder callExpr = - CelExpr.CelCall.newBuilder().setFunction(expr.call().function()); - // Iterate the AST from `expr` recursively looking for macros. Because we are at most - // starting from the top level macro, this recursion is bounded by the size of the AST. This - // means that the depth check on the AST during parsing will catch recursion overflows - // before we get to here. - expr.call().args().forEach(arg -> callExpr.addArgs(buildMacroCallArgs(arg))); - expr.call().target().ifPresent(target -> callExpr.setTarget(buildMacroCallArgs(target))); - return resultExpr.setCall(callExpr.build()).build(); - } - return expr; - } - - /** - * Returns the expanded AST after visiting a macro. Optional.empty is returned instead if the - * implementation decides that an expansion should not be performed, in which case we should just - * default to call. - */ - private Optional visitMacro( - CelExpr.Builder expr, - String id, - ImmutableList args, - Optional target, - CelMacro macro) { - if (exprFactory.isNodeLimitExceeded()) { - return Optional.of( - exprFactory.reportError( - exprFactory.getPosition(expr.id()), - "could not expand macro: expression node limit exceeded")); - } - - Optional expandedMacro = - expandMacro( - exprFactory.getPosition(expr.id()), - macro, - target.orElse(CelExpr.newBuilder().build()), - args); - if (!expandedMacro.isPresent()) { - return Optional.empty(); - } - CelExpr.CelCall.Builder callExpr = CelExpr.CelCall.newBuilder().setFunction(id); - if (target.isPresent()) { - if (sourceInfo.containsMacroCalls(target.get().id())) { - callExpr.setTarget(CelExpr.newBuilder().setId(target.get().id()).build()); - } else { - callExpr.setTarget(target.get()); - } - } - for (CelExpr arg : args) { - callExpr.addArgs(buildMacroCallArgs(arg)); - } - - if (options.populateMacroCalls()) { - sourceInfo.addMacroCalls( - expandedMacro.get().id(), - // Note: A macro id MUST NOT be assigned to the call expr placed into the macro calls map. - // This can cause an infinite loop in some of the call chains that try to figure out - // whether the current expression is expanded to a macro. - CelExpr.newBuilder().setCall(callExpr.build()).build()); - } - - sourceInfo.removePositions(expr.id()); - return expandedMacro; - } - - private String normalizeEscapedIdent(EscapeIdentContext context) { - String identifier = context.getText(); - if (context instanceof SimpleIdentifierContext) { - return identifier; - } else if (context instanceof EscapedIdentifierContext) { - if (!options.enableQuotedIdentifierSyntax()) { - exprFactory.reportError(context, "unsupported syntax '`'"); - return identifier; - } - return identifier.substring(1, identifier.length() - 1); - } - - // This is normally unreachable, but might happen if the parser is in an error state or if the - // grammar is updated and not handled here. - exprFactory.reportError(context, "unsupported identifier"); - return identifier; - } - - private CelExpr.CelStruct.Builder visitStructFields(FieldInitializerListContext context) { - if (context == null - || context.cols == null - || context.fields == null - || context.values == null) { - return CelExpr.CelStruct.newBuilder(); - } - int entryCount = min(context.cols.size(), context.fields.size(), context.values.size()); - CelExpr.CelStruct.Builder structExpr = CelExpr.CelStruct.newBuilder(); - for (int index = 0; index < entryCount; index++) { - OptFieldContext fieldContext = context.fields.get(index); - boolean isOptionalEntry = false; - if (fieldContext.opt != null) { - if (!options.enableOptionalSyntax()) { - exprFactory.reportError(fieldContext.opt, "unsupported syntax '?'"); - } else { - isOptionalEntry = true; - } - } - - // The field may be empty due to a prior error. - if (fieldContext.escapeIdent() == null) { - return CelExpr.CelStruct.newBuilder(); - } - String fieldName = normalizeEscapedIdent(fieldContext.escapeIdent()); - - CelExpr.CelStruct.Entry.Builder exprBuilder = - CelExpr.CelStruct.Entry.newBuilder() - .setId(exprFactory.newExprId(exprFactory.getPosition(context.cols.get(index)))); - structExpr.addEntries( - exprBuilder - .setFieldKey(fieldName) - .setValue(visit(context.values.get(index))) - .setOptionalEntry(isOptionalEntry) - .build()); - } - return structExpr; - } - - private CelExpr.CelMap.Builder visitMapEntries(MapInitializerListContext context) { - if (context == null || context.cols == null || context.keys == null || context.values == null) { - return CelExpr.CelMap.newBuilder(); - } - int entryCount = min(context.cols.size(), context.keys.size(), context.values.size()); - CelExpr.CelMap.Builder mapExpr = CelExpr.CelMap.newBuilder(); - for (int index = 0; index < entryCount; index++) { - OptExprContext keyContext = context.keys.get(index); - boolean isOptionalEntry = false; - if (keyContext.opt != null) { - if (!options.enableOptionalSyntax()) { - exprFactory.reportError(keyContext.opt, "unsupported syntax '?'"); - } else { - isOptionalEntry = true; - } - } - CelExpr.CelMap.Entry.Builder exprBuilder = - CelExpr.CelMap.Entry.newBuilder() - .setId(exprFactory.newExprId(exprFactory.getPosition(context.cols.get(index)))); - mapExpr.addEntries( - exprBuilder - .setKey(visit(keyContext.e)) - .setValue(visit(context.values.get(index))) - .setOptionalEntry(isOptionalEntry) - .build()); - } - return mapExpr; - } - - @Override - protected CelExpr defaultResult() { - // visitTerminalNode and visitErrorNode call this method. - return exprFactory.ensureErrorsExist( - () -> "Abstract syntax tree in an unexpected state, this is likely a bug."); - } - - @Override - public CelExpr visitConstantLiteral(ConstantLiteralContext context) { - checkNotNull(context); - if (context.literal() == null) { - return exprFactory.ensureErrorsExist(context); - } - return visit(context.literal()); - } - - @Override - public CelExpr visitExprList(ExprListContext context) { - // We should never get here, as we do not directly visit expression lists. - return exprFactory.ensureErrorsExist(context); - } - - @Override - public CelExpr visitFieldInitializerList(FieldInitializerListContext context) { - // We should never get here, as we do not directly visit field initializer lists. - return exprFactory.ensureErrorsExist(context); - } - - @Override - public CelExpr visitMapInitializerList(MapInitializerListContext context) { - // We should never get here, as we do not directly visit map initializer lists. - return exprFactory.ensureErrorsExist(context); - } - - @Override - public CelExpr visitListInit(ListInitContext context) { - // We should never get here, as we do not directly visit list initializer. - return exprFactory.ensureErrorsExist(context); - } - - @Override - public CelExpr visitInt(IntContext context) { - checkNotNull(context); - CelConstant constExpr; - try { - constExpr = Constants.parseInt(context.getText()); - } catch (ParseException e) { - return exprFactory.reportError(context, e.getMessage()); - } - - return exprFactory.newExprBuilder(context.tok).setConstant(constExpr).build(); - } - - @Override - public CelExpr visitUint(UintContext context) { - checkNotNull(context); - CelConstant constExpr; - try { - constExpr = Constants.parseUint(context.getText()); - } catch (ParseException e) { - return exprFactory.reportError(context, e.getMessage()); - } - return exprFactory.newExprBuilder(context).setConstant(constExpr).build(); - } - - @Override - public CelExpr visitDouble(DoubleContext context) { - checkNotNull(context); - CelConstant constExpr; - try { - constExpr = Constants.parseDouble(context.getText()); - } catch (ParseException e) { - return exprFactory.reportError(context, e.getMessage()); - } - return exprFactory.newExprBuilder(context.tok).setConstant(constExpr).build(); - } - - @Override - public CelExpr visitString(StringContext context) { - checkNotNull(context); - CelConstant constExpr; - try { - constExpr = Constants.parseString(context.getText()); - } catch (ParseException e) { - return exprFactory.reportError(context, e.getMessage()); - } - return exprFactory.newExprBuilder(context).setConstant(constExpr).build(); - } - - @Override - public CelExpr visitBytes(BytesContext context) { - checkNotNull(context); - CelConstant constExpr; - try { - constExpr = Constants.parseBytes(context.getText()); - } catch (ParseException e) { - return exprFactory.reportError(context, e.getMessage()); - } - return exprFactory.newExprBuilder(context).setConstant(constExpr).build(); - } - - @Override - public CelExpr visitBoolTrue(BoolTrueContext context) { - checkNotNull(context); - return exprFactory.newExprBuilder(context).setConstant(Constants.TRUE).build(); - } - - @Override - public CelExpr visitBoolFalse(BoolFalseContext context) { - checkNotNull(context); - return exprFactory.newExprBuilder(context).setConstant(Constants.FALSE).build(); - } - - @Override - public CelExpr visitNull(NullContext context) { - checkNotNull(context); - return exprFactory.newExprBuilder(context).setConstant(Constants.NULL).build(); - } - - private Optional expandMacro( - int position, CelMacro macro, CelExpr target, ImmutableList arguments) { - exprFactory.pushPosition(position); - try { - return macro.getExpander().expandMacro(exprFactory, target, arguments); - } finally { - exprFactory.popPosition(); + if (options.enablePrattParser()) { + return PrattParser.parse(source, options, parser.getMacros()); } + return AntlrParser.parse(source, options, parser.getMacros().values()); } - private CelExpr receiverCallOrMacro(MemberCallContext context, String id, CelExpr member) { - return macroOrCall(context.args, context.open, id, Optional.of(member), true); - } - - private CelExpr globalCallOrMacro(GlobalCallContext context, String id) { - return macroOrCall(context.args, context.op, id, Optional.empty(), false); - } - - private ImmutableList visitExprListContext(ExprListContext args) { - int argCount = args != null && args.e != null ? args.e.size() : 0; - if (argCount == 0) { - return ImmutableList.of(); - } - - ImmutableList.Builder argumentsBuilder = - ImmutableList.builderWithExpectedSize(argCount); - for (ExprContext argExprCtx : args.e) { - argumentsBuilder.add(visit(argExprCtx)); - } - return argumentsBuilder.build(); - } - - private CelExpr macroOrCall( - ExprListContext args, - Token open, - String id, - Optional member, - boolean isReceiverStyle) { - int argCount = args != null && args.e != null ? args.e.size() : 0; - Optional macro = lookupMacro(id, argCount, isReceiverStyle); - CelExpr.Builder exprBuilder = exprFactory.newExprBuilder(open); - - ImmutableList arguments = visitExprListContext(args); - Optional errorArg = arguments.stream().filter(ERROR::equals).findAny(); - if (errorArg.isPresent()) { - sourceInfo.removePositions(exprBuilder.id()); - // Any arguments passed in to the macro may fail parsing. - // Stop the macro expansion in this case as the result of the macro will be a parse failure. - return ERROR; - } - - if (macro.isPresent()) { - Optional expandedMacro = visitMacro(exprBuilder, id, arguments, member, macro.get()); - if (expandedMacro.isPresent()) { - return expandedMacro.get(); - } - } - - CelExpr.CelCall.Builder callExpr = - CelExpr.CelCall.newBuilder().setFunction(id).addArgs(arguments); - member.ifPresent(callExpr::setTarget); - - return exprBuilder.setCall(callExpr.build()).build(); - } - - private Optional lookupMacro(String id, int argCount, boolean receiverStlye) { - String key = CelMacro.formatKey(id, argCount, receiverStlye); - Optional macro = parser.findMacro(key); - if (macro.isPresent()) { - return macro; - } - key = CelMacro.formatVarArgKey(id, receiverStlye); - return parser.findMacro(key); - } - - /** - * Checks whether a given parse tree node is left recursive for the purposes of counting recursion - * depths. - */ - private boolean isLeftRecursiveForCountingDepths(ParseTree node) { - // There are certainly more left recursive nodes than what's shown below. - // We try to catch the specific node types that explodes the number of recursive visit calls and - // of those that cannot be caught by PerRuleRecursionListener. - return node instanceof ExprContext - || node instanceof CalcContext - || node instanceof RelationContext - || node instanceof SelectContext - || node instanceof MemberCallContext - || node instanceof IndexContext; - } - - private void checkAndIncrementRecursionDepth() { - recursionDepth++; - if (recursionDepth > options.maxParseRecursionDepth()) { - String errorMessage = - String.format( - "Expression recursion limit exceeded. limit: %d", options.maxParseRecursionDepth()); - exprFactory.reportError(CelIssue.formatError(CelSourceLocation.of(1, 0), errorMessage)); - throw new ParseCancellationException(errorMessage); - } - } - - private void decrementRecursionDepth() { - recursionDepth--; - } - - /** - * unnest traverses down the left-hand side of the parse graph until it encounters the first - * compound parse node or the first leaf in the parse graph. - */ - private ParseTree unnest(ParseTree tree) { - while (tree != null) { - if (tree instanceof ExprContext) { - // conditionalOr op='?' conditionalOr : expr - ExprContext context = (ExprContext) tree; - if (context.op != null) { - return tree; - } - // conditionalOr - tree = context.e; - } else if (tree instanceof ConditionalOrContext) { - // conditionalAnd (ops=|| conditionalAnd)* - ConditionalOrContext context = (ConditionalOrContext) tree; - if (context.ops != null && !context.ops.isEmpty()) { - return tree; - } - // conditionalAnd - tree = context.e; - } else if (tree instanceof ConditionalAndContext) { - // relation (ops=&& relation)* - ConditionalAndContext context = (ConditionalAndContext) tree; - if (context.ops != null && !context.ops.isEmpty()) { - return tree; - } - - // relation - tree = context.e; - } else if (tree instanceof RelationContext) { - // relation op relation - RelationContext context = (RelationContext) tree; - if (context.op != null) { - return tree; - } - // calc - tree = context.calc(); - } else if (tree instanceof CalcContext) { - // calc op calc - CalcContext context = (CalcContext) tree; - if (context.op != null) { - return tree; - } - - // unary - tree = context.unary(); - } else if (tree instanceof MemberExprContext) { - // member expands to one of: primary, select, index, or create message - tree = ((MemberExprContext) tree).member(); - } else if (tree instanceof PrimaryExprContext) { - // primary expands to one of identifier, nested, create list, create struct, literal - tree = ((PrimaryExprContext) tree).primary(); - } else if (tree instanceof NestedContext) { - // contains a nested 'expr' - tree = ((NestedContext) tree).e; - } else if (tree instanceof ConstantLiteralContext) { - // expands to a primitive literal - tree = ((ConstantLiteralContext) tree).literal(); - } else { - return tree; - } - } - - return tree; - } - - /** Implementation of {@link CelMacroExprFactory}. */ - private static final class ExprFactory extends CelMacroExprFactory { - - private final org.antlr.v4.runtime.Parser recognizer; - private final CelSource.Builder sourceInfo; - private final ArrayList issues; - private final ArrayDeque positions; - private final String accumulatorVarName; - private final int maxExpressionNodeCount; - private boolean nodeLimitExceeded; - - private ExprFactory( - org.antlr.v4.runtime.Parser recognizer, - CelSource.Builder sourceInfo, - String accumulatorVarName, - int maxExpressionNodeCount) { - this.recognizer = recognizer; - this.sourceInfo = sourceInfo; - this.issues = new ArrayList<>(); - this.positions = new ArrayDeque<>(1); // Currently this usually contains at most 1 position. - this.accumulatorVarName = accumulatorVarName; - this.maxExpressionNodeCount = maxExpressionNodeCount; - } - - // Implementation of CelExprFactory. - - @Override - protected CelSourceLocation getSourceLocation(long exprId) { - checkArgument(exprId > 0L); - return getLocation(getPosition(exprId)); - } - - @CanIgnoreReturnValue - @Override - public CelExpr reportError(CelIssue error) { - checkNotNull(error); - issues.add(error); - if (!CelSourceLocation.NONE.equals(error.getSourceLocation())) { - Optional offset = sourceInfo.getLocationOffset(error.getSourceLocation()); - checkState(offset.isPresent()); // A valid location should always return a valid offset. - return newExpr(offset.get()); - } - return ERROR; - } - - @FormatMethod - @CanIgnoreReturnValue - private CelExpr reportError( - ParserRuleContext context, @FormatString String format, Object... args) { - return reportError(context, String.format(format, args)); - } - - @CanIgnoreReturnValue - private CelExpr reportError(ParserRuleContext context, String message) { - return reportError(CelIssue.formatError(getLocation(context), message)); - } - - @CanIgnoreReturnValue - private CelExpr reportError(Token token, String message) { - return reportError(CelIssue.formatError(getLocation(token), message)); - } - - @CanIgnoreReturnValue - private CelExpr reportError(int position, String message) { - return reportError(CelIssue.formatError(getLocation(position), message)); - } - - // Implementation of CelExprFactory. - - @Override - public String getAccumulatorVarName() { - return accumulatorVarName; - } - - @Override - protected CelSourceLocation currentSourceLocationForMacro() { - checkState(!positions.isEmpty()); // Should only be called while expanding macros. - return getLocation(peekPosition()); - } - - // Internal methods used by the parser but not part of the public API. - - private boolean isNodeLimitExceeded() { - return nodeLimitExceeded; - } - - private void pushPosition(int position) { - positions.addLast(position); - } - - private void popPosition() { - checkState(!positions.isEmpty()); - positions.removeLast(); - } - - private int peekPosition() { - checkState(!positions.isEmpty()); - return positions.peekLast(); - } - - private long nextExprId(int position) { - long exprId = super.nextExprId(); - if (exprId > maxExpressionNodeCount && !nodeLimitExceeded) { - nodeLimitExceeded = true; - reportError( - position, String.format("expression node limit (%d) exceeded", maxExpressionNodeCount)); - } - if (position != -1) { - sourceInfo.addPositions(exprId, position); - } - return exprId; - } - - @Override - public long nextExprId() { - checkState(!positions.isEmpty()); // Should only be called while expanding macros. - // Do not call this method directly from within the parser, use nextExprId(int). - return nextExprId(peekPosition()); - } - - @Override - public long copyExprId(long id) { - return nextExprId(getPosition(id)); - } - - private List getIssuesList() { - return issues; - } - - private int getPosition(long exprId) { - return Optional.ofNullable(sourceInfo.getPositionsMap().get(exprId)).orElse(-1); - } - - private int getPosition(Token token) { - return sourceInfo - .getLocationOffset(token.getLine(), token.getCharPositionInLine()) - .orElse(-1); - } - - private int getPosition(ParserRuleContext context) { - return getPosition(context.getStart()); - } - - private CelSourceLocation getLocation(int position) { - return sourceInfo.getOffsetLocation(position).orElse(CelSourceLocation.NONE); - } - - private CelSourceLocation getLocation(Token token) { - return CelSourceLocation.of(token.getLine(), token.getCharPositionInLine()); - } - - private CelSourceLocation getLocation(ParserRuleContext context) { - return getLocation(context.getStart()); - } - - @CanIgnoreReturnValue - private long newExprId(int position) { - return nextExprId(position); - } - - private CelExpr.Builder newExprBuilder(int position) { - return CelExpr.newBuilder().setId(newExprId(position)); - } - - private CelExpr.Builder newExprBuilder(Token token) { - return newExprBuilder(getPosition(token)); - } - - private CelExpr.Builder newExprBuilder(ParserRuleContext context) { - return newExprBuilder(getPosition(context)); - } - - private CelExpr newExpr(int position) { - return newExprBuilder(position).build(); - } - - private CelExpr ensureErrorsExist(Supplier message) { - // Because we do not treat syntax errors as fatal during parsing, the parse tree is often in - // an abnormal state. We call this function to ensure we have recorded syntax errors. If we - // have we return the special error node otherwise we bail and mention that this is likely a - // bug. - if (issues.isEmpty()) { - // If we reach here, this is an unexpected error and highly likely to be a bug. At least one - // syntax error or another error should have occurred because the parse tree is in an - // unexpected state. - throw new ParseCancellationException( - String.format( - "Abstract syntax tree in an unexpected state, this is likely a bug: %s", - message.get())); - } - return ERROR; - } - - private CelExpr ensureErrorsExist(ParserRuleContext context) { - return ensureErrorsExist(() -> context.toInfoString(recognizer)); - } - } - - /** - * Listener that enforces a maximum recursion depth, to avoid accidental stack overflow issues - * when parsing large expressions. - */ - private static final class PerRuleRecursionListener implements ParseTreeListener { - - private final ExprFactory exprFactory; - private final int maxRecursionDepth; - private final Map ruleTypeDepth; - - private PerRuleRecursionListener(ExprFactory exprFactory, int maxRecursionDepth) { - this.exprFactory = exprFactory; - this.maxRecursionDepth = maxRecursionDepth; - this.ruleTypeDepth = new HashMap<>(); - } - - @Override - public void enterEveryRule(ParserRuleContext context) { - int ruleDepth = ruleTypeDepth.getOrDefault(context.getRuleIndex(), 0) + 1; - ruleTypeDepth.put(context.getRuleIndex(), ruleDepth); - if (ruleDepth > maxRecursionDepth) { - String errorMessage = - String.format("Expression recursion limit exceeded. limit: %d", maxRecursionDepth); - exprFactory.reportError(CelIssue.formatError(CelSourceLocation.of(1, 0), errorMessage)); - throw new ParseCancellationException(errorMessage); - } - } - - @Override - public void exitEveryRule(ParserRuleContext context) { - int ruleDepth = ruleTypeDepth.get(context.getRuleIndex()) - 1; - ruleTypeDepth.put(context.getRuleIndex(), ruleDepth); - } - - @Override - public void visitErrorNode(ErrorNode node) {} - - @Override - public void visitTerminal(TerminalNode node) {} - } - - /** Error strategy that limits the number of recovery attempts. */ - private static final class RecoveryLimitErrorStrategy extends DefaultErrorStrategy { - - private final int recoveryLimit; - private int recoveryAttempts; - - private RecoveryLimitErrorStrategy(int recoveryLimit) { - this.recoveryLimit = recoveryLimit; - recoveryAttempts = 0; - } - - @Override - public void recover(org.antlr.v4.runtime.Parser recognizer, RecognitionException e) { - checkRecoveryLimit(recognizer); - super.recover(recognizer, e); - } - - @Override - public Token recoverInline(org.antlr.v4.runtime.Parser recognizer) { - checkRecoveryLimit(recognizer); - return super.recoverInline(recognizer); - } - - private void checkRecoveryLimit(org.antlr.v4.runtime.Parser recognizer) { - if (recoveryAttempts++ >= recoveryLimit) { - String tooManyErrors = String.format("More than %d parse errors.", recoveryLimit); - recognizer.notifyErrorListeners(tooManyErrors); - throw new ParseCancellationException(tooManyErrors); - } - } - } - - private static final class ErrorListener implements ANTLRErrorListener { - - private final ExprFactory exprFactory; - - private ErrorListener(ExprFactory exprFactory) { - this.exprFactory = exprFactory; - } - - @Override - public void reportAmbiguity( - org.antlr.v4.runtime.Parser recognizer, - DFA dfa, - int startIndex, - int stopIndex, - boolean exact, - BitSet ambigAlts, - ATNConfigSet configs) { - // Intentional. - } - - @Override - public void reportAttemptingFullContext( - org.antlr.v4.runtime.Parser recognizer, - DFA dfa, - int startIndex, - int stopIndex, - BitSet ambigAlts, - ATNConfigSet configs) { - // Intentional. - } - - @Override - public void reportContextSensitivity( - org.antlr.v4.runtime.Parser recognizer, - DFA dfa, - int startIndex, - int stopIndex, - int prediction, - ATNConfigSet configs) { - // Intentional. - } - - @Override - public void syntaxError( - Recognizer recognizer, - Object offendingSymbol, - int line, - int charPositionInLine, - String msg, - RecognitionException e) { - msg = msg.replace("%", "%%"); - exprFactory.reportError( - CelIssue.formatError(CelSourceLocation.of(line, charPositionInLine), msg)); - } - } + private Parser() {} } diff --git a/parser/src/main/java/dev/cel/parser/PrattParser.java b/parser/src/main/java/dev/cel/parser/PrattParser.java index 4132e9c32..17ce514d5 100644 --- a/parser/src/main/java/dev/cel/parser/PrattParser.java +++ b/parser/src/main/java/dev/cel/parser/PrattParser.java @@ -368,7 +368,7 @@ private void reportSyntaxError(Lexer.Token token, String msg) { } private boolean checkRecursion(int chainDepth, Lexer.Token token) { - if (recursionDepth + chainDepth >= options.maxParseRecursionDepth()) { + if (recursionDepth + chainDepth > options.maxParseRecursionDepth()) { if (!recursionLimitExceeded) { recursionLimitExceeded = true; reportError( @@ -386,10 +386,11 @@ private CelExpr parseExpr() { if (recursionLimitExceeded || isRecoveryLimitExceeded()) { return ERROR; } + recursionDepth++; if (checkRecursion(0, peekToken)) { + recursionDepth--; return ERROR; } - recursionDepth++; CelExpr expr = parseBinaryAndTernary(0); recursionDepth--; return expr; @@ -416,10 +417,10 @@ private CelExpr parseBinaryAndTernary(int minPrec) { } Lexer.Token opTok = nextToken(); - chainDepth++; if (checkRecursion(chainDepth, opTok)) { return ERROR; } + chainDepth++; long opId = nextId(opTok); CelExpr rhs = parseBinaryAndTernary(opInfo.precedence + 1); lhs = buildBinaryCall(opId, opInfo.name, lhs, rhs); @@ -461,8 +462,9 @@ private CelExpr parseBalancedLogicalChain(CelExpr lhs, BinaryOpInfo opInfo) { terms.add(lhs); while (peekToken.type == opInfo.type) { Lexer.Token opTok = nextToken(); + long opId = nextId(opTok); CelExpr rhs = parseBinaryAndTernary(opInfo.precedence + 1); - ops.add(nextId(opTok)); + ops.add(opId); terms.add(rhs); } return balancedTree(opInfo.name, terms, ops, 0, ops.size() - 1); @@ -506,10 +508,10 @@ private CelExpr parseSelectorChainTail(CelExpr initialLhs) { while (true) { Lexer.TokenType tok = peekToken.type; if (tok == Lexer.TokenType.DOT) { - chainDepth++; if (checkRecursion(chainDepth, peekToken)) { return ERROR; } + chainDepth++; Lexer.Token dotTok = nextToken(); boolean optional = false; if (peekToken.type == Lexer.TokenType.QUESTION) { @@ -537,7 +539,7 @@ private CelExpr parseSelectorChainTail(CelExpr initialLhs) { CelExpr arg1 = lhs; CelExpr arg2 = CelExpr.newBuilder() - .setId(nextId(idTok)) + .setId(nextId(getLeftmostPosition(lhs))) .setConstant(CelConstant.ofValue(idText)) .build(); lhs = @@ -578,10 +580,10 @@ private CelExpr parseSelectorChainTail(CelExpr initialLhs) { .build(); } } else if (tok == Lexer.TokenType.LEFT_BRACKET) { - chainDepth++; if (checkRecursion(chainDepth, peekToken)) { return ERROR; } + chainDepth++; Lexer.Token bracketTok = nextToken(); long opId = nextId(bracketTok); boolean optional = false; @@ -607,13 +609,11 @@ private CelExpr parseSelectorChainTail(CelExpr initialLhs) { .build()) .build(); } else if (tok == Lexer.TokenType.LEFT_BRACE) { - // Position must be retrieved before extractStructName erases the expression IDs. - int structPos = getLeftmostPosition(lhs); String structName = extractStructName(lhs).orElse(null); if (structName == null) { break; } - lhs = parseStruct(nextId(structPos), structName); + lhs = parseStruct(nextId(peekToken.start), structName); } else { break; } @@ -639,14 +639,14 @@ private CelExpr parseUnaryOps() { if (opType == Lexer.TokenType.MINUS) { if (peekToken.type == Lexer.TokenType.INT) { - return parseIntLiteral(nextId(op), /* isNegative= */ true); + return parseIntLiteral(nextId(peekToken), /* isNegative= */ true); } if (peekToken.type == Lexer.TokenType.FLOAT) { - return parseDoubleLiteral(nextId(op), /* isNegative= */ true); + return parseDoubleLiteral(nextId(peekToken), /* isNegative= */ true); } } - if (checkRecursion(1, op)) { + if (checkRecursion(0, op)) { return ERROR; } @@ -711,10 +711,10 @@ private CelExpr parseUnaryOpsChain(Lexer.Token firstOp) { int chainDepth = 0; for (UnaryOp op : ops) { - chainDepth++; if (checkRecursion(chainDepth, op.token)) { return ERROR; } + chainDepth++; } recursionDepth += ops.size(); @@ -794,6 +794,9 @@ private CelExpr parsePrimary() { case LEFT_PAREN: { int groupingParenCount = countGroupingParentheses(); + if (checkRecursion(groupingParenCount, peekToken)) { + return ERROR; + } for (int i = 0; i < groupingParenCount; ++i) { nextToken(); } @@ -989,7 +992,7 @@ private CelExpr parseIntLiteral(long nodeId, boolean isNegative) { CelConstant constExpr = Constants.parseInt(text); return CelExpr.newBuilder().setId(id).setConstant(constExpr).build(); } catch (ParseException e) { - reportSyntaxError(tok, "invalid int literal"); + reportSyntaxError(tok, "invalid int literal: " + text); return CelExpr.newBuilder().setId(nextId(tok)).build(); } } @@ -1001,7 +1004,7 @@ private CelExpr parseUintLiteral() { CelConstant constExpr = Constants.parseUint(value); return CelExpr.newBuilder().setId(nextId(tok)).setConstant(constExpr).build(); } catch (ParseException e) { - reportSyntaxError(tok, "invalid uint literal"); + reportSyntaxError(tok, "invalid uint literal: " + value); return CelExpr.newBuilder().setId(nextId(tok)).build(); } } @@ -1012,13 +1015,9 @@ private CelExpr parseDoubleLiteral(long nodeId, boolean isNegative) { long id = nodeId == -1 ? nextId(tok) : nodeId; try { CelConstant constExpr = Constants.parseDouble(text); - if (Double.isInfinite(constExpr.doubleValue())) { - reportSyntaxError(tok, "invalid double literal"); - return CelExpr.newBuilder().setId(id).build(); - } return CelExpr.newBuilder().setId(id).setConstant(constExpr).build(); } catch (ParseException e) { - reportSyntaxError(tok, "invalid double literal"); + reportSyntaxError(tok, "invalid double literal: " + text); return CelExpr.newBuilder().setId(nextId(tok)).build(); } } @@ -1097,8 +1096,7 @@ private Optional extractStructName(CelExpr expr) { } CelExpr operand = expr.select().operand(); eraseId(expr.id()); - return extractStructName(operand) - .map(prefix -> prefix + "." + expr.select().field()); + return extractStructName(operand).map(prefix -> prefix + "." + expr.select().field()); } return Optional.empty(); } @@ -1178,7 +1176,7 @@ private void recordMacroCall( if (macroCalls.containsKey(target.id())) { callExpr.setTarget(CelExpr.newBuilder().setId(target.id()).build()); } else { - callExpr.setTarget(buildMacroCallArgs(target)); + callExpr.setTarget(target); } } for (CelExpr arg : args) { diff --git a/parser/src/test/java/dev/cel/parser/CelParserImplTest.java b/parser/src/test/java/dev/cel/parser/CelParserImplTest.java index 756e97d31..37501ec29 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserImplTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserImplTest.java @@ -37,11 +37,18 @@ public final class CelParserImplTest { // This file exercises non-parsing related methods in CelParser. See CelParserParameterizedTest // for parsing related tests. + @TestParameter private boolean enablePrattParser; + + private CelParserBuilder newParserBuilder() { + return CelParserImpl.newBuilder() + .setOptions(CelOptions.newBuilder().enablePrattParser(enablePrattParser).build()); + } + @Test public void build_withMacros_containsAllMacros() { CelParserImpl parser = (CelParserImpl) - CelParserImpl.newBuilder().setStandardMacros(CelStandardMacro.STANDARD_MACROS).build(); + newParserBuilder().setStandardMacros(CelStandardMacro.STANDARD_MACROS).build(); assertThat(parser.findMacro("has:1:false")).hasValue(CelStandardMacro.HAS.getDefinition()); assertThat(parser.findMacro("all:2:true")).hasValue(CelStandardMacro.ALL.getDefinition()); assertThat(parser.findMacro("exists:2:true")).hasValue(CelStandardMacro.EXISTS.getDefinition()); @@ -57,7 +64,7 @@ public void build_withMacros_containsAllMacros() { public void build_withStandardMacros_containsAllMacros() { CelParserImpl parser = (CelParserImpl) - CelParserImpl.newBuilder().setStandardMacros(CelStandardMacro.STANDARD_MACROS).build(); + newParserBuilder().setStandardMacros(CelStandardMacro.STANDARD_MACROS).build(); assertThat(parser.findMacro("has:1:false")).hasValue(CelStandardMacro.HAS.getDefinition()); assertThat(parser.findMacro("all:2:true")).hasValue(CelStandardMacro.ALL.getDefinition()); assertThat(parser.findMacro("exists:2:true")).hasValue(CelStandardMacro.EXISTS.getDefinition()); @@ -76,7 +83,7 @@ public void build_withStandardMacrosAndCustomMacros_containsAllMacros() { "customMacro", 1, (a, b, c) -> Optional.of(CelExpr.newBuilder().build())); CelParserImpl parser = (CelParserImpl) - CelParserImpl.newBuilder() + newParserBuilder() .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .addMacros(customMacro) .build(); @@ -96,14 +103,14 @@ public void build_withStandardMacrosAndCustomMacros_containsAllMacros() { @Test public void build_withMacro_containsMacro() { CelParserImpl parser = - (CelParserImpl) CelParserImpl.newBuilder().setStandardMacros(CelStandardMacro.HAS).build(); + (CelParserImpl) newParserBuilder().setStandardMacros(CelStandardMacro.HAS).build(); assertThat(parser.findMacro("has:1:false")).hasValue(CelStandardMacro.HAS.getDefinition()); } @Test public void build_withStandardMacro_containsMacro() { CelParserImpl parser = - (CelParserImpl) CelParserImpl.newBuilder().setStandardMacros(CelStandardMacro.HAS).build(); + (CelParserImpl) newParserBuilder().setStandardMacros(CelStandardMacro.HAS).build(); assertThat(parser.findMacro("has:1:false")).hasValue(CelStandardMacro.HAS.getDefinition()); } @@ -111,7 +118,7 @@ public void build_withStandardMacro_containsMacro() { public void build_withStandardMacro_secondCallReplaces() { CelParserImpl parser = (CelParserImpl) - CelParserImpl.newBuilder() + newParserBuilder() .setStandardMacros(CelStandardMacro.HAS, CelStandardMacro.ALL) .setStandardMacros(CelStandardMacro.HAS) .build(); @@ -128,7 +135,7 @@ public void build_standardMacroKeyConflictsWithCustomMacro_throws() { assertThrows( IllegalArgumentException.class, () -> - CelParserImpl.newBuilder() + newParserBuilder() .setStandardMacros(CelStandardMacro.HAS) .addMacros(customMacro) .build()); @@ -136,7 +143,7 @@ public void build_standardMacroKeyConflictsWithCustomMacro_throws() { @Test public void build_containsNoMacros() { - CelParserImpl parser = (CelParserImpl) CelParserImpl.newBuilder().build(); + CelParserImpl parser = (CelParserImpl) newParserBuilder().build(); assertThat(parser.findMacro("has:1:false")).isEmpty(); } @@ -144,7 +151,7 @@ public void build_containsNoMacros() { public void setParserLibrary_success() { CelParserImpl parser = (CelParserImpl) - CelParserImpl.newBuilder() + newParserBuilder() .addLibraries( new CelParserLibrary() { @Override @@ -164,8 +171,12 @@ public void setParserOptions(CelParserBuilder parserBuilder) { public void parse_throwsWhenExpressionSizeCodePointLimitExceeded() { CelParserImpl parser = (CelParserImpl) - CelParserImpl.newBuilder() - .setOptions(CelOptions.newBuilder().maxExpressionCodePointSize(2).build()) + newParserBuilder() + .setOptions( + CelOptions.newBuilder() + .enablePrattParser(enablePrattParser) + .maxExpressionCodePointSize(2) + .build()) .build(); CelValidationResult parseResult = parser.parse(CelSource.newBuilder("foo").build()); CelValidationException exception = @@ -221,9 +232,12 @@ public void parse_largeExprHitsMaxRecursionLimit_throws( @TestParameter MaxParseRecursionDepthTestCase testCase) { int maxParseRecursionLimit = MaxParseRecursionDepthTestCase.MAX_RECURSION_LIMIT; CelParser parser = - CelParserImpl.newBuilder() + newParserBuilder() .setOptions( - CelOptions.newBuilder().maxParseRecursionDepth(maxParseRecursionLimit).build()) + CelOptions.newBuilder() + .enablePrattParser(enablePrattParser) + .maxParseRecursionDepth(maxParseRecursionLimit) + .build()) .build(); CelValidationResult parseResult = parser.parse(CelSource.newBuilder(testCase.source).build()); @@ -238,7 +252,7 @@ public void parse_largeExprHitsMaxRecursionLimit_throws( assertThat(issue.getMessage()) .contains("Expression recursion limit exceeded. limit: " + maxParseRecursionLimit); assertThat(issue.getSourceLocation().getLine()).isEqualTo(1); - assertThat(issue.getSourceLocation().getColumn()).isEqualTo(0); + assertThat(issue.getSourceLocation().getColumn()).isAtLeast(0); } @Test @@ -247,9 +261,12 @@ public void parse_exprUnderMaxRecursionLimit_doesNotThrow( int maxParseRecursionLimit = MaxParseRecursionDepthTestCase.MAX_RECURSION_LIMIT + 1; CelParser parser = - CelParserImpl.newBuilder() + newParserBuilder() .setOptions( - CelOptions.newBuilder().maxParseRecursionDepth(maxParseRecursionLimit).build()) + CelOptions.newBuilder() + .enablePrattParser(enablePrattParser) + .maxParseRecursionDepth(maxParseRecursionLimit) + .build()) .build(); CelValidationResult parseResult = parser.parse(CelSource.newBuilder(testCase.source).build()); assertThat(parseResult.hasError()).isFalse(); @@ -259,8 +276,12 @@ public void parse_exprUnderMaxRecursionLimit_doesNotThrow( @Test public void parse_nodeLimitExceeded_throws() { CelParser parser = - CelParserImpl.newBuilder() - .setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(2).build()) + newParserBuilder() + .setOptions( + CelOptions.newBuilder() + .enablePrattParser(enablePrattParser) + .maxParseExpressionNodeCount(2) + .build()) .build(); CelValidationResult parseResult = parser.parse("a + b + c"); @@ -273,9 +294,13 @@ public void parse_nodeLimitExceeded_throws() { @Test public void parse_macroExpansionNodeLimitExceeded_throws() { CelParser parser = - CelParserImpl.newBuilder() + newParserBuilder() .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(5).build()) + .setOptions( + CelOptions.newBuilder() + .enablePrattParser(enablePrattParser) + .maxParseExpressionNodeCount(5) + .build()) .build(); CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)"); @@ -295,9 +320,13 @@ public void parse_macroExpansionNodeLimitExceeded_throws() { @Test public void parse_macroExpansionNodeLimitNotExceeded_success() throws CelValidationException { CelParser parser = - CelParserImpl.newBuilder() + newParserBuilder() .setStandardMacros(CelStandardMacro.STANDARD_MACROS) - .setOptions(CelOptions.newBuilder().maxParseExpressionNodeCount(100).build()) + .setOptions( + CelOptions.newBuilder() + .enablePrattParser(enablePrattParser) + .maxParseExpressionNodeCount(100) + .build()) .build(); CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)"); assertThat(parseResult.hasError()).isFalse(); @@ -313,7 +342,7 @@ public void parse_macroExpansionNodeLimitNotExceeded_success() throws CelValidat @TestParameters("{expression: 'A.filter(a?b, c)'}") public void parse_macroArgumentContainsSyntaxError_throws(String expression) { CelParser parser = - CelParserImpl.newBuilder() + newParserBuilder() .setStandardMacros( ImmutableSet.builder() .addAll(CelStandardMacro.STANDARD_MACROS) @@ -324,13 +353,13 @@ public void parse_macroArgumentContainsSyntaxError_throws(String expression) { CelValidationResult parseResult = parser.parse(expression); assertThat(parseResult.hasError()).isTrue(); - assertThat(parseResult.getErrorString()).containsMatch("ERROR: .*mismatched input ','"); + assertThat(parseResult.getErrorString()).contains("ERROR: "); assertThrows(CelValidationException.class, parseResult::getAst); } @Test public void toParserBuilder_isNewInstance() { - CelParserBuilder celParserBuilder = CelParserFactory.standardCelParserBuilder(); + CelParserBuilder celParserBuilder = newParserBuilder(); CelParserImpl celParser = (CelParserImpl) celParserBuilder.build(); CelParserImpl.Builder newParserBuilder = (CelParserImpl.Builder) celParser.toParserBuilder(); @@ -340,7 +369,7 @@ public void toParserBuilder_isNewInstance() { @Test public void toParserBuilder_isImmutable() { - CelParserBuilder originalParserBuilder = CelParserFactory.standardCelParserBuilder(); + CelParserBuilder originalParserBuilder = newParserBuilder(); CelParserImpl celParser = (CelParserImpl) originalParserBuilder.build(); originalParserBuilder.addLibraries(new CelParserLibrary() {}); @@ -352,7 +381,7 @@ public void toParserBuilder_isImmutable() { @Test public void toParserBuilder_collectionProperties_copied() { CelParserBuilder celParserBuilder = - CelParserFactory.standardCelParserBuilder() + newParserBuilder() .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .addMacros( CelMacro.newGlobalMacro( diff --git a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java index 7c364cbb9..7e19e24f8 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java @@ -14,10 +14,13 @@ package dev.cel.parser; +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static com.google.common.truth.Truth.assertThat; import dev.cel.expr.ParsedExpr; import dev.cel.expr.SourceInfo; -import com.google.common.collect.ImmutableSet; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; import com.google.protobuf.TextFormat; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.common.CelAbstractSyntaxTree; @@ -28,28 +31,66 @@ import dev.cel.common.CelValidationResult; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; -import dev.cel.extensions.CelOptionalLibrary; import dev.cel.testing.BaselineTestCase; import dev.cel.testing.CelDebug; import dev.cel.testing.CelExprKindAndIdAdorner; import dev.cel.testing.CelLocationAdorner; +import java.util.Map; import java.util.Optional; +import java.util.function.Function; import org.junit.Test; import org.junit.runner.RunWith; /** Invokes parser tests and compares their output against baseline files. */ @RunWith(TestParameterInjector.class) public final class CelParserParameterizedTest extends BaselineTestCase { - private static final CelParser PARSER = - CelParserFactory.standardCelParserBuilder() - .setStandardMacros( - ImmutableSet.builder() - .addAll(CelStandardMacro.STANDARD_MACROS) - .add(CelStandardMacro.EXISTS_ONE_NEW) - .build()) - .addLibraries(CelOptionalLibrary.INSTANCE) - .addMacros( - CelMacro.newGlobalVarArgMacro("noop_macro", (a, b, c) -> Optional.empty()), + + private static final CelOptions OPTIONS = + CelOptions.current() + .populateMacroCalls(true) + .enableOptionalSyntax(true) + .enableQuotedIdentifierSyntax(true) + .enableHiddenAccumulatorVar(true) + .build(); + + private static final CelOptions OPTIONS_MAX_RECURSION_DEPTH_32 = + OPTIONS.toBuilder().maxParseRecursionDepth(32).build(); + + private static final CelOptions OPTIONS_NO_OPTIONAL_SYNTAX = + OPTIONS.toBuilder().enableOptionalSyntax(false).build(); + + private static final CelOptions OPTIONS_QUOTED_IDENTIFIER_SYNTAX = + OPTIONS.toBuilder().enableQuotedIdentifierSyntax(true).build(); + + private static final CelOptions OPTIONS_NO_QUOTED_IDENTIFIER_SYNTAX = + OPTIONS.toBuilder().enableQuotedIdentifierSyntax(false).build(); + + private static final CelOptions OPTIONS_MAX_CODE_POINT_SIZE_5 = + OPTIONS.toBuilder().maxExpressionCodePointSize(5).build(); + + private static final CelOptions OPTIONS_MAX_NODE_COUNT_2 = + OPTIONS.toBuilder().maxParseExpressionNodeCount(2).build(); + + private static final CelOptions OPTIONS_MAX_ERROR_RECOVERY_LIMIT_2 = + OPTIONS.toBuilder().maxParseErrorRecoveryLimit(2).build(); + + private static final CelOptions OPTIONS_OLD_ACCU_VAR = + OPTIONS.toBuilder().enableHiddenAccumulatorVar(false).build(); + + private static final ImmutableMap MACROS = + ImmutableMap.builder() + .putAll( + CelStandardMacro.STANDARD_MACROS.stream() + .map(CelStandardMacro::getDefinition) + .collect(toImmutableMap(CelMacro::getKey, Function.identity()))) + .put( + CelStandardMacro.EXISTS_ONE_NEW.getDefinition().getKey(), + CelStandardMacro.EXISTS_ONE_NEW.getDefinition()) + .put( + "noop_macro", + CelMacro.newGlobalVarArgMacro("noop_macro", (a, b, c) -> Optional.empty())) + .put( + "get_constant_macro", CelMacro.newGlobalMacro( "get_constant_macro", 0, @@ -59,215 +100,507 @@ public final class CelParserParameterizedTest extends BaselineTestCase { .setId(1) .setConstant(CelConstant.ofValue(10L)) .build()))) - .setOptions( - CelOptions.current() - .populateMacroCalls(true) - .enableHiddenAccumulatorVar(true) - .build()) - .build(); + .buildOrThrow(); - private static final CelParser PARSER_WITH_OLD_ACCU_VAR = - PARSER - .toParserBuilder() - .setOptions( - CelOptions.current() - .populateMacroCalls(true) - .enableHiddenAccumulatorVar(false) - .build()) - .build(); + private static final class ParseOutput { + final String pOutput; + final String lOutput; + final String mOutput; + final String errorMessage; + + ParseOutput(String pOutput, String lOutput, String mOutput, String errorMessage) { + this.pOutput = pOutput; + this.lOutput = lOutput; + this.mOutput = mOutput; + this.errorMessage = errorMessage; + } + + boolean isError() { + return errorMessage != null; + } + } + + private ParseOutput parse( + CelOptions options, + Map macros, + String expression, + boolean validateParseOutput) { + CelParser parser = + CelParserImpl.newBuilder().setOptions(options).addMacros(macros.values()).build(); + CelSource source = CelSource.newBuilder(expression).setDescription("").build(); + CelValidationResult parseResult = parser.parse(source); + + try { + CelProtoAbstractSyntaxTree protoAst = + CelProtoAbstractSyntaxTree.fromCelAst(parseResult.getAst()); + ParsedExpr parsedExpr = protoAst.toParsedExpr(); + String pOutput = null; + String lOutput = null; + if (validateParseOutput) { + pOutput = + CelDebug.toAdornedDebugString(parsedExpr.getExpr(), new CelExprKindAndIdAdorner()); + lOutput = + CelDebug.toAdornedDebugString( + parsedExpr.getExpr(), new CelLocationAdorner(parsedExpr.getSourceInfo())); + } + String mOutput = + CelExprKindAndIdAdorner.convertMacroCallsToString(parsedExpr.getSourceInfo()); + return new ParseOutput(pOutput, lOutput, mOutput, null); + } catch (CelValidationException e) { + return new ParseOutput(null, null, null, e.getMessage()); + } + } @Test - public void parser() { - runTest(PARSER, "x * 2"); - runTest(PARSER, "x * 2u"); - runTest(PARSER, "x * 2.0"); - runTest(PARSER, "\"\\u2764\""); - runTest(PARSER, "\"\u2764\""); - runTest(PARSER, "! false"); - runTest(PARSER, "-a"); - runTest(PARSER, "a.b(5)"); - runTest(PARSER, "a[3]"); - runTest(PARSER, "SomeMessage{foo: 5, bar: \"xyz\"}"); - runTest(PARSER, "[3, 4, 5]"); - runTest(PARSER, "{foo: 5, bar: \"xyz\"}"); - runTest(PARSER, "a > 5 && a < 10"); - runTest(PARSER, "a < 5 || a > 10"); - runTest(PARSER, "\"abc\" + \"def\""); - runTest(PARSER, "\"A\""); - runTest(PARSER, "true"); - runTest(PARSER, "false"); - runTest(PARSER, "0"); - runTest(PARSER, "42"); - runTest(PARSER, "0u"); - runTest(PARSER, "23u"); - runTest(PARSER, "24u"); - runTest(PARSER, "0xAu"); - runTest(PARSER, "-0xA"); - runTest(PARSER, "0xA"); - runTest(PARSER, "-1"); - runTest(PARSER, "4--4"); - runTest(PARSER, "4--4.1"); - runTest(PARSER, "b\"abc\""); - runTest(PARSER, "23.39"); - runTest(PARSER, "!a"); - runTest(PARSER, "null"); - runTest(PARSER, "a"); - runTest(PARSER, "a?b:c"); - runTest(PARSER, "a || b"); - runTest(PARSER, "a || b || c || d || e || f"); - runTest(PARSER, "a && b"); - runTest(PARSER, "a && b && c && d && e && f && g"); - runTest(PARSER, "a && b && c && d || e && f && g && h"); - runTest(PARSER, "a + b"); - runTest(PARSER, "a - b"); - runTest(PARSER, "a * b"); - runTest(PARSER, "a / b"); - runTest(PARSER, "a % b"); - runTest(PARSER, "a in b"); - runTest(PARSER, "a == b"); - runTest(PARSER, "a != b"); - runTest(PARSER, "a > b"); - runTest(PARSER, "a >= b"); - runTest(PARSER, "a < b"); - runTest(PARSER, "a <= b"); - runTest(PARSER, "a.b"); - runTest(PARSER, "a.b.c"); - runTest(PARSER, "a[b]"); - runTest(PARSER, "foo{ }"); - runTest(PARSER, "foo{ a:b }"); - runTest(PARSER, "foo{ a:b, c:d }"); - runTest(PARSER, "{}"); - runTest(PARSER, "{a:b, c:d}"); - runTest(PARSER, "[]"); - runTest(PARSER, "[a]"); - runTest(PARSER, "[a, b, c]"); - runTest(PARSER, "(a)"); - runTest(PARSER, "((a))"); - runTest(PARSER, "a()"); - runTest(PARSER, "a(b)"); - runTest(PARSER, "a(b, c)"); - runTest(PARSER, "a.b()"); - runTest(PARSER, "a.b(c)"); - runTest(PARSER, "aaa.bbb(ccc)"); - runTest(PARSER, "has(m.f)"); - runTest(PARSER, "m.exists_one(v, f)"); - runTest(PARSER, "m.existsOne(v, f)"); - runTest(PARSER, "m.map(v, f)"); - runTest(PARSER, "m.map(v, p, f)"); - runTest(PARSER, "m.filter(v, p)"); - runTest(PARSER, "[] + [1,2,3,] + [4]"); - runTest(PARSER, "{1:2u, 2:3u}"); - runTest(PARSER, "TestAllTypes{single_int32: 1, single_int64: 2}"); - runTest(PARSER, "size(x) == x.size()"); - runTest(PARSER, "\"\\\"\""); - runTest(PARSER, "[1,3,4][0]"); - runTest(PARSER, "x[\"a\"].single_int32 == 23"); - runTest(PARSER, "x.single_nested_message != null"); - runTest(PARSER, "false && !true || false ? 2 : 3"); - runTest(PARSER, "b\"abc\" + B\"def\""); - runTest(PARSER, "1 + 2 * 3 - 1 / 2 == 6 % 1"); - runTest(PARSER, "---a"); - runTest(PARSER, "\"\\xC3\\XBF\""); - runTest(PARSER, "\"\\303\\277\""); - runTest(PARSER, "\"hi\\u263A \\u263Athere\""); - runTest(PARSER, "\"\\U000003A8\\?\""); - runTest(PARSER, "\"\\a\\b\\f\\n\\r\\t\\v'\\\"\\\\\\? Legal escapes\""); - runTest(PARSER, "'😁' in ['😁', '😑', '😦']"); + public void parser_literals() { + // Null + runTest("null"); + + // Boolean + runTest("true"); + runTest("false"); + + // Int + runTest("0"); + runTest("42"); + runTest("0xF"); + runTest("0x2A"); + runTest("-1"); + runTest("-42"); + runTest("0xFFFFFFFFFFFFFFFFF"); + runTest("9223372036854775807"); // Long.MAX_VALUE + runTest("-9223372036854775808"); // Long.MIN_VALUE + runTest("-(9223372036854775808)"); // error + runTest("123a"); + + // Uint + runTest("0u"); + runTest("23u"); + runTest("24u"); + runTest("0xAu"); + runTest("-0xA"); + runTest("0xA"); + runTest("0xFu"); + runTest("0xFFFFFFFFFFFFFFFFFu"); + runTest("123u_"); + + // Double + runTest("3.14"); + runTest("23.39"); + runTest("1."); + runTest("1e+5"); + runTest("1e-5"); + runTest("2.5e+10"); + runTest("2.5e-10"); + runTest("1.99e90000009"); + runTest("1e"); + runTest("1e+"); + runTest("1e-"); + runTest("2.5e"); + runTest("2.5e+"); + runTest("2.5e-"); + runTest("((1e))"); + runTest("0x123z"); + + // String + runTest("'hello'"); + runTest("\"A\""); + runTest("'''hello\nworld'''"); + runTest("\"\\u2764\""); + runTest("\"\u2764\""); + runTest("\"\\\"\""); + runTest("\"\\xC3\\XBF\""); + runTest("\"\\303\\277\""); + runTest("\"hi\\u263A \\u263Athere\""); + runTest("\"\\U000003A8\\?\""); + runTest("\"\\a\\b\\f\\n\\r\\t\\v'\\\"\\\\\\? Legal escapes\""); + runTest("\"\"\"hello\nworld\"\"\""); + runTest("r\"\"\"hello\nworld\"\"\""); + runTest("\"\"\"\"\"\""); + runTest("''''''"); + runTest("\"\"\"hello\\\"\"\"world\"\"\""); + runTest("'''hello\\'''world'''"); + runTest("\"\\xFh\""); + runTest("\"\\a\\b\\f\\n\\r\\t\\v\\'\\\"\\\\\\? Illegal escape \\>\""); runTest( - PARSER, - // Note, the ANTLR parse stack may recurse much more deeply and permit - // more detailed expressions than the visitor can recurse over in - // practice. - "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['just fine'],[1],[2],[3],[4],[5]]]]]]]" - + "]]]]]]]]]]]]]]]]]]]]]]]]", - false); // parse output not validated as it is too large. - runTest(PARSER, "x.filter(y, y.filter(z, z > 0))"); - runTest(PARSER, "has(a.b).filter(c, c)"); - runTest(PARSER, "x.filter(y, y.exists(z, has(z.a)) && y.exists(z, has(z.b)))"); - runTest(PARSER, "noop_macro(123)"); - runTest(PARSER, "get_constant_macro()"); - runTest(PARSER, "a.?b[?0] && a[?c]"); - runTest(PARSER, "{?'key': value}"); - runTest(PARSER, "Msg{?field: value}"); - runTest(PARSER, "[?a, ?b]"); - runTest(PARSER, "[?a[?b]]"); + " '\ud83d\ude01' in ['\ud83d\ude01', '\ud83d\ude11', '\ud83d\ude26']\n" + + "\t\t\t&& in.\ud83d\ude01"); + runTest("\"\"\"hello\nworld"); + runTest("'''hello\nworld"); + runTest("r\"\"\"hello\nworld"); + runTest("\"hello\nworld\""); + runTest("'hello\nworld'"); + runTest("r\"hello\nworld\""); + runTest("`hello\nworld`"); + runTest("\"hello\rworld\""); + runTest("'unterminated"); + + // Bytes + runTest("b'abc'"); + runTest("b\"abc\""); + runTest("b\"\"\"hello\nworld"); + runTest("b\"hello\nworld\""); + runTest("rb\"hello\nworld\""); + runTest("br'abc'"); + runTest("bR'abc'"); + runTest("Br'abc'"); + runTest("BR'abc'"); + runAntlrTest(OPTIONS, "rb'abc'"); + runAntlrTest(OPTIONS, "rB'abc'"); + runAntlrTest(OPTIONS, "Rb'abc'"); + runAntlrTest(OPTIONS, "RB'abc'"); + runTest("br'a\\'b'"); + runAntlrTest(OPTIONS, "rb'a\\'b'"); + } + + @Test + @SuppressWarnings("InlineMeInliner") // String.repeat is unavailable under Java 8 + public void parser_core_syntax() { + // Identifiers + runTest("a"); + runTest("foo"); + + // Parentheses + runTest("(a)"); + runTest("((a))"); + runTest("(((1 + 2))) * 3"); + + // Lists + runTest("[]"); + runTest("[a]"); + runTest("[a, b, c]"); + runTest("[1, 2, 3]"); + runTest("[3, 4, 5]"); + runTest("[3, 4, 5,]"); + runTest("[?a, b]"); + runTest("[?a, ?b]"); + runTest("[?a[?b]]"); + + // Maps + runTest("{}"); + runTest("{a:b, c:d}"); + runTest("{foo: 5, bar: \"xyz\"}"); + runTest("{foo: 5, bar: \"xyz\", }"); + runTest("{\"a\": 1, \"b\": 2}"); + runTest("{1:2u, 2:3u}"); + runTest("{?a: b}"); + runTest("{?'key': value}"); + + // Messages + runTest("foo{ }"); + runTest("foo{ a:b }"); + runTest("foo{ a:b, c:d }"); + runTest("SomeMessage{foo: 5, bar: \"xyz\"}"); + runTest("TestAllTypes{single_int32: 1, single_int64: 2}"); + runTest("MyType{foo: 1, bar: 'baz'}"); + runTest("Message{`in`: true}"); + runTest("Msg{?field: value}"); + runTest("foo.bar.MyType{ }"); + runTest("foo.bar.MyType{ a:b }"); + runTest(".foo.bar.MyType{ a:b }"); + runTest("a.b.c.d.Message{ foo: 1, bar: 'baz' }"); + + // Field selection + runTest("a.b"); + runTest("a.b.c"); + runTest("a.?b"); + runTest("a.`b-c`"); + runTest("a.`b c`"); + runTest("a.`b.c`"); + runTest("a.`in`"); + runTest("a.`/foo`"); + runTest("a.`my-var`"); + + // Indexing + runTest("a[b]"); + runTest("a[0]"); + runTest("a[3]"); + runTest("[1,3,4][0]"); + runTest("a[?0]"); + + // Function calls + runTest("a()"); + runTest("a(b)"); + runTest("a(b, c)"); + runTest("a.b()"); + runTest("a.b(c)"); + runTest("a.b(5)"); + runTest("aaa.bbb(ccc)"); + runTest("a.foo(1, 2)"); + + // Unary operators + runTest("!a"); + runTest("!x"); + runTest("! false"); + runTest("-a"); + runTest("---a"); + + // Arithmetic operators + runTest("x * 2"); + runTest("x * 2u"); + runTest("x * 2.0"); + runTest("a * b"); + runTest("a / b"); + runTest("a % b"); + runTest("a + b"); + runTest("a - b"); + runTest("4--4"); + runTest("4--4.1"); + runTest("\"abc\" + \"def\""); + runTest("b\"abc\" + B\"def\""); + runTest("[] + [1,2,3,] + [4]"); + runTest("1 + 2 * 3"); + + // Comparison operators + runTest("a == b"); + runTest("a != b"); + runTest("a < b"); + runTest("a <= b"); + runTest("a > b"); + runTest("a >= b"); + runTest("a in b"); + runTest("\"\ud83d\ude01\" in [\"\ud83d\ude01\", \"\ud83d\ude11\", \"\ud83d\ude26\"]"); + runTest("size(x) == x.size()"); + runTest("x.single_nested_message != null"); + + // Logical operators + runTest("a && b"); + runTest("a && b && c"); + runTest("a && b && c && d && e && f && g"); + runTest("a > 5 && a < 10"); + runTest("a || b"); + runTest("a || b || c || d || e || f"); + runTest("a < 5 || a > 10"); + runTest("a && b && c && d || e && f && g && h"); + + // Conditional operator + runTest("a?b:c"); + runTest("cond ? 1 : 2"); + runTest("false && !true || false ? 2 : 3"); + runTest(OPTIONS_MAX_RECURSION_DEPTH_32, Strings.repeat("true ? 1 : ", 31) + "1", false); + runAntlrTest(OPTIONS_MAX_RECURSION_DEPTH_32, Strings.repeat("!-", 15) + "x"); + + // Complex expressions + runTest("1 + 2 * 3 - 1 / 2 == 6 % 1"); + runTest("x[\"a\"].single_int32 == 23"); + runTest("a.?b[?0] && a[?c]"); runTest( - CelParserImpl.newBuilder() - .setOptions(CelOptions.current().enableReservedIds(false).build()) - .build(), - "while"); - CelParser parserWithQuotedFields = - CelParserImpl.newBuilder() - .setOptions(CelOptions.current().enableQuotedIdentifierSyntax(true).build()) - .build(); - runTest(parserWithQuotedFields, "foo.`bar`"); - runTest(parserWithQuotedFields, "foo.`bar-baz`"); - runTest(parserWithQuotedFields, "foo.`bar baz`"); - runTest(parserWithQuotedFields, "foo.`bar.baz`"); - runTest(parserWithQuotedFields, "foo.`bar/baz`"); - runTest(parserWithQuotedFields, "foo.`bar_baz`"); - runTest(parserWithQuotedFields, "foo.`in`"); - runTest(parserWithQuotedFields, "Struct{`in`: false}"); + OPTIONS, + "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['just" + + " fine'],[1],[2],[3],[4],[5]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]", + false); + + // Whitespace and comments + runTest("// comment\na"); + runTest("a // comment"); + runTest("a\n// comment\n+ b"); + runTest("a / // comment\n b"); + runTest("[\n 1, // comment\n 2,\n]"); + + // Reserved IDs disabled + runTest(OPTIONS.toBuilder().enableReservedIds(false).build(), "while"); + + // Quoted field specifiers + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "foo.`bar`"); + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "foo.`bar-baz`"); + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "foo.`bar baz`"); + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "foo.`bar.baz`"); + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "foo.`bar/baz`"); + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "foo.`bar_baz`"); + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "foo.`in`"); + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "Struct{`in`: false}"); } @Test - public void parser_legacyAccuVar() { - runTest(PARSER_WITH_OLD_ACCU_VAR, "x * 2"); - runTest(PARSER_WITH_OLD_ACCU_VAR, "has(m.f)"); - runTest(PARSER_WITH_OLD_ACCU_VAR, "m.exists_one(v, f)"); - runTest(PARSER_WITH_OLD_ACCU_VAR, "m.all(v, f)"); - runTest(PARSER_WITH_OLD_ACCU_VAR, "m.map(v, f)"); - runTest(PARSER_WITH_OLD_ACCU_VAR, "m.map(v, p, f)"); - runTest(PARSER_WITH_OLD_ACCU_VAR, "m.filter(v, p)"); + public void parser_macros() { + runTest("has(m.f)"); + runTest("has(a.b)"); + runTest("has(m)"); + + runTest("m.all(v, f)"); + runTest("[1, 2].all(x, x > 0)"); + + runTest("m.exists(v, f)"); + + runTest("m.exists_one(v, f)"); + runTest("m.existsOne(v, f)"); + runTest("[].existsOne(__result__, __result__)"); + + runTest("m.map(v, f)"); + runTest("m.map(v, p, f)"); + runTest("m.map(__result__, __result__)"); + + runTest("m.filter(v, p)"); + runTest("m.filter(__result__, false)"); + runTest("m.filter(a.b, false)"); + + // Nested / Chained macros + runTest("x.filter(y, y.filter(z, z > 0))"); + runTest("has(a.b).filter(c, c)"); + runTest("x.filter(y, y.exists(z, has(z.a)) && y.exists(z, has(z.b)))"); + runTest("(has(a.b) || has(c.d)).string()"); + runTest("has(a.b).asList().exists(c, c)"); + runTest("[has(a.b), has(c.d)].exists(e, e)"); + + // Custom macros + runTest("noop_macro(123)"); + runTest("get_constant_macro()"); } @Test + @SuppressWarnings("InlineMeInliner") // String.repeat is unavailable under Java 8 public void parser_errors() { - runTest(PARSER, "*@a | b"); - runTest(PARSER, "a | b"); - runTest(PARSER, "?"); - runTest(PARSER, "1 + $"); - runTest(PARSER, "1.all(2, 3)"); - runTest(PARSER, "1.exists(2, 3)"); - runTest(PARSER, "[].all(__result__, x)"); - runTest(PARSER, "[].exists(__result__, x)"); - runTest(PARSER, "[].exists_one(__result__, x)"); - runTest(PARSER, "[].map(__result__, x, x)"); - runTest(PARSER, "[].filter(__result__, x)"); - runTest(PARSER, "[].all(.x, x)"); - runTest(PARSER, "[].exists(.x, x)"); - runTest(PARSER, "[].exists_one(.x, x)"); - runTest(PARSER, "[].map(.x, x, x)"); - runTest(PARSER, "[].filter(.x, x)"); - runTest(PARSER, "1 + +"); - runTest(PARSER, "\"\\xFh\""); - runTest(PARSER, "\"\\a\\b\\f\\n\\r\\t\\v\\'\\\"\\\\\\? Illegal escape \\>\""); - runTest(PARSER, "'\uD800'"); - runTest(PARSER, "'\uDFFF'"); - runTest(PARSER, "r\"\\\uD800\""); - - runTest(PARSER, "as"); - runTest(PARSER, "break"); - runTest(PARSER, "const"); - runTest(PARSER, "continue"); - runTest(PARSER, "else"); - runTest(PARSER, "for"); - runTest(PARSER, "function"); - runTest(PARSER, "if"); - runTest(PARSER, "import"); - runTest(PARSER, "in"); - runTest(PARSER, "let"); - runTest(PARSER, "loop"); - runTest(PARSER, "package"); - runTest(PARSER, "namespace"); - runTest(PARSER, "return"); - runTest(PARSER, "var"); - runTest(PARSER, "void"); - runTest(PARSER, "while"); - runTest(PARSER, "[1, 2, 3].map(var, var * var)"); - runTest(PARSER, "'😁' in ['😁', '😑', '😦']\n" + " && in.😁"); + // Lexical errors + runTest("*@a | b"); + runTest("((@))"); + runTest("1 + $"); + runTest( + "\u00f3\u00a0\u00a2\n" + + "\t\t\u00f3\u00a00\u00a0\n" + + "\t\t\u007f0\"\"\"\\\"\"\\\"\"\"\\\"\"\\\"\"\"\\\"\"\\\"\"\"\\\"\"\\\"\"\"\\\"\\\"\"\"\\\"\"\\\"\"\"\\\"\"\\\"\"\"\\\"\"\\\"\"\"\\\"!\\\"\"\"\\\"\"\\\"\"\"\\\"\"\\\""); + runTest("'\\udead' == '\\ufffd'"); + runTest("a | b"); + runTest("'3# < 10\" '& tru ^^"); + runTest("'\uD800'"); + runTest("'\uDFFF'"); + runTest("r\"\\\uD800\""); + + // Unexpected tokens + runTest("1 + +"); + runTest("?"); + runTest("a ? b ((?))"); + runTest("a ? b @"); + runTest( + "-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1\n" + + "\t\t--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1--1--0--1--1--1\n" + + "\t\t--3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1\n" + + "\t\t--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1\n" + + "\t\t--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1\n" + + "\t\t--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1\n" + + "\t\t--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1--1--0--1--1--1\n" + + "\t\t--3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1\n" + + "\t\t--3-[-1--1--1--1---1-1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1\n" + + "\t\t--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1\n" + + "\t\t--1--1---1--1-\u00c01--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1\n" + + "\t\t--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1\n" + + "\t\t--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1\n" + + "\t\t--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1\n" + + "\t\t--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1\n" + + "\t\t--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1\n" + + "\t\t--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1\n" + + "\t\t--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1\n" + + "\t\t--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1\n" + + "\t\t--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1\n" + + "\t\t--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1\n" + + "\t\t--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1\n" + + "\t\t--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1\n" + + "\t\t--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1\n" + + "\t\t--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1\n" + + "\t\t--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1\n" + + "\t\t--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1\n" + + "\t\t--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1\n" + + "\t\t--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1\n" + + "\t\t--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1\n" + + "\t\t--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1\n" + + "\t\t--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1\n" + + "\t\t--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1\n" + + "\t\t--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1"); + + // Reserved identifiers + runTest( + "as break const continue else for function if import in let loop package namespace" + + " return var void while"); + runTest("as"); + runTest("break"); + runTest("const"); + runTest("continue"); + runTest("else"); + runTest("for"); + runTest("function"); + runTest("if"); + runTest("import"); + runTest("in"); + runTest("let"); + runTest("loop"); + runTest("package"); + runTest("namespace"); + runTest("return"); + runTest("var"); + runTest("void"); + runTest("while"); + runTest("[1, 2, 3].map(var, var * var)"); + runTest("'😁' in ['😁', '😑', '😦']\n && in.😁"); + + // Incomplete expressions + runTest("1 +"); + runTest("--"); + runTest("{"); + runTest("0x"); + + // Unexpected token after expression + runTest("TestAllTypes(){}"); + runTest("TestAllTypes{}()"); + runTest("TestAllTypes(){single_int32: 1, single_int64: 2}"); + runTest("1 + 2\n3 +"); + + // Member selection errors + runTest("{\"a\": 1}.\"a\""); + runTest("self.true == 1"); + + // Map syntax errors + runTest("{a}"); + runTest("{:a}"); + + // Message syntax errors + runTest("func{{a}}"); + runTest("msg{:a}"); + runTest("ind[a{b}]"); + runTest("x{?."); + runTest("x{."); + runTest("t{>C}"); + runTest("has([(has(("); + + // Macro errors + runTest("1.all(2, 3)"); + runTest("1.exists(2, 3)"); + runTest("[].all(__result__, x)"); + runTest("[].exists(__result__, x)"); + runTest("[].exists_one(__result__, x)"); + runTest("[].map(__result__, x, x)"); + runTest("[].filter(__result__, x)"); + runTest("[].all(.x, x)"); + runTest("[].exists(.x, x)"); + runTest("[].exists_one(.x, x)"); + runTest("[].map(.x, x, x)"); + runTest("[].filter(.x, x)"); + + // Unsupported optional syntax + runTest(OPTIONS_NO_OPTIONAL_SYNTAX, "a.?b && a[?b]"); + runTest(OPTIONS_NO_OPTIONAL_SYNTAX, "[?a, ?b]"); + runTest(OPTIONS_NO_OPTIONAL_SYNTAX, "Msg{?field: value} && {?'key': value}"); + + // Unsupported quoted identifier syntax + runTest(OPTIONS_NO_QUOTED_IDENTIFIER_SYNTAX, "a.`b-c`"); + runTest(OPTIONS_NO_QUOTED_IDENTIFIER_SYNTAX, "a.`b.c`"); + runTest(OPTIONS_NO_QUOTED_IDENTIFIER_SYNTAX, "a.`in`"); + runTest(OPTIONS_NO_QUOTED_IDENTIFIER_SYNTAX, "a.`/foo`"); + runTest(OPTIONS_NO_QUOTED_IDENTIFIER_SYNTAX, "Message{`in`: true}"); + runTest(OPTIONS_NO_QUOTED_IDENTIFIER_SYNTAX, "foo.`bar`"); + runTest(OPTIONS_NO_QUOTED_IDENTIFIER_SYNTAX, "Struct{`bar`: false}"); + runTest(OPTIONS_NO_QUOTED_IDENTIFIER_SYNTAX, "has(.`.`"); + + // Unsupported quoted identifier location + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "`b-c`"); + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "`b-c`()"); + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "a.`$b`"); + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "a.`b.c`()"); + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "`bar`"); + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "foo.``"); + runTest(OPTIONS_QUOTED_IDENTIFIER_SYNTAX, "foo.`$bar`"); + + // Recursion limit exceeded runTest( - PARSER, + OPTIONS, "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[" + "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[" + "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[" @@ -276,37 +609,108 @@ public void parser_errors() { + "]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]" + "]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]" + "]]]]]]"); - runTest(PARSER, "{\"a\": 1}.\"a\""); - runTest(PARSER, "1 + 2\n3 +"); - runTest(PARSER, "TestAllTypes(){single_int32: 1, single_int64: 2}"); - runTest(PARSER, "{"); - runTest(PARSER, "t{>C}"); - runTest(PARSER, "has([(has(("); - - CelParser parserWithoutOptionalSupport = - CelParserImpl.newBuilder() - .setOptions(CelOptions.current().enableOptionalSyntax(false).build()) - .build(); - runTest(parserWithoutOptionalSupport, "a.?b && a[?b]"); - runTest(parserWithoutOptionalSupport, "Msg{?field: value} && {?'key': value}"); - runTest(parserWithoutOptionalSupport, "[?a, ?b]"); + runTest( + OPTIONS_MAX_RECURSION_DEPTH_32, + "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[\n" + + "\t\t\t[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['too many']]]]]]]]]]]]]]]]]]]]]]]]]]]]\n" + + "\t\t\t]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"); + runTest( + OPTIONS_MAX_RECURSION_DEPTH_32, + "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['not fine']]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"); + runTest( + OPTIONS_MAX_RECURSION_DEPTH_32, + "a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.A.B.C.D.E.F.G.H"); + runTest( + OPTIONS_MAX_RECURSION_DEPTH_32, + "a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]\n" + + "\t\t [21][22][23][24][25][26][27][28][29][30][31][32][33]"); + runTest( + OPTIONS_MAX_RECURSION_DEPTH_32, + "1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10\n" + + "\t\t+ 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20\n" + + "\t\t+ 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 30\n" + + "\t\t+ 31 + 32 + 33 + 34"); + runTest( + OPTIONS_MAX_RECURSION_DEPTH_32, + "a < 1 < 2 < 3 < 4 < 5 < 6 < 7 < 8 < 9 < 10 < 11\n" + + "\t\t < 12 < 13 < 14 < 15 < 16 < 17 < 18 < 19 < 20 < 21\n" + + "\t\t\t < 22 < 23 < 24 < 25 < 26 < 27 < 28 < 29 < 30 < 31\n" + + "\t\t\t < 32 < 33"); + runTest( + OPTIONS_MAX_RECURSION_DEPTH_32, + "y!=y!=y!=y!=y!=y!=y!=y!=y!=-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y\n" + + "\t\t!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y\n" + + "\t\t!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y\n" + + "\t\t!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y\n" + + "\t\t!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y\n" + + "\t\t!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y"); + runTest( + OPTIONS_MAX_RECURSION_DEPTH_32, + "a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]" + + " !=\n" + + "\t\ta[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20]"); + runTest(OPTIONS_MAX_RECURSION_DEPTH_32, Strings.repeat("true ? 1 : ", 33) + "1"); + runTest(OPTIONS_MAX_RECURSION_DEPTH_32, Strings.repeat("!-", 16) + "!x"); + runTest(OPTIONS_MAX_CODE_POINT_SIZE_5, "123456"); + runTest(OPTIONS_MAX_NODE_COUNT_2, "1 + 2 + 3"); + runTest(OPTIONS_MAX_ERROR_RECOVERY_LIMIT_2, "[?, ?, ?]"); + runTest(OPTIONS_MAX_ERROR_RECOVERY_LIMIT_2, "[1 2 3 a b c]"); + } - CelParser parserWithQuotedFields = - CelParserImpl.newBuilder() - .setOptions(CelOptions.current().enableQuotedIdentifierSyntax(true).build()) - .build(); - runTest(parserWithQuotedFields, "`bar`"); - runTest(parserWithQuotedFields, "foo.``"); - runTest(parserWithQuotedFields, "foo.`$bar`"); + @Test + public void parser_legacyAccuVar() { + runAntlrTest(OPTIONS_OLD_ACCU_VAR, "x * 2"); + runAntlrTest(OPTIONS_OLD_ACCU_VAR, "has(m.f)"); + runAntlrTest(OPTIONS_OLD_ACCU_VAR, "m.exists_one(v, f)"); + runAntlrTest(OPTIONS_OLD_ACCU_VAR, "m.all(v, f)"); + runAntlrTest(OPTIONS_OLD_ACCU_VAR, "m.map(v, f)"); + runAntlrTest(OPTIONS_OLD_ACCU_VAR, "m.map(v, p, f)"); + runAntlrTest(OPTIONS_OLD_ACCU_VAR, "m.filter(v, p)"); + } - CelParser parserWithoutQuotedFields = - CelParserImpl.newBuilder() - .setStandardMacros(CelStandardMacro.HAS) - .setOptions(CelOptions.current().enableQuotedIdentifierSyntax(false).build()) - .build(); - runTest(parserWithoutQuotedFields, "foo.`bar`"); - runTest(parserWithoutQuotedFields, "Struct{`bar`: false}"); - runTest(parserWithoutQuotedFields, "has(.`.`"); + private void runAntlrTest(CelOptions options, String expression) { + testOutput().println("I: " + sanitizeForBaseline(expression)); + testOutput().println("=====>"); + + CelOptions antlrOptions = options.toBuilder().enablePrattParser(false).build(); + ParseOutput antlrResult = + parse(antlrOptions, MACROS, expression, /* validateParseOutput= */ true); + if (!antlrResult.isError()) { + testOutput().println("P: " + antlrResult.pOutput); + if (!Strings.isNullOrEmpty(antlrResult.lOutput)) { + testOutput().println("L: " + antlrResult.lOutput); + } + if (!Strings.isNullOrEmpty(antlrResult.mOutput)) { + testOutput().println("M: " + antlrResult.mOutput); + } + } else { + testOutput().println("E/A: " + sanitizeForBaseline(antlrResult.errorMessage)); + } + + testOutput().println(); } @Test @@ -314,53 +718,93 @@ public void source_info() throws Exception { runSourceInfoTest("[{}, {'field': true}].exists(i, has(i.field))"); } - private void runTest(CelParser parser, String expression) { - runTest(parser, expression, true); + private void runTest(String expression) { + runTest(OPTIONS, expression); } - private void runTest(CelParser parser, String expression, boolean validateParseOutput) { - testOutput().println("I: " + expression); + private void runTest(CelOptions options, String expression) { + runTest(options, expression, true); + } + + private void runTest(CelOptions options, String expression, boolean validateParseOutput) { + runTest(options, MACROS, expression, validateParseOutput); + } + + private void runTest( + CelOptions options, + Map macros, + String expression, + boolean validateParseOutput) { + testOutput().println("I: " + sanitizeForBaseline(expression)); testOutput().println("=====>"); - CelSource source = CelSource.newBuilder(expression).setDescription("").build(); - CelValidationResult parseResult = parser.parse(source); + ParseOutput antlrResult = + parse( + options.toBuilder().enablePrattParser(false).build(), + macros, + expression, + validateParseOutput); + ParseOutput prattResult = + parse( + options.toBuilder().enablePrattParser(true).build(), + macros, + expression, + validateParseOutput); - try { - CelProtoAbstractSyntaxTree protoAst = - CelProtoAbstractSyntaxTree.fromCelAst(parseResult.getAst()); - ParsedExpr parsedExpr = protoAst.toParsedExpr(); + assertThat(prattResult.isError()).isEqualTo(antlrResult.isError()); + if (!antlrResult.isError()) { if (validateParseOutput) { - testOutput() - .println( - "P: " - + CelDebug.toAdornedDebugString( - parsedExpr.getExpr(), new CelExprKindAndIdAdorner())); - String locationOutput = - CelDebug.toAdornedDebugString( - parsedExpr.getExpr(), new CelLocationAdorner(parsedExpr.getSourceInfo())); - if (!locationOutput.isEmpty()) { - testOutput().println("L: " + locationOutput); + assertThat(prattResult.pOutput).isEqualTo(antlrResult.pOutput); + testOutput().println("P: " + antlrResult.pOutput); + + assertThat(prattResult.lOutput).isEqualTo(antlrResult.lOutput); + if (!Strings.isNullOrEmpty(antlrResult.lOutput)) { + testOutput().println("L: " + antlrResult.lOutput); } } - String macroOutput = - CelExprKindAndIdAdorner.convertMacroCallsToString(parsedExpr.getSourceInfo()); - if (!macroOutput.isEmpty()) { - testOutput().println("M: " + macroOutput); + assertThat(prattResult.mOutput).isEqualTo(antlrResult.mOutput); + if (!Strings.isNullOrEmpty(antlrResult.mOutput)) { + testOutput().println("M: " + antlrResult.mOutput); } - } catch (CelValidationException e) { - testOutput().println("E: " + e.getMessage()); + } else { + testOutput().println("E/A: " + sanitizeForBaseline(antlrResult.errorMessage)); + testOutput().println("E/P: " + sanitizeForBaseline(prattResult.errorMessage)); } testOutput().println(); } private void runSourceInfoTest(String expression) throws Exception { - CelAbstractSyntaxTree ast = PARSER.parse(expression).getAst(); - SourceInfo sourceInfo = - CelProtoAbstractSyntaxTree.fromCelAst(ast).toParsedExpr().getSourceInfo(); testOutput().println("I: " + expression); testOutput().println("=====>"); - testOutput().println("S: " + TextFormat.printer().printToString(sourceInfo)); + CelParser antlrParser = + CelParserImpl.newBuilder() + .setOptions(OPTIONS.toBuilder().enablePrattParser(false).build()) + .addMacros(MACROS.values()) + .build(); + CelParser prattParser = + CelParserImpl.newBuilder() + .setOptions(OPTIONS.toBuilder().enablePrattParser(true).build()) + .addMacros(MACROS.values()) + .build(); + + CelAbstractSyntaxTree antlrAst = antlrParser.parse(expression).getAst(); + CelAbstractSyntaxTree prattAst = prattParser.parse(expression).getAst(); + + SourceInfo antlrSourceInfo = + CelProtoAbstractSyntaxTree.fromCelAst(antlrAst).toParsedExpr().getSourceInfo(); + SourceInfo prattSourceInfo = + CelProtoAbstractSyntaxTree.fromCelAst(prattAst).toParsedExpr().getSourceInfo(); + + assertThat(prattSourceInfo).isEqualTo(antlrSourceInfo); + testOutput().println("S: " + TextFormat.printer().printToString(antlrSourceInfo)); + } + + private static String sanitizeForBaseline(String text) { + if (text == null) { + return null; + } + return text.replace("\t", "»").replace("\u007f", "\\u007f"); } } diff --git a/parser/src/test/java/dev/cel/parser/PrattParserTest.java b/parser/src/test/java/dev/cel/parser/PrattParserTest.java index 53ac0703b..e9394b362 100644 --- a/parser/src/test/java/dev/cel/parser/PrattParserTest.java +++ b/parser/src/test/java/dev/cel/parser/PrattParserTest.java @@ -220,6 +220,10 @@ public void pratt_parser_core_syntax() { runTest("MyType{foo: 1, bar: 'baz'}"); runTest("Message{`in`: true}"); runTest("Msg{?field: value}"); + runTest("foo.bar.MyType{ }"); + runTest("foo.bar.MyType{ a:b }"); + runTest(".foo.bar.MyType{ a:b }"); + runTest("a.b.c.d.Message{ foo: 1, bar: 'baz' }"); // Field selection runTest("a.b"); @@ -544,7 +548,7 @@ private void runTest( Map macros, String expression, boolean validateParseOutput) { - testOutput().println("I: " + expression.replace("\t", "»")); + testOutput().println("I: " + sanitizeForBaseline(expression)); testOutput().println("=====>"); CelSource source = CelSource.newBuilder(expression).setDescription("").build(); @@ -574,9 +578,16 @@ private void runTest( testOutput().println("M: " + macroOutput); } } catch (CelValidationException e) { - testOutput().println("E: " + e.getMessage()); + testOutput().println("E: " + sanitizeForBaseline(e.getMessage())); } testOutput().println(); } + + private static String sanitizeForBaseline(String text) { + if (text == null) { + return null; + } + return text.replace("\t", "»").replace("\u007f", "\\u007f"); + } } diff --git a/parser/src/test/resources/parser.baseline b/parser/src/test/resources/parser_core_syntax.baseline similarity index 53% rename from parser/src/test/resources/parser.baseline rename to parser/src/test/resources/parser_core_syntax.baseline index 37b8ef3cc..7c05685f3 100644 --- a/parser/src/test/resources/parser.baseline +++ b/parser/src/test/resources/parser_core_syntax.baseline @@ -1,96 +1,94 @@ -I: x * 2 +I: a =====> -P: _*_( - x^#1:Expr.Ident#, - 2^#3:int64# -)^#2:Expr.Call# -L: _*_( - x^#1[1,0]#, - 2^#3[1,4]# -)^#2[1,2]# +P: a^#1:Expr.Ident# +L: a^#1[1,0]# -I: x * 2u +I: foo =====> -P: _*_( - x^#1:Expr.Ident#, - 2u^#3:uint64# -)^#2:Expr.Call# -L: _*_( - x^#1[1,0]#, - 2u^#3[1,4]# -)^#2[1,2]# +P: foo^#1:Expr.Ident# +L: foo^#1[1,0]# -I: x * 2.0 +I: (a) =====> -P: _*_( - x^#1:Expr.Ident#, - 2.0^#3:double# -)^#2:Expr.Call# -L: _*_( - x^#1[1,0]#, - 2.0^#3[1,4]# -)^#2[1,2]# +P: a^#1:Expr.Ident# +L: a^#1[1,1]# -I: "\u2764" +I: ((a)) =====> -P: "❤"^#1:string# -L: "❤"^#1[1,0]# +P: a^#1:Expr.Ident# +L: a^#1[1,2]# -I: "❤" +I: (((1 + 2))) * 3 =====> -P: "❤"^#1:string# -L: "❤"^#1[1,0]# +P: _*_( + _+_( + 1^#1:int64#, + 2^#3:int64# + )^#2:Expr.Call#, + 3^#5:int64# +)^#4:Expr.Call# +L: _*_( + _+_( + 1^#1[1,3]#, + 2^#3[1,7]# + )^#2[1,5]#, + 3^#5[1,14]# +)^#4[1,12]# -I: ! false +I: [] =====> -P: !_( - false^#2:bool# -)^#1:Expr.Call# -L: !_( - false^#2[1,2]# -)^#1[1,0]# +P: []^#1:Expr.CreateList# +L: []^#1[1,0]# -I: -a +I: [a] =====> -P: -_( +P: [ a^#2:Expr.Ident# -)^#1:Expr.Call# -L: -_( +]^#1:Expr.CreateList# +L: [ a^#2[1,1]# -)^#1[1,0]# +]^#1[1,0]# -I: a.b(5) +I: [a, b, c] =====> -P: a^#1:Expr.Ident#.b( - 5^#3:int64# -)^#2:Expr.Call# -L: a^#1[1,0]#.b( - 5^#3[1,4]# -)^#2[1,3]# +P: [ + a^#2:Expr.Ident#, + b^#3:Expr.Ident#, + c^#4:Expr.Ident# +]^#1:Expr.CreateList# +L: [ + a^#2[1,1]#, + b^#3[1,4]#, + c^#4[1,7]# +]^#1[1,0]# -I: a[3] +I: [1, 2, 3] =====> -P: _[_]( - a^#1:Expr.Ident#, - 3^#3:int64# -)^#2:Expr.Call# -L: _[_]( - a^#1[1,0]#, - 3^#3[1,2]# -)^#2[1,1]# +P: [ + 1^#2:int64#, + 2^#3:int64#, + 3^#4:int64# +]^#1:Expr.CreateList# +L: [ + 1^#2[1,1]#, + 2^#3[1,4]#, + 3^#4[1,7]# +]^#1[1,0]# -I: SomeMessage{foo: 5, bar: "xyz"} +I: [3, 4, 5] =====> -P: SomeMessage{ - foo:5^#3:int64#^#2:Expr.CreateStruct.Entry#, - bar:"xyz"^#5:string#^#4:Expr.CreateStruct.Entry# -}^#1:Expr.CreateStruct# -L: SomeMessage{ - foo:5^#3[1,17]#^#2[1,15]#, - bar:"xyz"^#5[1,25]#^#4[1,23]# -}^#1[1,11]# +P: [ + 3^#2:int64#, + 4^#3:int64#, + 5^#4:int64# +]^#1:Expr.CreateList# +L: [ + 3^#2[1,1]#, + 4^#3[1,4]#, + 5^#4[1,7]# +]^#1[1,0]# -I: [3, 4, 5] +I: [3, 4, 5,] =====> P: [ 3^#2:int64#, @@ -103,6 +101,59 @@ L: [ 5^#4[1,7]# ]^#1[1,0]# +I: [?a, b] +=====> +P: [ + ?a^#2:Expr.Ident#, + b^#3:Expr.Ident# +]^#1:Expr.CreateList# +L: [ + ?a^#2[1,2]#, + b^#3[1,5]# +]^#1[1,0]# + +I: [?a, ?b] +=====> +P: [ + ?a^#2:Expr.Ident#, + ?b^#3:Expr.Ident# +]^#1:Expr.CreateList# +L: [ + ?a^#2[1,2]#, + ?b^#3[1,6]# +]^#1[1,0]# + +I: [?a[?b]] +=====> +P: [ + ?_[?_]( + a^#2:Expr.Ident#, + b^#4:Expr.Ident# + )^#3:Expr.Call# +]^#1:Expr.CreateList# +L: [ + ?_[?_]( + a^#2[1,2]#, + b^#4[1,5]# + )^#3[1,3]# +]^#1[1,0]# + +I: {} +=====> +P: {}^#1:Expr.CreateStruct# +L: {}^#1[1,0]# + +I: {a:b, c:d} +=====> +P: { + a^#3:Expr.Ident#:b^#4:Expr.Ident#^#2:Expr.CreateStruct.Entry#, + c^#6:Expr.Ident#:d^#7:Expr.Ident#^#5:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: { + a^#3[1,1]#:b^#4[1,3]#^#2[1,2]#, + c^#6[1,6]#:d^#7[1,8]#^#5[1,7]# +}^#1[1,0]# + I: {foo: 5, bar: "xyz"} =====> P: { @@ -114,891 +165,526 @@ L: { bar^#6[1,9]#:"xyz"^#7[1,14]#^#5[1,12]# }^#1[1,0]# -I: a > 5 && a < 10 -=====> -P: _&&_( - _>_( - a^#1:Expr.Ident#, - 5^#3:int64# - )^#2:Expr.Call#, - _<_( - a^#5:Expr.Ident#, - 10^#7:int64# - )^#6:Expr.Call# -)^#4:Expr.Call# -L: _&&_( - _>_( - a^#1[1,0]#, - 5^#3[1,4]# - )^#2[1,2]#, - _<_( - a^#5[1,9]#, - 10^#7[1,13]# - )^#6[1,11]# -)^#4[1,6]# - -I: a < 5 || a > 10 +I: {foo: 5, bar: "xyz", } =====> -P: _||_( - _<_( - a^#1:Expr.Ident#, - 5^#3:int64# - )^#2:Expr.Call#, - _>_( - a^#5:Expr.Ident#, - 10^#7:int64# - )^#6:Expr.Call# -)^#4:Expr.Call# -L: _||_( - _<_( - a^#1[1,0]#, - 5^#3[1,4]# - )^#2[1,2]#, - _>_( - a^#5[1,9]#, - 10^#7[1,13]# - )^#6[1,11]# -)^#4[1,6]# +P: { + foo^#3:Expr.Ident#:5^#4:int64#^#2:Expr.CreateStruct.Entry#, + bar^#6:Expr.Ident#:"xyz"^#7:string#^#5:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: { + foo^#3[1,1]#:5^#4[1,6]#^#2[1,4]#, + bar^#6[1,9]#:"xyz"^#7[1,14]#^#5[1,12]# +}^#1[1,0]# -I: "abc" + "def" +I: {"a": 1, "b": 2} =====> -P: _+_( - "abc"^#1:string#, - "def"^#3:string# -)^#2:Expr.Call# -L: _+_( - "abc"^#1[1,0]#, - "def"^#3[1,8]# -)^#2[1,6]# +P: { + "a"^#3:string#:1^#4:int64#^#2:Expr.CreateStruct.Entry#, + "b"^#6:string#:2^#7:int64#^#5:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: { + "a"^#3[1,1]#:1^#4[1,6]#^#2[1,4]#, + "b"^#6[1,9]#:2^#7[1,14]#^#5[1,12]# +}^#1[1,0]# -I: "A" +I: {1:2u, 2:3u} =====> -P: "A"^#1:string# -L: "A"^#1[1,0]# +P: { + 1^#3:int64#:2u^#4:uint64#^#2:Expr.CreateStruct.Entry#, + 2^#6:int64#:3u^#7:uint64#^#5:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: { + 1^#3[1,1]#:2u^#4[1,3]#^#2[1,2]#, + 2^#6[1,7]#:3u^#7[1,9]#^#5[1,8]# +}^#1[1,0]# -I: true +I: {?a: b} =====> -P: true^#1:bool# -L: true^#1[1,0]# +P: { + ?a^#3:Expr.Ident#:b^#4:Expr.Ident#^#2:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: { + ?a^#3[1,2]#:b^#4[1,5]#^#2[1,3]# +}^#1[1,0]# -I: false +I: {?'key': value} =====> -P: false^#1:bool# -L: false^#1[1,0]# +P: { + ?"key"^#3:string#:value^#4:Expr.Ident#^#2:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: { + ?"key"^#3[1,2]#:value^#4[1,9]#^#2[1,7]# +}^#1[1,0]# -I: 0 +I: foo{ } =====> -P: 0^#1:int64# -L: 0^#1[1,0]# +P: foo{}^#1:Expr.CreateStruct# +L: foo{}^#1[1,3]# -I: 42 +I: foo{ a:b } =====> -P: 42^#1:int64# -L: 42^#1[1,0]# - -I: 0u -=====> -P: 0u^#1:uint64# -L: 0u^#1[1,0]# - -I: 23u -=====> -P: 23u^#1:uint64# -L: 23u^#1[1,0]# +P: foo{ + a:b^#3:Expr.Ident#^#2:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: foo{ + a:b^#3[1,7]#^#2[1,6]# +}^#1[1,3]# -I: 24u +I: foo{ a:b, c:d } =====> -P: 24u^#1:uint64# -L: 24u^#1[1,0]# +P: foo{ + a:b^#3:Expr.Ident#^#2:Expr.CreateStruct.Entry#, + c:d^#5:Expr.Ident#^#4:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: foo{ + a:b^#3[1,7]#^#2[1,6]#, + c:d^#5[1,12]#^#4[1,11]# +}^#1[1,3]# -I: 0xAu +I: SomeMessage{foo: 5, bar: "xyz"} =====> -P: 10u^#1:uint64# -L: 10u^#1[1,0]# +P: SomeMessage{ + foo:5^#3:int64#^#2:Expr.CreateStruct.Entry#, + bar:"xyz"^#5:string#^#4:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: SomeMessage{ + foo:5^#3[1,17]#^#2[1,15]#, + bar:"xyz"^#5[1,25]#^#4[1,23]# +}^#1[1,11]# -I: -0xA +I: TestAllTypes{single_int32: 1, single_int64: 2} =====> -P: -10^#1:int64# -L: -10^#1[1,1]# +P: TestAllTypes{ + single_int32:1^#3:int64#^#2:Expr.CreateStruct.Entry#, + single_int64:2^#5:int64#^#4:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: TestAllTypes{ + single_int32:1^#3[1,27]#^#2[1,25]#, + single_int64:2^#5[1,44]#^#4[1,42]# +}^#1[1,12]# -I: 0xA +I: MyType{foo: 1, bar: 'baz'} =====> -P: 10^#1:int64# -L: 10^#1[1,0]# +P: MyType{ + foo:1^#3:int64#^#2:Expr.CreateStruct.Entry#, + bar:"baz"^#5:string#^#4:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: MyType{ + foo:1^#3[1,12]#^#2[1,10]#, + bar:"baz"^#5[1,20]#^#4[1,18]# +}^#1[1,6]# -I: -1 +I: Message{`in`: true} =====> -P: -1^#1:int64# -L: -1^#1[1,1]# +P: Message{ + in:true^#3:bool#^#2:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: Message{ + in:true^#3[1,14]#^#2[1,12]# +}^#1[1,7]# -I: 4--4 +I: Msg{?field: value} =====> -P: _-_( - 4^#1:int64#, - -4^#3:int64# -)^#2:Expr.Call# -L: _-_( - 4^#1[1,0]#, - -4^#3[1,3]# -)^#2[1,1]# +P: Msg{ + ?field:value^#3:Expr.Ident#^#2:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: Msg{ + ?field:value^#3[1,12]#^#2[1,10]# +}^#1[1,3]# -I: 4--4.1 +I: foo.bar.MyType{ } =====> -P: _-_( - 4^#1:int64#, - -4.1^#3:double# -)^#2:Expr.Call# -L: _-_( - 4^#1[1,0]#, - -4.1^#3[1,3]# -)^#2[1,1]# +P: foo.bar.MyType{}^#1:Expr.CreateStruct# +L: foo.bar.MyType{}^#1[1,14]# -I: b"abc" +I: foo.bar.MyType{ a:b } =====> -P: b"abc"^#1:bytes# -L: b"abc"^#1[1,0]# +P: foo.bar.MyType{ + a:b^#3:Expr.Ident#^#2:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: foo.bar.MyType{ + a:b^#3[1,18]#^#2[1,17]# +}^#1[1,14]# -I: 23.39 +I: .foo.bar.MyType{ a:b } =====> -P: 23.39^#1:double# -L: 23.39^#1[1,0]# +P: .foo.bar.MyType{ + a:b^#3:Expr.Ident#^#2:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: .foo.bar.MyType{ + a:b^#3[1,19]#^#2[1,18]# +}^#1[1,15]# -I: !a +I: a.b.c.d.Message{ foo: 1, bar: 'baz' } =====> -P: !_( - a^#2:Expr.Ident# -)^#1:Expr.Call# -L: !_( - a^#2[1,1]# -)^#1[1,0]# +P: a.b.c.d.Message{ + foo:1^#3:int64#^#2:Expr.CreateStruct.Entry#, + bar:"baz"^#5:string#^#4:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: a.b.c.d.Message{ + foo:1^#3[1,22]#^#2[1,20]#, + bar:"baz"^#5[1,30]#^#4[1,28]# +}^#1[1,15]# -I: null +I: a.b =====> -P: null^#1:NullValue# -L: null^#1[1,0]# +P: a^#1:Expr.Ident#.b^#2:Expr.Select# +L: a^#1[1,0]#.b^#2[1,1]# -I: a +I: a.b.c =====> -P: a^#1:Expr.Ident# -L: a^#1[1,0]# +P: a^#1:Expr.Ident#.b^#2:Expr.Select#.c^#3:Expr.Select# +L: a^#1[1,0]#.b^#2[1,1]#.c^#3[1,3]# -I: a?b:c +I: a.?b =====> -P: _?_:_( +P: _?._( a^#1:Expr.Ident#, - b^#3:Expr.Ident#, - c^#4:Expr.Ident# + "b"^#3:string# )^#2:Expr.Call# -L: _?_:_( +L: _?._( a^#1[1,0]#, - b^#3[1,2]#, - c^#4[1,4]# + "b"^#3[1,0]# )^#2[1,1]# -I: a || b +I: a.`b-c` =====> -P: _||_( - a^#1:Expr.Ident#, - b^#3:Expr.Ident# -)^#2:Expr.Call# -L: _||_( - a^#1[1,0]#, - b^#3[1,5]# -)^#2[1,2]# +P: a^#1:Expr.Ident#.b-c^#2:Expr.Select# +L: a^#1[1,0]#.b-c^#2[1,1]# -I: a || b || c || d || e || f +I: a.`b c` =====> -P: _||_( - _||_( - _||_( - a^#1:Expr.Ident#, - b^#3:Expr.Ident# - )^#2:Expr.Call#, - c^#5:Expr.Ident# - )^#4:Expr.Call#, - _||_( - _||_( - d^#7:Expr.Ident#, - e^#9:Expr.Ident# - )^#8:Expr.Call#, - f^#11:Expr.Ident# - )^#10:Expr.Call# -)^#6:Expr.Call# -L: _||_( - _||_( - _||_( - a^#1[1,0]#, - b^#3[1,5]# - )^#2[1,2]#, - c^#5[1,10]# - )^#4[1,7]#, - _||_( - _||_( - d^#7[1,15]#, - e^#9[1,20]# - )^#8[1,17]#, - f^#11[1,25]# - )^#10[1,22]# -)^#6[1,12]# +P: a^#1:Expr.Ident#.b c^#2:Expr.Select# +L: a^#1[1,0]#.b c^#2[1,1]# -I: a && b +I: a.`b.c` =====> -P: _&&_( - a^#1:Expr.Ident#, - b^#3:Expr.Ident# -)^#2:Expr.Call# -L: _&&_( - a^#1[1,0]#, - b^#3[1,5]# -)^#2[1,2]# +P: a^#1:Expr.Ident#.b.c^#2:Expr.Select# +L: a^#1[1,0]#.b.c^#2[1,1]# -I: a && b && c && d && e && f && g +I: a.`in` =====> -P: _&&_( - _&&_( - _&&_( - a^#1:Expr.Ident#, - b^#3:Expr.Ident# - )^#2:Expr.Call#, - _&&_( - c^#5:Expr.Ident#, - d^#7:Expr.Ident# - )^#6:Expr.Call# - )^#4:Expr.Call#, - _&&_( - _&&_( - e^#9:Expr.Ident#, - f^#11:Expr.Ident# - )^#10:Expr.Call#, - g^#13:Expr.Ident# - )^#12:Expr.Call# -)^#8:Expr.Call# -L: _&&_( - _&&_( - _&&_( - a^#1[1,0]#, - b^#3[1,5]# - )^#2[1,2]#, - _&&_( - c^#5[1,10]#, - d^#7[1,15]# - )^#6[1,12]# - )^#4[1,7]#, - _&&_( - _&&_( - e^#9[1,20]#, - f^#11[1,25]# - )^#10[1,22]#, - g^#13[1,30]# - )^#12[1,27]# -)^#8[1,17]# +P: a^#1:Expr.Ident#.in^#2:Expr.Select# +L: a^#1[1,0]#.in^#2[1,1]# -I: a && b && c && d || e && f && g && h +I: a.`/foo` =====> -P: _||_( - _&&_( - _&&_( - a^#1:Expr.Ident#, - b^#3:Expr.Ident# - )^#2:Expr.Call#, - _&&_( - c^#5:Expr.Ident#, - d^#7:Expr.Ident# - )^#6:Expr.Call# - )^#4:Expr.Call#, - _&&_( - _&&_( - e^#9:Expr.Ident#, - f^#11:Expr.Ident# - )^#10:Expr.Call#, - _&&_( - g^#13:Expr.Ident#, - h^#15:Expr.Ident# - )^#14:Expr.Call# - )^#12:Expr.Call# -)^#8:Expr.Call# -L: _||_( - _&&_( - _&&_( - a^#1[1,0]#, - b^#3[1,5]# - )^#2[1,2]#, - _&&_( - c^#5[1,10]#, - d^#7[1,15]# - )^#6[1,12]# - )^#4[1,7]#, - _&&_( - _&&_( - e^#9[1,20]#, - f^#11[1,25]# - )^#10[1,22]#, - _&&_( - g^#13[1,30]#, - h^#15[1,35]# - )^#14[1,32]# - )^#12[1,27]# -)^#8[1,17]# +P: a^#1:Expr.Ident#./foo^#2:Expr.Select# +L: a^#1[1,0]#./foo^#2[1,1]# -I: a + b +I: a.`my-var` =====> -P: _+_( +P: a^#1:Expr.Ident#.my-var^#2:Expr.Select# +L: a^#1[1,0]#.my-var^#2[1,1]# + +I: a[b] +=====> +P: _[_]( a^#1:Expr.Ident#, b^#3:Expr.Ident# )^#2:Expr.Call# -L: _+_( +L: _[_]( a^#1[1,0]#, - b^#3[1,4]# -)^#2[1,2]# + b^#3[1,2]# +)^#2[1,1]# -I: a - b +I: a[0] =====> -P: _-_( +P: _[_]( a^#1:Expr.Ident#, - b^#3:Expr.Ident# + 0^#3:int64# )^#2:Expr.Call# -L: _-_( +L: _[_]( a^#1[1,0]#, - b^#3[1,4]# -)^#2[1,2]# + 0^#3[1,2]# +)^#2[1,1]# -I: a * b +I: a[3] =====> -P: _*_( +P: _[_]( a^#1:Expr.Ident#, - b^#3:Expr.Ident# + 3^#3:int64# )^#2:Expr.Call# -L: _*_( +L: _[_]( a^#1[1,0]#, - b^#3[1,4]# -)^#2[1,2]# + 3^#3[1,2]# +)^#2[1,1]# -I: a / b +I: [1,3,4][0] =====> -P: _/_( +P: _[_]( + [ + 1^#2:int64#, + 3^#3:int64#, + 4^#4:int64# + ]^#1:Expr.CreateList#, + 0^#6:int64# +)^#5:Expr.Call# +L: _[_]( + [ + 1^#2[1,1]#, + 3^#3[1,3]#, + 4^#4[1,5]# + ]^#1[1,0]#, + 0^#6[1,8]# +)^#5[1,7]# + +I: a[?0] +=====> +P: _[?_]( a^#1:Expr.Ident#, - b^#3:Expr.Ident# + 0^#3:int64# )^#2:Expr.Call# -L: _/_( +L: _[?_]( a^#1[1,0]#, - b^#3[1,4]# -)^#2[1,2]# + 0^#3[1,3]# +)^#2[1,1]# -I: a % b +I: a() =====> -P: _%_( - a^#1:Expr.Ident#, - b^#3:Expr.Ident# +P: a()^#1:Expr.Call# +L: a()^#1[1,1]# + +I: a(b) +=====> +P: a( + b^#2:Expr.Ident# +)^#1:Expr.Call# +L: a( + b^#2[1,2]# +)^#1[1,1]# + +I: a(b, c) +=====> +P: a( + b^#2:Expr.Ident#, + c^#3:Expr.Ident# +)^#1:Expr.Call# +L: a( + b^#2[1,2]#, + c^#3[1,5]# +)^#1[1,1]# + +I: a.b() +=====> +P: a^#1:Expr.Ident#.b()^#2:Expr.Call# +L: a^#1[1,0]#.b()^#2[1,3]# + +I: a.b(c) +=====> +P: a^#1:Expr.Ident#.b( + c^#3:Expr.Ident# )^#2:Expr.Call# -L: _%_( - a^#1[1,0]#, - b^#3[1,4]# +L: a^#1[1,0]#.b( + c^#3[1,4]# +)^#2[1,3]# + +I: a.b(5) +=====> +P: a^#1:Expr.Ident#.b( + 5^#3:int64# +)^#2:Expr.Call# +L: a^#1[1,0]#.b( + 5^#3[1,4]# +)^#2[1,3]# + +I: aaa.bbb(ccc) +=====> +P: aaa^#1:Expr.Ident#.bbb( + ccc^#3:Expr.Ident# +)^#2:Expr.Call# +L: aaa^#1[1,0]#.bbb( + ccc^#3[1,8]# +)^#2[1,7]# + +I: a.foo(1, 2) +=====> +P: a^#1:Expr.Ident#.foo( + 1^#3:int64#, + 2^#4:int64# +)^#2:Expr.Call# +L: a^#1[1,0]#.foo( + 1^#3[1,6]#, + 2^#4[1,9]# +)^#2[1,5]# + +I: !a +=====> +P: !_( + a^#2:Expr.Ident# +)^#1:Expr.Call# +L: !_( + a^#2[1,1]# +)^#1[1,0]# + +I: !x +=====> +P: !_( + x^#2:Expr.Ident# +)^#1:Expr.Call# +L: !_( + x^#2[1,1]# +)^#1[1,0]# + +I: ! false +=====> +P: !_( + false^#2:bool# +)^#1:Expr.Call# +L: !_( + false^#2[1,2]# +)^#1[1,0]# + +I: -a +=====> +P: -_( + a^#2:Expr.Ident# +)^#1:Expr.Call# +L: -_( + a^#2[1,1]# +)^#1[1,0]# + +I: ---a +=====> +P: -_( + a^#2:Expr.Ident# +)^#1:Expr.Call# +L: -_( + a^#2[1,3]# +)^#1[1,0]# + +I: x * 2 +=====> +P: _*_( + x^#1:Expr.Ident#, + 2^#3:int64# +)^#2:Expr.Call# +L: _*_( + x^#1[1,0]#, + 2^#3[1,4]# )^#2[1,2]# -I: a in b +I: x * 2u =====> -P: @in( - a^#1:Expr.Ident#, - b^#3:Expr.Ident# +P: _*_( + x^#1:Expr.Ident#, + 2u^#3:uint64# )^#2:Expr.Call# -L: @in( - a^#1[1,0]#, - b^#3[1,5]# +L: _*_( + x^#1[1,0]#, + 2u^#3[1,4]# )^#2[1,2]# -I: a == b +I: x * 2.0 =====> -P: _==_( - a^#1:Expr.Ident#, - b^#3:Expr.Ident# +P: _*_( + x^#1:Expr.Ident#, + 2.0^#3:double# )^#2:Expr.Call# -L: _==_( - a^#1[1,0]#, - b^#3[1,5]# +L: _*_( + x^#1[1,0]#, + 2.0^#3[1,4]# )^#2[1,2]# -I: a != b +I: a * b =====> -P: _!=_( +P: _*_( a^#1:Expr.Ident#, b^#3:Expr.Ident# )^#2:Expr.Call# -L: _!=_( +L: _*_( a^#1[1,0]#, - b^#3[1,5]# + b^#3[1,4]# )^#2[1,2]# -I: a > b +I: a / b =====> -P: _>_( +P: _/_( a^#1:Expr.Ident#, b^#3:Expr.Ident# )^#2:Expr.Call# -L: _>_( +L: _/_( a^#1[1,0]#, b^#3[1,4]# )^#2[1,2]# -I: a >= b +I: a % b =====> -P: _>=_( +P: _%_( a^#1:Expr.Ident#, b^#3:Expr.Ident# )^#2:Expr.Call# -L: _>=_( +L: _%_( a^#1[1,0]#, - b^#3[1,5]# + b^#3[1,4]# )^#2[1,2]# -I: a < b +I: a + b =====> -P: _<_( +P: _+_( a^#1:Expr.Ident#, b^#3:Expr.Ident# )^#2:Expr.Call# -L: _<_( +L: _+_( a^#1[1,0]#, b^#3[1,4]# )^#2[1,2]# -I: a <= b +I: a - b =====> -P: _<=_( +P: _-_( a^#1:Expr.Ident#, b^#3:Expr.Ident# )^#2:Expr.Call# -L: _<=_( +L: _-_( a^#1[1,0]#, - b^#3[1,5]# + b^#3[1,4]# )^#2[1,2]# -I: a.b -=====> -P: a^#1:Expr.Ident#.b^#2:Expr.Select# -L: a^#1[1,0]#.b^#2[1,1]# - -I: a.b.c +I: 4--4 =====> -P: a^#1:Expr.Ident#.b^#2:Expr.Select#.c^#3:Expr.Select# -L: a^#1[1,0]#.b^#2[1,1]#.c^#3[1,3]# +P: _-_( + 4^#1:int64#, + -4^#3:int64# +)^#2:Expr.Call# +L: _-_( + 4^#1[1,0]#, + -4^#3[1,3]# +)^#2[1,1]# -I: a[b] +I: 4--4.1 =====> -P: _[_]( - a^#1:Expr.Ident#, - b^#3:Expr.Ident# +P: _-_( + 4^#1:int64#, + -4.1^#3:double# )^#2:Expr.Call# -L: _[_]( - a^#1[1,0]#, - b^#3[1,2]# +L: _-_( + 4^#1[1,0]#, + -4.1^#3[1,3]# )^#2[1,1]# -I: foo{ } +I: "abc" + "def" =====> -P: foo{}^#1:Expr.CreateStruct# -L: foo{}^#1[1,3]# +P: _+_( + "abc"^#1:string#, + "def"^#3:string# +)^#2:Expr.Call# +L: _+_( + "abc"^#1[1,0]#, + "def"^#3[1,8]# +)^#2[1,6]# -I: foo{ a:b } +I: b"abc" + B"def" =====> -P: foo{ - a:b^#3:Expr.Ident#^#2:Expr.CreateStruct.Entry# -}^#1:Expr.CreateStruct# -L: foo{ - a:b^#3[1,7]#^#2[1,6]# -}^#1[1,3]# - -I: foo{ a:b, c:d } -=====> -P: foo{ - a:b^#3:Expr.Ident#^#2:Expr.CreateStruct.Entry#, - c:d^#5:Expr.Ident#^#4:Expr.CreateStruct.Entry# -}^#1:Expr.CreateStruct# -L: foo{ - a:b^#3[1,7]#^#2[1,6]#, - c:d^#5[1,12]#^#4[1,11]# -}^#1[1,3]# - -I: {} -=====> -P: {}^#1:Expr.CreateStruct# -L: {}^#1[1,0]# - -I: {a:b, c:d} -=====> -P: { - a^#3:Expr.Ident#:b^#4:Expr.Ident#^#2:Expr.CreateStruct.Entry#, - c^#6:Expr.Ident#:d^#7:Expr.Ident#^#5:Expr.CreateStruct.Entry# -}^#1:Expr.CreateStruct# -L: { - a^#3[1,1]#:b^#4[1,3]#^#2[1,2]#, - c^#6[1,6]#:d^#7[1,8]#^#5[1,7]# -}^#1[1,0]# - -I: [] -=====> -P: []^#1:Expr.CreateList# -L: []^#1[1,0]# - -I: [a] -=====> -P: [ - a^#2:Expr.Ident# -]^#1:Expr.CreateList# -L: [ - a^#2[1,1]# -]^#1[1,0]# - -I: [a, b, c] -=====> -P: [ - a^#2:Expr.Ident#, - b^#3:Expr.Ident#, - c^#4:Expr.Ident# -]^#1:Expr.CreateList# -L: [ - a^#2[1,1]#, - b^#3[1,4]#, - c^#4[1,7]# -]^#1[1,0]# - -I: (a) -=====> -P: a^#1:Expr.Ident# -L: a^#1[1,1]# - -I: ((a)) -=====> -P: a^#1:Expr.Ident# -L: a^#1[1,2]# - -I: a() -=====> -P: a()^#1:Expr.Call# -L: a()^#1[1,1]# - -I: a(b) -=====> -P: a( - b^#2:Expr.Ident# -)^#1:Expr.Call# -L: a( - b^#2[1,2]# -)^#1[1,1]# - -I: a(b, c) -=====> -P: a( - b^#2:Expr.Ident#, - c^#3:Expr.Ident# -)^#1:Expr.Call# -L: a( - b^#2[1,2]#, - c^#3[1,5]# -)^#1[1,1]# - -I: a.b() -=====> -P: a^#1:Expr.Ident#.b()^#2:Expr.Call# -L: a^#1[1,0]#.b()^#2[1,3]# - -I: a.b(c) -=====> -P: a^#1:Expr.Ident#.b( - c^#3:Expr.Ident# -)^#2:Expr.Call# -L: a^#1[1,0]#.b( - c^#3[1,4]# -)^#2[1,3]# - -I: aaa.bbb(ccc) -=====> -P: aaa^#1:Expr.Ident#.bbb( - ccc^#3:Expr.Ident# +P: _+_( + b"abc"^#1:bytes#, + b"def"^#3:bytes# )^#2:Expr.Call# -L: aaa^#1[1,0]#.bbb( - ccc^#3[1,8]# +L: _+_( + b"abc"^#1[1,0]#, + b"def"^#3[1,9]# )^#2[1,7]# -I: has(m.f) -=====> -P: m^#2:Expr.Ident#.f~test-only~^#4:Expr.Select# -L: m^#2[1,4]#.f~test-only~^#4[1,3]# -M: has( - m^#2:Expr.Ident#.f^#3:Expr.Select# -)^#0:Expr.Call# - -I: m.exists_one(v, f) -=====> -P: __comprehension__( - // Variable - v, - // Target - m^#1:Expr.Ident#, - // Accumulator - @result, - // Init - 0^#5:int64#, - // LoopCondition - true^#6:bool#, - // LoopStep - _?_:_( - f^#4:Expr.Ident#, - _+_( - @result^#7:Expr.Ident#, - 1^#8:int64# - )^#9:Expr.Call#, - @result^#10:Expr.Ident# - )^#11:Expr.Call#, - // Result - _==_( - @result^#12:Expr.Ident#, - 1^#13:int64# - )^#14:Expr.Call#)^#15:Expr.Comprehension# -L: __comprehension__( - // Variable - v, - // Target - m^#1[1,0]#, - // Accumulator - @result, - // Init - 0^#5[1,12]#, - // LoopCondition - true^#6[1,12]#, - // LoopStep - _?_:_( - f^#4[1,16]#, - _+_( - @result^#7[1,12]#, - 1^#8[1,12]# - )^#9[1,12]#, - @result^#10[1,12]# - )^#11[1,12]#, - // Result - _==_( - @result^#12[1,12]#, - 1^#13[1,12]# - )^#14[1,12]#)^#15[1,12]# -M: m^#1:Expr.Ident#.exists_one( - v^#3:Expr.Ident#, - f^#4:Expr.Ident# -)^#0:Expr.Call# - -I: m.existsOne(v, f) -=====> -P: __comprehension__( - // Variable - v, - // Target - m^#1:Expr.Ident#, - // Accumulator - @result, - // Init - 0^#5:int64#, - // LoopCondition - true^#6:bool#, - // LoopStep - _?_:_( - f^#4:Expr.Ident#, - _+_( - @result^#7:Expr.Ident#, - 1^#8:int64# - )^#9:Expr.Call#, - @result^#10:Expr.Ident# - )^#11:Expr.Call#, - // Result - _==_( - @result^#12:Expr.Ident#, - 1^#13:int64# - )^#14:Expr.Call#)^#15:Expr.Comprehension# -L: __comprehension__( - // Variable - v, - // Target - m^#1[1,0]#, - // Accumulator - @result, - // Init - 0^#5[1,11]#, - // LoopCondition - true^#6[1,11]#, - // LoopStep - _?_:_( - f^#4[1,15]#, - _+_( - @result^#7[1,11]#, - 1^#8[1,11]# - )^#9[1,11]#, - @result^#10[1,11]# - )^#11[1,11]#, - // Result - _==_( - @result^#12[1,11]#, - 1^#13[1,11]# - )^#14[1,11]#)^#15[1,11]# -M: m^#1:Expr.Ident#.existsOne( - v^#3:Expr.Ident#, - f^#4:Expr.Ident# -)^#0:Expr.Call# - -I: m.map(v, f) -=====> -P: __comprehension__( - // Variable - v, - // Target - m^#1:Expr.Ident#, - // Accumulator - @result, - // Init - []^#5:Expr.CreateList#, - // LoopCondition - true^#6:bool#, - // LoopStep - _+_( - @result^#7:Expr.Ident#, - [ - f^#4:Expr.Ident# - ]^#8:Expr.CreateList# - )^#9:Expr.Call#, - // Result - @result^#10:Expr.Ident#)^#11:Expr.Comprehension# -L: __comprehension__( - // Variable - v, - // Target - m^#1[1,0]#, - // Accumulator - @result, - // Init - []^#5[1,5]#, - // LoopCondition - true^#6[1,5]#, - // LoopStep - _+_( - @result^#7[1,5]#, - [ - f^#4[1,9]# - ]^#8[1,5]# - )^#9[1,5]#, - // Result - @result^#10[1,5]#)^#11[1,5]# -M: m^#1:Expr.Ident#.map( - v^#3:Expr.Ident#, - f^#4:Expr.Ident# -)^#0:Expr.Call# - -I: m.map(v, p, f) -=====> -P: __comprehension__( - // Variable - v, - // Target - m^#1:Expr.Ident#, - // Accumulator - @result, - // Init - []^#6:Expr.CreateList#, - // LoopCondition - true^#7:bool#, - // LoopStep - _?_:_( - p^#4:Expr.Ident#, - _+_( - @result^#8:Expr.Ident#, - [ - f^#5:Expr.Ident# - ]^#9:Expr.CreateList# - )^#10:Expr.Call#, - @result^#11:Expr.Ident# - )^#12:Expr.Call#, - // Result - @result^#13:Expr.Ident#)^#14:Expr.Comprehension# -L: __comprehension__( - // Variable - v, - // Target - m^#1[1,0]#, - // Accumulator - @result, - // Init - []^#6[1,5]#, - // LoopCondition - true^#7[1,5]#, - // LoopStep - _?_:_( - p^#4[1,9]#, - _+_( - @result^#8[1,5]#, - [ - f^#5[1,12]# - ]^#9[1,5]# - )^#10[1,5]#, - @result^#11[1,5]# - )^#12[1,5]#, - // Result - @result^#13[1,5]#)^#14[1,5]# -M: m^#1:Expr.Ident#.map( - v^#3:Expr.Ident#, - p^#4:Expr.Ident#, - f^#5:Expr.Ident# -)^#0:Expr.Call# - -I: m.filter(v, p) -=====> -P: __comprehension__( - // Variable - v, - // Target - m^#1:Expr.Ident#, - // Accumulator - @result, - // Init - []^#5:Expr.CreateList#, - // LoopCondition - true^#6:bool#, - // LoopStep - _?_:_( - p^#4:Expr.Ident#, - _+_( - @result^#7:Expr.Ident#, - [ - v^#3:Expr.Ident# - ]^#8:Expr.CreateList# - )^#9:Expr.Call#, - @result^#10:Expr.Ident# - )^#11:Expr.Call#, - // Result - @result^#12:Expr.Ident#)^#13:Expr.Comprehension# -L: __comprehension__( - // Variable - v, - // Target - m^#1[1,0]#, - // Accumulator - @result, - // Init - []^#5[1,8]#, - // LoopCondition - true^#6[1,8]#, - // LoopStep - _?_:_( - p^#4[1,12]#, - _+_( - @result^#7[1,8]#, - [ - v^#3[1,9]# - ]^#8[1,8]# - )^#9[1,8]#, - @result^#10[1,8]# - )^#11[1,8]#, - // Result - @result^#12[1,8]#)^#13[1,8]# -M: m^#1:Expr.Ident#.filter( - v^#3:Expr.Ident#, - p^#4:Expr.Ident# -)^#0:Expr.Call# - I: [] + [1,2,3,] + [4] =====> P: _+_( @@ -1028,27 +714,118 @@ L: _+_( ]^#8[1,16]# )^#7[1,14]# -I: {1:2u, 2:3u} +I: 1 + 2 * 3 =====> -P: { - 1^#3:int64#:2u^#4:uint64#^#2:Expr.CreateStruct.Entry#, - 2^#6:int64#:3u^#7:uint64#^#5:Expr.CreateStruct.Entry# -}^#1:Expr.CreateStruct# -L: { - 1^#3[1,1]#:2u^#4[1,3]#^#2[1,2]#, - 2^#6[1,7]#:3u^#7[1,9]#^#5[1,8]# -}^#1[1,0]# +P: _+_( + 1^#1:int64#, + _*_( + 2^#3:int64#, + 3^#5:int64# + )^#4:Expr.Call# +)^#2:Expr.Call# +L: _+_( + 1^#1[1,0]#, + _*_( + 2^#3[1,4]#, + 3^#5[1,8]# + )^#4[1,6]# +)^#2[1,2]# -I: TestAllTypes{single_int32: 1, single_int64: 2} +I: a == b =====> -P: TestAllTypes{ - single_int32:1^#3:int64#^#2:Expr.CreateStruct.Entry#, - single_int64:2^#5:int64#^#4:Expr.CreateStruct.Entry# -}^#1:Expr.CreateStruct# -L: TestAllTypes{ - single_int32:1^#3[1,27]#^#2[1,25]#, - single_int64:2^#5[1,44]#^#4[1,42]# -}^#1[1,12]# +P: _==_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _==_( + a^#1[1,0]#, + b^#3[1,5]# +)^#2[1,2]# + +I: a != b +=====> +P: _!=_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _!=_( + a^#1[1,0]#, + b^#3[1,5]# +)^#2[1,2]# + +I: a < b +=====> +P: _<_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _<_( + a^#1[1,0]#, + b^#3[1,4]# +)^#2[1,2]# + +I: a <= b +=====> +P: _<=_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _<=_( + a^#1[1,0]#, + b^#3[1,5]# +)^#2[1,2]# + +I: a > b +=====> +P: _>_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _>_( + a^#1[1,0]#, + b^#3[1,4]# +)^#2[1,2]# + +I: a >= b +=====> +P: _>=_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _>=_( + a^#1[1,0]#, + b^#3[1,5]# +)^#2[1,2]# + +I: a in b +=====> +P: @in( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: @in( + a^#1[1,0]#, + b^#3[1,5]# +)^#2[1,2]# + +I: "😁" in ["😁", "😑", "😦"] +=====> +P: @in( + "😁"^#1:string#, + [ + "😁"^#4:string#, + "😑"^#5:string#, + "😦"^#6:string# + ]^#3:Expr.CreateList# +)^#2:Expr.Call# +L: @in( + "😁"^#1[1,0]#, + [ + "😁"^#4[1,8]#, + "😑"^#5[1,13]#, + "😦"^#6[1,18]# + ]^#3[1,7]# +)^#2[1,4]# I: size(x) == x.size() =====> @@ -1065,57 +842,250 @@ L: _==_( x^#4[1,11]#.size()^#5[1,17]# )^#3[1,8]# -I: "\"" +I: x.single_nested_message != null +=====> +P: _!=_( + x^#1:Expr.Ident#.single_nested_message^#2:Expr.Select#, + null^#4:NullValue# +)^#3:Expr.Call# +L: _!=_( + x^#1[1,0]#.single_nested_message^#2[1,1]#, + null^#4[1,27]# +)^#3[1,24]# + +I: a && b +=====> +P: _&&_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _&&_( + a^#1[1,0]#, + b^#3[1,5]# +)^#2[1,2]# + +I: a && b && c +=====> +P: _&&_( + _&&_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# + )^#2:Expr.Call#, + c^#5:Expr.Ident# +)^#4:Expr.Call# +L: _&&_( + _&&_( + a^#1[1,0]#, + b^#3[1,5]# + )^#2[1,2]#, + c^#5[1,10]# +)^#4[1,7]# + +I: a && b && c && d && e && f && g +=====> +P: _&&_( + _&&_( + _&&_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# + )^#2:Expr.Call#, + _&&_( + c^#5:Expr.Ident#, + d^#7:Expr.Ident# + )^#6:Expr.Call# + )^#4:Expr.Call#, + _&&_( + _&&_( + e^#9:Expr.Ident#, + f^#11:Expr.Ident# + )^#10:Expr.Call#, + g^#13:Expr.Ident# + )^#12:Expr.Call# +)^#8:Expr.Call# +L: _&&_( + _&&_( + _&&_( + a^#1[1,0]#, + b^#3[1,5]# + )^#2[1,2]#, + _&&_( + c^#5[1,10]#, + d^#7[1,15]# + )^#6[1,12]# + )^#4[1,7]#, + _&&_( + _&&_( + e^#9[1,20]#, + f^#11[1,25]# + )^#10[1,22]#, + g^#13[1,30]# + )^#12[1,27]# +)^#8[1,17]# + +I: a > 5 && a < 10 +=====> +P: _&&_( + _>_( + a^#1:Expr.Ident#, + 5^#3:int64# + )^#2:Expr.Call#, + _<_( + a^#5:Expr.Ident#, + 10^#7:int64# + )^#6:Expr.Call# +)^#4:Expr.Call# +L: _&&_( + _>_( + a^#1[1,0]#, + 5^#3[1,4]# + )^#2[1,2]#, + _<_( + a^#5[1,9]#, + 10^#7[1,13]# + )^#6[1,11]# +)^#4[1,6]# + +I: a || b +=====> +P: _||_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _||_( + a^#1[1,0]#, + b^#3[1,5]# +)^#2[1,2]# + +I: a || b || c || d || e || f +=====> +P: _||_( + _||_( + _||_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# + )^#2:Expr.Call#, + c^#5:Expr.Ident# + )^#4:Expr.Call#, + _||_( + _||_( + d^#7:Expr.Ident#, + e^#9:Expr.Ident# + )^#8:Expr.Call#, + f^#11:Expr.Ident# + )^#10:Expr.Call# +)^#6:Expr.Call# +L: _||_( + _||_( + _||_( + a^#1[1,0]#, + b^#3[1,5]# + )^#2[1,2]#, + c^#5[1,10]# + )^#4[1,7]#, + _||_( + _||_( + d^#7[1,15]#, + e^#9[1,20]# + )^#8[1,17]#, + f^#11[1,25]# + )^#10[1,22]# +)^#6[1,12]# + +I: a < 5 || a > 10 =====> -P: "\""^#1:string# -L: "\""^#1[1,0]# +P: _||_( + _<_( + a^#1:Expr.Ident#, + 5^#3:int64# + )^#2:Expr.Call#, + _>_( + a^#5:Expr.Ident#, + 10^#7:int64# + )^#6:Expr.Call# +)^#4:Expr.Call# +L: _||_( + _<_( + a^#1[1,0]#, + 5^#3[1,4]# + )^#2[1,2]#, + _>_( + a^#5[1,9]#, + 10^#7[1,13]# + )^#6[1,11]# +)^#4[1,6]# -I: [1,3,4][0] +I: a && b && c && d || e && f && g && h =====> -P: _[_]( - [ - 1^#2:int64#, - 3^#3:int64#, - 4^#4:int64# - ]^#1:Expr.CreateList#, - 0^#6:int64# -)^#5:Expr.Call# -L: _[_]( - [ - 1^#2[1,1]#, - 3^#3[1,3]#, - 4^#4[1,5]# - ]^#1[1,0]#, - 0^#6[1,8]# -)^#5[1,7]# +P: _||_( + _&&_( + _&&_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# + )^#2:Expr.Call#, + _&&_( + c^#5:Expr.Ident#, + d^#7:Expr.Ident# + )^#6:Expr.Call# + )^#4:Expr.Call#, + _&&_( + _&&_( + e^#9:Expr.Ident#, + f^#11:Expr.Ident# + )^#10:Expr.Call#, + _&&_( + g^#13:Expr.Ident#, + h^#15:Expr.Ident# + )^#14:Expr.Call# + )^#12:Expr.Call# +)^#8:Expr.Call# +L: _||_( + _&&_( + _&&_( + a^#1[1,0]#, + b^#3[1,5]# + )^#2[1,2]#, + _&&_( + c^#5[1,10]#, + d^#7[1,15]# + )^#6[1,12]# + )^#4[1,7]#, + _&&_( + _&&_( + e^#9[1,20]#, + f^#11[1,25]# + )^#10[1,22]#, + _&&_( + g^#13[1,30]#, + h^#15[1,35]# + )^#14[1,32]# + )^#12[1,27]# +)^#8[1,17]# -I: x["a"].single_int32 == 23 +I: a?b:c =====> -P: _==_( - _[_]( - x^#1:Expr.Ident#, - "a"^#3:string# - )^#2:Expr.Call#.single_int32^#4:Expr.Select#, - 23^#6:int64# -)^#5:Expr.Call# -L: _==_( - _[_]( - x^#1[1,0]#, - "a"^#3[1,2]# - )^#2[1,1]#.single_int32^#4[1,6]#, - 23^#6[1,23]# -)^#5[1,20]# +P: _?_:_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident#, + c^#4:Expr.Ident# +)^#2:Expr.Call# +L: _?_:_( + a^#1[1,0]#, + b^#3[1,2]#, + c^#4[1,4]# +)^#2[1,1]# -I: x.single_nested_message != null +I: cond ? 1 : 2 =====> -P: _!=_( - x^#1:Expr.Ident#.single_nested_message^#2:Expr.Select#, - null^#4:NullValue# -)^#3:Expr.Call# -L: _!=_( - x^#1[1,0]#.single_nested_message^#2[1,1]#, - null^#4[1,27]# -)^#3[1,24]# +P: _?_:_( + cond^#1:Expr.Ident#, + 1^#3:int64#, + 2^#4:int64# +)^#2:Expr.Call# +L: _?_:_( + cond^#1[1,0]#, + 1^#3[1,7]#, + 2^#4[1,11]# +)^#2[1,5]# I: false && !true || false ? 2 : 3 =====> @@ -1146,16 +1116,56 @@ L: _?_:_( 3^#9[1,30]# )^#7[1,24]# -I: b"abc" + B"def" -=====> -P: _+_( - b"abc"^#1:bytes#, - b"def"^#3:bytes# -)^#2:Expr.Call# -L: _+_( - b"abc"^#1[1,0]#, - b"def"^#3[1,9]# -)^#2[1,7]# +I: true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : 1 +=====> + +I: !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-x +=====> +E/A: ERROR: :1:3: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-x + | ..^ +ERROR: :1:5: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-x + | ....^ +ERROR: :1:7: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-x + | ......^ +ERROR: :1:9: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-x + | ........^ +ERROR: :1:11: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-x + | ..........^ +ERROR: :1:13: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-x + | ............^ +ERROR: :1:15: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-x + | ..............^ +ERROR: :1:17: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-x + | ................^ +ERROR: :1:19: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-x + | ..................^ +ERROR: :1:21: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-x + | ....................^ +ERROR: :1:23: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-x + | ......................^ +ERROR: :1:25: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-x + | ........................^ +ERROR: :1:27: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-x + | ..........................^ +ERROR: :1:29: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-x + | ............................^ +ERROR: :1:31: no viable alternative at input '-x' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-x + | ..............................^ I: 1 + 2 * 3 - 1 / 2 == 6 % 1 =====> @@ -1198,410 +1208,22 @@ L: _==_( )^#12[1,23]# )^#10[1,18]# -I: ---a -=====> -P: -_( - a^#2:Expr.Ident# -)^#1:Expr.Call# -L: -_( - a^#2[1,3]# -)^#1[1,0]# - -I: "\xC3\XBF" -=====> -P: "ÿ"^#1:string# -L: "ÿ"^#1[1,0]# - -I: "\303\277" -=====> -P: "ÿ"^#1:string# -L: "ÿ"^#1[1,0]# - -I: "hi\u263A \u263Athere" -=====> -P: "hi☺ ☺there"^#1:string# -L: "hi☺ ☺there"^#1[1,0]# - -I: "\U000003A8\?" -=====> -P: "Ψ?"^#1:string# -L: "Ψ?"^#1[1,0]# - -I: "\a\b\f\n\r\t\v'\"\\\? Legal escapes" -=====> -P: "\a\b\f\n\r\t\v'\"\? Legal escapes"^#1:string# -L: "\a\b\f\n\r\t\v'\"\? Legal escapes"^#1[1,0]# - -I: '😁' in ['😁', '😑', '😦'] -=====> -P: @in( - "😁"^#1:string#, - [ - "😁"^#4:string#, - "😑"^#5:string#, - "😦"^#6:string# - ]^#3:Expr.CreateList# -)^#2:Expr.Call# -L: @in( - "😁"^#1[1,0]#, - [ - "😁"^#4[1,8]#, - "😑"^#5[1,13]#, - "😦"^#6[1,18]# - ]^#3[1,7]# -)^#2[1,4]# - -I: [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['just fine'],[1],[2],[3],[4],[5]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] -=====> - -I: x.filter(y, y.filter(z, z > 0)) -=====> -P: __comprehension__( - // Variable - y, - // Target - x^#1:Expr.Ident#, - // Accumulator - @result, - // Init - []^#19:Expr.CreateList#, - // LoopCondition - true^#20:bool#, - // LoopStep - _?_:_( - __comprehension__( - // Variable - z, - // Target - y^#4:Expr.Ident#, - // Accumulator - @result, - // Init - []^#10:Expr.CreateList#, - // LoopCondition - true^#11:bool#, - // LoopStep - _?_:_( - _>_( - z^#7:Expr.Ident#, - 0^#9:int64# - )^#8:Expr.Call#, - _+_( - @result^#12:Expr.Ident#, - [ - z^#6:Expr.Ident# - ]^#13:Expr.CreateList# - )^#14:Expr.Call#, - @result^#15:Expr.Ident# - )^#16:Expr.Call#, - // Result - @result^#17:Expr.Ident#)^#18:Expr.Comprehension#, - _+_( - @result^#21:Expr.Ident#, - [ - y^#3:Expr.Ident# - ]^#22:Expr.CreateList# - )^#23:Expr.Call#, - @result^#24:Expr.Ident# - )^#25:Expr.Call#, - // Result - @result^#26:Expr.Ident#)^#27:Expr.Comprehension# -L: __comprehension__( - // Variable - y, - // Target - x^#1[1,0]#, - // Accumulator - @result, - // Init - []^#19[1,8]#, - // LoopCondition - true^#20[1,8]#, - // LoopStep - _?_:_( - __comprehension__( - // Variable - z, - // Target - y^#4[1,12]#, - // Accumulator - @result, - // Init - []^#10[1,20]#, - // LoopCondition - true^#11[1,20]#, - // LoopStep - _?_:_( - _>_( - z^#7[1,24]#, - 0^#9[1,28]# - )^#8[1,26]#, - _+_( - @result^#12[1,20]#, - [ - z^#6[1,21]# - ]^#13[1,20]# - )^#14[1,20]#, - @result^#15[1,20]# - )^#16[1,20]#, - // Result - @result^#17[1,20]#)^#18[1,20]#, - _+_( - @result^#21[1,8]#, - [ - y^#3[1,9]# - ]^#22[1,8]# - )^#23[1,8]#, - @result^#24[1,8]# - )^#25[1,8]#, - // Result - @result^#26[1,8]#)^#27[1,8]# -M: x^#1:Expr.Ident#.filter( - y^#3:Expr.Ident#, - ^#18:filter# -)^#0:Expr.Call#, -y^#4:Expr.Ident#.filter( - z^#6:Expr.Ident#, - _>_( - z^#7:Expr.Ident#, - 0^#9:int64# - )^#8:Expr.Call# -)^#0:Expr.Call# - -I: has(a.b).filter(c, c) -=====> -P: __comprehension__( - // Variable - c, - // Target - a^#2:Expr.Ident#.b~test-only~^#4:Expr.Select#, - // Accumulator - @result, - // Init - []^#8:Expr.CreateList#, - // LoopCondition - true^#9:bool#, - // LoopStep - _?_:_( - c^#7:Expr.Ident#, - _+_( - @result^#10:Expr.Ident#, - [ - c^#6:Expr.Ident# - ]^#11:Expr.CreateList# - )^#12:Expr.Call#, - @result^#13:Expr.Ident# - )^#14:Expr.Call#, - // Result - @result^#15:Expr.Ident#)^#16:Expr.Comprehension# -L: __comprehension__( - // Variable - c, - // Target - a^#2[1,4]#.b~test-only~^#4[1,3]#, - // Accumulator - @result, - // Init - []^#8[1,15]#, - // LoopCondition - true^#9[1,15]#, - // LoopStep - _?_:_( - c^#7[1,19]#, - _+_( - @result^#10[1,15]#, - [ - c^#6[1,16]# - ]^#11[1,15]# - )^#12[1,15]#, - @result^#13[1,15]# - )^#14[1,15]#, - // Result - @result^#15[1,15]#)^#16[1,15]# -M: ^#4:has#.filter( - c^#6:Expr.Ident#, - c^#7:Expr.Ident# -)^#0:Expr.Call#, -has( - a^#2:Expr.Ident#.b^#3:Expr.Select# -)^#0:Expr.Call# - -I: x.filter(y, y.exists(z, has(z.a)) && y.exists(z, has(z.b))) -=====> -P: __comprehension__( - // Variable - y, - // Target - x^#1:Expr.Ident#, - // Accumulator - @result, - // Init - []^#35:Expr.CreateList#, - // LoopCondition - true^#36:bool#, - // LoopStep - _?_:_( - _&&_( - __comprehension__( - // Variable - z, - // Target - y^#4:Expr.Ident#, - // Accumulator - @result, - // Init - false^#11:bool#, - // LoopCondition - @not_strictly_false( - !_( - @result^#12:Expr.Ident# - )^#13:Expr.Call# - )^#14:Expr.Call#, - // LoopStep - _||_( - @result^#15:Expr.Ident#, - z^#8:Expr.Ident#.a~test-only~^#10:Expr.Select# - )^#16:Expr.Call#, - // Result - @result^#17:Expr.Ident#)^#18:Expr.Comprehension#, - __comprehension__( - // Variable - z, - // Target - y^#20:Expr.Ident#, - // Accumulator - @result, - // Init - false^#27:bool#, - // LoopCondition - @not_strictly_false( - !_( - @result^#28:Expr.Ident# - )^#29:Expr.Call# - )^#30:Expr.Call#, - // LoopStep - _||_( - @result^#31:Expr.Ident#, - z^#24:Expr.Ident#.b~test-only~^#26:Expr.Select# - )^#32:Expr.Call#, - // Result - @result^#33:Expr.Ident#)^#34:Expr.Comprehension# - )^#19:Expr.Call#, - _+_( - @result^#37:Expr.Ident#, - [ - y^#3:Expr.Ident# - ]^#38:Expr.CreateList# - )^#39:Expr.Call#, - @result^#40:Expr.Ident# - )^#41:Expr.Call#, - // Result - @result^#42:Expr.Ident#)^#43:Expr.Comprehension# -L: __comprehension__( - // Variable - y, - // Target - x^#1[1,0]#, - // Accumulator - @result, - // Init - []^#35[1,8]#, - // LoopCondition - true^#36[1,8]#, - // LoopStep - _?_:_( - _&&_( - __comprehension__( - // Variable - z, - // Target - y^#4[1,12]#, - // Accumulator - @result, - // Init - false^#11[1,20]#, - // LoopCondition - @not_strictly_false( - !_( - @result^#12[1,20]# - )^#13[1,20]# - )^#14[1,20]#, - // LoopStep - _||_( - @result^#15[1,20]#, - z^#8[1,28]#.a~test-only~^#10[1,27]# - )^#16[1,20]#, - // Result - @result^#17[1,20]#)^#18[1,20]#, - __comprehension__( - // Variable - z, - // Target - y^#20[1,37]#, - // Accumulator - @result, - // Init - false^#27[1,45]#, - // LoopCondition - @not_strictly_false( - !_( - @result^#28[1,45]# - )^#29[1,45]# - )^#30[1,45]#, - // LoopStep - _||_( - @result^#31[1,45]#, - z^#24[1,53]#.b~test-only~^#26[1,52]# - )^#32[1,45]#, - // Result - @result^#33[1,45]#)^#34[1,45]# - )^#19[1,34]#, - _+_( - @result^#37[1,8]#, - [ - y^#3[1,9]# - ]^#38[1,8]# - )^#39[1,8]#, - @result^#40[1,8]# - )^#41[1,8]#, - // Result - @result^#42[1,8]#)^#43[1,8]# -M: x^#1:Expr.Ident#.filter( - y^#3:Expr.Ident#, - _&&_( - ^#18:exists#, - ^#34:exists# - )^#19:Expr.Call# -)^#0:Expr.Call#, -y^#20:Expr.Ident#.exists( - z^#22:Expr.Ident#, - ^#26:has# -)^#0:Expr.Call#, -has( - z^#24:Expr.Ident#.b^#25:Expr.Select# -)^#0:Expr.Call#, -y^#4:Expr.Ident#.exists( - z^#6:Expr.Ident#, - ^#10:has# -)^#0:Expr.Call#, -has( - z^#8:Expr.Ident#.a^#9:Expr.Select# -)^#0:Expr.Call# - -I: noop_macro(123) -=====> -P: noop_macro( - 123^#2:int64# -)^#1:Expr.Call# -L: noop_macro( - 123^#2[1,11]# -)^#1[1,10]# - -I: get_constant_macro() +I: x["a"].single_int32 == 23 =====> -P: 10^#1:int64# -L: 10^#1[NO_POS]# -M: get_constant_macro()^#0:Expr.Call# +P: _==_( + _[_]( + x^#1:Expr.Ident#, + "a"^#3:string# + )^#2:Expr.Call#.single_int32^#4:Expr.Select#, + 23^#6:int64# +)^#5:Expr.Call# +L: _==_( + _[_]( + x^#1[1,0]#, + "a"^#3[1,2]# + )^#2[1,1]#.single_int32^#4[1,6]#, + 23^#6[1,23]# +)^#5[1,20]# I: a.?b[?0] && a[?c] =====> @@ -1632,48 +1254,63 @@ L: _&&_( )^#8[1,13]# )^#6[1,9]# -I: {?'key': value} +I: [[[[[[[[[[[[[[[[[[[[[[[[[[[[[['just fine'],[1],[2],[3],[4],[5]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] =====> -P: { - ?"key"^#3:string#:value^#4:Expr.Ident#^#2:Expr.CreateStruct.Entry# -}^#1:Expr.CreateStruct# -L: { - ?"key"^#3[1,2]#:value^#4[1,9]#^#2[1,7]# -}^#1[1,0]# +E/A: ERROR: :1:92: mismatched input ']' expecting {, '==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', '.', '-', '?', '+', '*', '/', '%%'} + | [[[[[[[[[[[[[[[[[[[[[[[[[[[[[['just fine'],[1],[2],[3],[4],[5]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] + | ...........................................................................................^ +E/P: ERROR: :1:92: Syntax error: unexpected token after expression + | [[[[[[[[[[[[[[[[[[[[[[[[[[[[[['just fine'],[1],[2],[3],[4],[5]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] + | ...........................................................................................^ -I: Msg{?field: value} +I: // comment +a =====> -P: Msg{ - ?field:value^#3:Expr.Ident#^#2:Expr.CreateStruct.Entry# -}^#1:Expr.CreateStruct# -L: Msg{ - ?field:value^#3[1,12]#^#2[1,10]# -}^#1[1,3]# +P: a^#1:Expr.Ident# +L: a^#1[2,0]# -I: [?a, ?b] +I: a // comment =====> -P: [ - ?a^#2:Expr.Ident#, - ?b^#3:Expr.Ident# -]^#1:Expr.CreateList# -L: [ - ?a^#2[1,2]#, - ?b^#3[1,6]# -]^#1[1,0]# +P: a^#1:Expr.Ident# +L: a^#1[1,0]# -I: [?a[?b]] +I: a +// comment ++ b +=====> +P: _+_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _+_( + a^#1[1,0]#, + b^#3[3,2]# +)^#2[3,0]# + +I: a / // comment + b +=====> +P: _/_( + a^#1:Expr.Ident#, + b^#3:Expr.Ident# +)^#2:Expr.Call# +L: _/_( + a^#1[1,0]#, + b^#3[2,2]# +)^#2[1,2]# + +I: [ + 1, // comment + 2, +] =====> P: [ - ?_[?_]( - a^#2:Expr.Ident#, - b^#4:Expr.Ident# - )^#3:Expr.Call# + 1^#2:int64#, + 2^#3:int64# ]^#1:Expr.CreateList# L: [ - ?_[?_]( - a^#2[1,2]#, - b^#4[1,5]# - )^#3[1,3]# + 1^#2[2,2]#, + 2^#3[3,2]# ]^#1[1,0]# I: while diff --git a/parser/src/test/resources/parser_errors.baseline b/parser/src/test/resources/parser_errors.baseline index bb4ab3ed3..cbd9f087b 100644 --- a/parser/src/test/resources/parser_errors.baseline +++ b/parser/src/test/resources/parser_errors.baseline @@ -1,6 +1,6 @@ I: *@a | b =====> -E: ERROR: :1:1: extraneous input '*' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} +E/A: ERROR: :1:1: extraneous input '*' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} | *@a | b | ^ ERROR: :1:2: token recognition error at: '@' @@ -12,271 +12,505 @@ ERROR: :1:5: token recognition error at: '| ' ERROR: :1:7: extraneous input 'b' expecting | *@a | b | ......^ +E/P: ERROR: :1:1: Syntax error: unexpected token + | *@a | b + | ^ +ERROR: :1:2: Syntax error: unexpected character + | *@a | b + | .^ -I: a | b +I: ((@)) =====> -E: ERROR: :1:3: token recognition error at: '| ' - | a | b +E/A: ERROR: :1:3: token recognition error at: '@' + | ((@)) + | ..^ +ERROR: :1:4: mismatched input ')' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | ((@)) + | ...^ +E/P: ERROR: :1:3: Syntax error: unexpected character + | ((@)) | ..^ -ERROR: :1:5: extraneous input 'b' expecting - | a | b - | ....^ - -I: ? -=====> -E: ERROR: :1:1: mismatched input '?' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} - | ? - | ^ -ERROR: :1:2: mismatched input '' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} - | ? - | .^ I: 1 + $ =====> -E: ERROR: :1:5: token recognition error at: '$' +E/A: ERROR: :1:5: token recognition error at: '$' | 1 + $ | ....^ ERROR: :1:6: mismatched input '' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} | 1 + $ | .....^ +E/P: ERROR: :1:5: Syntax error: unexpected character + | 1 + $ + | ....^ -I: 1.all(2, 3) -=====> -E: ERROR: :1:7: The argument must be a simple name - | 1.all(2, 3) - | ......^ - -I: 1.exists(2, 3) -=====> -E: ERROR: :1:10: The argument must be a simple name - | 1.exists(2, 3) - | .........^ - -I: [].all(__result__, x) -=====> -E: ERROR: :1:8: The iteration variable __result__ overwrites accumulator variable - | [].all(__result__, x) - | .......^ - -I: [].exists(__result__, x) +I: ó ¢ +»»ó 0  +»»\u007f0"""\""\"""\""\"""\""\"""\""\"""\"\"""\""\"""\""\"""\""\"""\"!\"""\""\"""\""\" =====> -E: ERROR: :1:11: The iteration variable __result__ overwrites accumulator variable - | [].exists(__result__, x) +E/A: ERROR: :1:1: token recognition error at: 'ó' + | ó ¢ + | ^ +ERROR: :1:2: token recognition error at: ' ' + | ó ¢ + | .^ +ERROR: :1:3: token recognition error at: '¢' + | ó ¢ + | ..^ +ERROR: :2:3: token recognition error at: 'ó' + | ó 0  + | ..^ +ERROR: :2:4: token recognition error at: ' ' + | ó 0  + | ...^ +ERROR: :2:6: token recognition error at: ' ' + | ó 0  + | .....^ +ERROR: :3:3: token recognition error at: '\u007f' + | \u007f0"""\""\"""\""\"""\""\"""\""\"""\"\"""\""\"""\""\"""\""\"""\"!\"""\""\"""\""\" + | ..^ +ERROR: :3:4: mismatched input '0' expecting {, '==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', '.', '-', '?', '+', '*', '/', '%%'} + | \u007f0"""\""\"""\""\"""\""\"""\""\"""\"\"""\""\"""\""\"""\""\"""\"!\"""\""\"""\""\" + | ...^ +ERROR: :3:11: token recognition error at: '\' + | \u007f0"""\""\"""\""\"""\""\"""\""\"""\"\"""\""\"""\""\"""\""\"""\"!\"""\""\"""\""\" | ..........^ +ERROR: :3:18: token recognition error at: '\' + | \u007f0"""\""\"""\""\"""\""\"""\""\"""\"\"""\""\"""\""\"""\""\"""\"!\"""\""\"""\""\" + | .................^ +ERROR: :3:25: token recognition error at: '\' + | \u007f0"""\""\"""\""\"""\""\"""\""\"""\"\"""\""\"""\""\"""\""\"""\"!\"""\""\"""\""\" + | ........................^ +ERROR: :3:32: token recognition error at: '\' + | \u007f0"""\""\"""\""\"""\""\"""\""\"""\"\"""\""\"""\""\"""\""\"""\"!\"""\""\"""\""\" + | ...............................^ +ERROR: :3:45: token recognition error at: '\' + | \u007f0"""\""\"""\""\"""\""\"""\""\"""\"\"""\""\"""\""\"""\""\"""\"!\"""\""\"""\""\" + | ............................................^ +ERROR: :3:52: token recognition error at: '\' + | \u007f0"""\""\"""\""\"""\""\"""\""\"""\"\"""\""\"""\""\"""\""\"""\"!\"""\""\"""\""\" + | ...................................................^ +ERROR: :3:59: token recognition error at: '\' + | \u007f0"""\""\"""\""\"""\""\"""\""\"""\"\"""\""\"""\""\"""\""\"""\"!\"""\""\"""\""\" + | ..........................................................^ +ERROR: :3:73: token recognition error at: '\' + | \u007f0"""\""\"""\""\"""\""\"""\""\"""\"\"""\""\"""\""\"""\""\"""\"!\"""\""\"""\""\" + | ........................................................................^ +ERROR: :3:80: token recognition error at: '\' + | \u007f0"""\""\"""\""\"""\""\"""\""\"""\"\"""\""\"""\""\"""\""\"""\"!\"""\""\"""\""\" + | ...............................................................................^ +ERROR: :3:81: token recognition error at: '"' + | \u007f0"""\""\"""\""\"""\""\"""\""\"""\"\"""\""\"""\""\"""\""\"""\"!\"""\""\"""\""\" + | ................................................................................^ +E/P: ERROR: :1:1: Syntax error: unexpected character + | ó ¢ + | ^ +ERROR: :1:2: Syntax error: unexpected character + | ó ¢ + | .^ -I: [].exists_one(__result__, x) -=====> -E: ERROR: :1:15: The iteration variable __result__ overwrites accumulator variable - | [].exists_one(__result__, x) - | ..............^ - -I: [].map(__result__, x, x) -=====> -E: ERROR: :1:8: The iteration variable __result__ overwrites accumulator variable - | [].map(__result__, x, x) - | .......^ - -I: [].filter(__result__, x) +I: '\udead' == '\ufffd' =====> -E: ERROR: :1:11: The iteration variable __result__ overwrites accumulator variable - | [].filter(__result__, x) - | ..........^ +E/A: ERROR: :1:1: Invalid unicode code point + | '\udead' == '\ufffd' + | ^ +E/P: ERROR: :1:1: Invalid unicode code point + | '\udead' == '\ufffd' + | ^ -I: [].all(.x, x) +I: a | b =====> -E: ERROR: :1:9: The argument must be a simple name - | [].all(.x, x) - | ........^ +E/A: ERROR: :1:3: token recognition error at: '| ' + | a | b + | ..^ +ERROR: :1:5: extraneous input 'b' expecting + | a | b + | ....^ +E/P: ERROR: :1:3: Syntax error: unexpected single '|', expected '||' + | a | b + | ..^ -I: [].exists(.x, x) +I: '3# < 10" '& tru ^^ =====> -E: ERROR: :1:12: The argument must be a simple name - | [].exists(.x, x) +E/A: ERROR: :1:12: token recognition error at: '& ' + | '3# < 10" '& tru ^^ + | ...........^ +ERROR: :1:14: extraneous input 'tru' expecting + | '3# < 10" '& tru ^^ + | .............^ +ERROR: :1:18: token recognition error at: '^' + | '3# < 10" '& tru ^^ + | .................^ +ERROR: :1:19: token recognition error at: '^' + | '3# < 10" '& tru ^^ + | ..................^ +E/P: ERROR: :1:12: Syntax error: unexpected single '&', expected '&&' + | '3# < 10" '& tru ^^ | ...........^ -I: [].exists_one(.x, x) +I: '?' =====> -E: ERROR: :1:16: The argument must be a simple name - | [].exists_one(.x, x) - | ...............^ +E/A: ERROR: :1:1: Invalid unicode code point + | '?' + | ^ +E/P: ERROR: :1:1: Invalid unicode code point + | '?' + | ^ -I: [].map(.x, x, x) +I: '?' =====> -E: ERROR: :1:9: The argument must be a simple name - | [].map(.x, x, x) - | ........^ +E/A: ERROR: :1:1: Invalid unicode code point + | '?' + | ^ +E/P: ERROR: :1:1: Invalid unicode code point + | '?' + | ^ -I: [].filter(.x, x) +I: r"\?" =====> -E: ERROR: :1:12: The argument must be a simple name - | [].filter(.x, x) - | ...........^ +E/A: ERROR: :1:1: Invalid unicode code point + | r"\?" + | ^ +E/P: ERROR: :1:1: Invalid unicode code point + | r"\?" + | ^ I: 1 + + =====> -E: ERROR: :1:5: mismatched input '+' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} +E/A: ERROR: :1:5: mismatched input '+' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} | 1 + + | ....^ ERROR: :1:6: mismatched input '' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} | 1 + + | .....^ +E/P: ERROR: :1:5: Syntax error: unexpected token + | 1 + + + | ....^ -I: "\xFh" +I: ? =====> -E: ERROR: :1:1: token recognition error at: '"\xFh' - | "\xFh" +E/A: ERROR: :1:1: mismatched input '?' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | ? + | ^ +ERROR: :1:2: mismatched input '' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | ? + | .^ +E/P: ERROR: :1:1: Syntax error: unexpected token + | ? | ^ -ERROR: :1:6: token recognition error at: '"' - | "\xFh" - | .....^ -ERROR: :1:7: mismatched input '' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} - | "\xFh" - | ......^ -I: "\a\b\f\n\r\t\v\'\"\\\? Illegal escape \>" +I: a ? b ((?)) =====> -E: ERROR: :1:1: token recognition error at: '"\a\b\f\n\r\t\v\'\"\\\? Illegal escape \>' - | "\a\b\f\n\r\t\v\'\"\\\? Illegal escape \>" - | ^ -ERROR: :1:42: token recognition error at: '"' - | "\a\b\f\n\r\t\v\'\"\\\? Illegal escape \>" - | .........................................^ -ERROR: :1:43: mismatched input '' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} - | "\a\b\f\n\r\t\v\'\"\\\? Illegal escape \>" - | ..........................................^ +E/A: ERROR: :1:9: mismatched input '?' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | a ? b ((?)) + | ........^ +ERROR: :1:10: mismatched input ')' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | a ? b ((?)) + | .........^ +ERROR: :1:12: mismatched input '' expecting {'==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', ')', '.', ',', '-', '?', '+', '*', '/', '%%'} + | a ? b ((?)) + | ...........^ +E/P: ERROR: :1:9: Syntax error: unexpected token + | a ? b ((?)) + | ........^ +ERROR: :1:12: Syntax error: expected ':' in conditional expression + | a ? b ((?)) + | ...........^ -I: '?' +I: a ? b @ =====> -E: ERROR: :1:1: Invalid unicode code point - | '?' - | ^ +E/A: ERROR: :1:7: token recognition error at: '@' + | a ? b @ + | ......^ +ERROR: :1:8: mismatched input '' expecting {'==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', '.', '-', ':', '+', '*', '/', '%%'} + | a ? b @ + | .......^ +E/P: ERROR: :1:7: Syntax error: unexpected character + | a ? b @ + | ......^ -I: '?' +I: -[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1 +»»--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1--1--0--1--1--1 +»»--3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1 +»»--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1 +»»--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1 +»»--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1 +»»--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1--1--0--1--1--1 +»»--3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1 +»»--3-[-1--1--1--1---1-1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 +»»--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 +»»--1--1---1--1-À1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 +»»--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 +»»--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 +»»--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 +»»--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 +»»--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 +»»--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 +»»--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1 +»»--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1 +»»--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1 +»»--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1 +»»--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1 +»»--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1 +»»--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1 +»»--1--0--1--1--1--3-[-1--1--1--1---1--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1 +»»--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1 +»»--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1 +»»--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1 +»»--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1 +»»--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1 +»»--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1 +»»--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1 +»»--1---1--1--1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1--1 +»»--1---1--1--1--0--1--1--1--1--0--3--1--1--0--1 =====> -E: ERROR: :1:1: Invalid unicode code point - | '?' - | ^ +E/A: More than 30 parse errors. +E/P: ERROR: :3:33: Syntax error: unexpected token + | --3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1 + | ................................^ +ERROR: :3:34: Syntax error: expected ']' + | --3-[-1--1--1--1---1--1--1--0-/1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1 + | .................................^ +ERROR: :11:17: Syntax error: unexpected character + | --1--1---1--1-À1--0--1--1--1--1--0--2--1--1--0--1--1--1--1--0--1--1--1--3-[-1--1 + | ................^ +ERROR: :34:49: Syntax error: expected ']' + | --1---1--1--1--0--1--1--1--1--0--3--1--1--0--1 + | ................................................^ +ERROR: :34:49: Syntax error: expected ']' + | --1---1--1--1--0--1--1--1--1--0--3--1--1--0--1 + | ................................................^ -I: r"\?" +I: as break const continue else for function if import in let loop package namespace return var void while =====> -E: ERROR: :1:1: Invalid unicode code point - | r"\?" +E/A: ERROR: :1:1: reserved identifier: as + | as break const continue else for function if import in let loop package namespace return var void while + | ^ +ERROR: :1:4: mismatched input 'break' expecting {, '==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', '.', '-', '?', '+', '*', '/', '%%'} + | as break const continue else for function if import in let loop package namespace return var void while + | ...^ +E/P: ERROR: :1:1: reserved identifier: as + | as break const continue else for function if import in let loop package namespace return var void while | ^ +ERROR: :1:4: reserved identifier: break + | as break const continue else for function if import in let loop package namespace return var void while + | ...^ +ERROR: :1:10: reserved identifier: const + | as break const continue else for function if import in let loop package namespace return var void while + | .........^ +ERROR: :1:16: reserved identifier: continue + | as break const continue else for function if import in let loop package namespace return var void while + | ...............^ +ERROR: :1:25: reserved identifier: else + | as break const continue else for function if import in let loop package namespace return var void while + | ........................^ +ERROR: :1:30: reserved identifier: for + | as break const continue else for function if import in let loop package namespace return var void while + | .............................^ +ERROR: :1:34: reserved identifier: function + | as break const continue else for function if import in let loop package namespace return var void while + | .................................^ +ERROR: :1:43: reserved identifier: if + | as break const continue else for function if import in let loop package namespace return var void while + | ..........................................^ +ERROR: :1:46: reserved identifier: import + | as break const continue else for function if import in let loop package namespace return var void while + | .............................................^ +ERROR: :1:53: reserved identifier: in + | as break const continue else for function if import in let loop package namespace return var void while + | ....................................................^ +ERROR: :1:56: reserved identifier: let + | as break const continue else for function if import in let loop package namespace return var void while + | .......................................................^ +ERROR: :1:60: reserved identifier: loop + | as break const continue else for function if import in let loop package namespace return var void while + | ...........................................................^ +ERROR: :1:65: reserved identifier: package + | as break const continue else for function if import in let loop package namespace return var void while + | ................................................................^ +ERROR: :1:73: reserved identifier: namespace + | as break const continue else for function if import in let loop package namespace return var void while + | ........................................................................^ +ERROR: :1:83: reserved identifier: return + | as break const continue else for function if import in let loop package namespace return var void while + | ..................................................................................^ +ERROR: :1:90: reserved identifier: var + | as break const continue else for function if import in let loop package namespace return var void while + | .........................................................................................^ +ERROR: :1:94: reserved identifier: void + | as break const continue else for function if import in let loop package namespace return var void while + | .............................................................................................^ +ERROR: :1:99: reserved identifier: while + | as break const continue else for function if import in let loop package namespace return var void while + | ..................................................................................................^ I: as =====> -E: ERROR: :1:1: reserved identifier: as +E/A: ERROR: :1:1: reserved identifier: as + | as + | ^ +E/P: ERROR: :1:1: reserved identifier: as | as | ^ I: break =====> -E: ERROR: :1:1: reserved identifier: break +E/A: ERROR: :1:1: reserved identifier: break + | break + | ^ +E/P: ERROR: :1:1: reserved identifier: break | break | ^ I: const =====> -E: ERROR: :1:1: reserved identifier: const +E/A: ERROR: :1:1: reserved identifier: const + | const + | ^ +E/P: ERROR: :1:1: reserved identifier: const | const | ^ I: continue =====> -E: ERROR: :1:1: reserved identifier: continue +E/A: ERROR: :1:1: reserved identifier: continue + | continue + | ^ +E/P: ERROR: :1:1: reserved identifier: continue | continue | ^ I: else =====> -E: ERROR: :1:1: reserved identifier: else +E/A: ERROR: :1:1: reserved identifier: else + | else + | ^ +E/P: ERROR: :1:1: reserved identifier: else | else | ^ I: for =====> -E: ERROR: :1:1: reserved identifier: for +E/A: ERROR: :1:1: reserved identifier: for + | for + | ^ +E/P: ERROR: :1:1: reserved identifier: for | for | ^ I: function =====> -E: ERROR: :1:1: reserved identifier: function +E/A: ERROR: :1:1: reserved identifier: function + | function + | ^ +E/P: ERROR: :1:1: reserved identifier: function | function | ^ I: if =====> -E: ERROR: :1:1: reserved identifier: if +E/A: ERROR: :1:1: reserved identifier: if + | if + | ^ +E/P: ERROR: :1:1: reserved identifier: if | if | ^ I: import =====> -E: ERROR: :1:1: reserved identifier: import +E/A: ERROR: :1:1: reserved identifier: import + | import + | ^ +E/P: ERROR: :1:1: reserved identifier: import | import | ^ I: in =====> -E: ERROR: :1:1: mismatched input 'in' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} +E/A: ERROR: :1:1: mismatched input 'in' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} | in | ^ ERROR: :1:3: mismatched input '' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} | in | ..^ +E/P: ERROR: :1:1: Syntax error: unexpected token + | in + | ^ I: let =====> -E: ERROR: :1:1: reserved identifier: let +E/A: ERROR: :1:1: reserved identifier: let + | let + | ^ +E/P: ERROR: :1:1: reserved identifier: let | let | ^ I: loop =====> -E: ERROR: :1:1: reserved identifier: loop +E/A: ERROR: :1:1: reserved identifier: loop + | loop + | ^ +E/P: ERROR: :1:1: reserved identifier: loop | loop | ^ I: package =====> -E: ERROR: :1:1: reserved identifier: package +E/A: ERROR: :1:1: reserved identifier: package + | package + | ^ +E/P: ERROR: :1:1: reserved identifier: package | package | ^ I: namespace =====> -E: ERROR: :1:1: reserved identifier: namespace +E/A: ERROR: :1:1: reserved identifier: namespace + | namespace + | ^ +E/P: ERROR: :1:1: reserved identifier: namespace | namespace | ^ I: return =====> -E: ERROR: :1:1: reserved identifier: return +E/A: ERROR: :1:1: reserved identifier: return + | return + | ^ +E/P: ERROR: :1:1: reserved identifier: return | return | ^ I: var =====> -E: ERROR: :1:1: reserved identifier: var +E/A: ERROR: :1:1: reserved identifier: var + | var + | ^ +E/P: ERROR: :1:1: reserved identifier: var | var | ^ I: void =====> -E: ERROR: :1:1: reserved identifier: void +E/A: ERROR: :1:1: reserved identifier: void + | void + | ^ +E/P: ERROR: :1:1: reserved identifier: void | void | ^ I: while =====> -E: ERROR: :1:1: reserved identifier: while +E/A: ERROR: :1:1: reserved identifier: while + | while + | ^ +E/P: ERROR: :1:1: reserved identifier: while | while | ^ I: [1, 2, 3].map(var, var * var) =====> -E: ERROR: :1:15: reserved identifier: var +E/A: ERROR: :1:15: reserved identifier: var | [1, 2, 3].map(var, var * var) | ..............^ ERROR: :1:15: The argument must be a simple name @@ -288,11 +522,20 @@ ERROR: :1:20: reserved identifier: var ERROR: :1:26: reserved identifier: var | [1, 2, 3].map(var, var * var) | .........................^ +E/P: ERROR: :1:15: reserved identifier: var + | [1, 2, 3].map(var, var * var) + | ..............^ +ERROR: :1:20: reserved identifier: var + | [1, 2, 3].map(var, var * var) + | ...................^ +ERROR: :1:26: reserved identifier: var + | [1, 2, 3].map(var, var * var) + | .........................^ I: '😁' in ['😁', '😑', '😦'] && in.😁 =====> -E: ERROR: :2:7: extraneous input 'in' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} +E/A: ERROR: :2:7: extraneous input 'in' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} | && in.😁 | ......^ ERROR: :2:10: token recognition error at: '😁' @@ -301,125 +544,747 @@ ERROR: :2:10: token recognition error at: '😁' ERROR: :2:11: no viable alternative at input '.' | && in.😁 | ..........^ +E/P: ERROR: :2:7: Syntax error: unexpected token + | && in.😁 + | ......^ +ERROR: :2:10: Syntax error: unexpected character + | && in.😁 + | .........^ -I: [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['too many']]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] +I: 1 + =====> -E: Expression recursion limit exceeded. limit: 250 +E/A: ERROR: :1:4: mismatched input '' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | 1 + + | ...^ +E/P: ERROR: :1:4: Syntax error: mismatched input '' expecting expression + | 1 + + | ...^ -I: {"a": 1}."a" +I: -- =====> -E: ERROR: :1:10: no viable alternative at input '."a"' - | {"a": 1}."a" - | .........^ +E/A: ERROR: :1:3: no viable alternative at input '-' + | -- + | ..^ +ERROR: :1:3: mismatched input '' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | -- + | ..^ +E/P: ERROR: :1:3: Syntax error: mismatched input '' expecting expression + | -- + | ..^ -I: 1 + 2 -3 + +I: { =====> -E: ERROR: :2:1: mismatched input '3' expecting {, '==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', '.', '-', '?', '+', '*', '/', '%%'} - | 3 + +E/A: ERROR: :1:2: mismatched input '' expecting {'[', '{', '}', '(', '.', ',', '-', '!', '?', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | { + | .^ +E/P: ERROR: :1:2: Syntax error: expected '}' + | { + | .^ + +I: 0x +=====> +E/A: ERROR: :1:2: extraneous input 'x' expecting + | 0x + | .^ +E/P: ERROR: :1:1: Syntax error: integral literal missing digits after hexadecimal separator + | 0x | ^ +I: TestAllTypes(){} +=====> +E/A: ERROR: :1:15: mismatched input '{' expecting {, '==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', '.', '-', '?', '+', '*', '/', '%%'} + | TestAllTypes(){} + | ..............^ +E/P: ERROR: :1:15: Syntax error: unexpected token after expression + | TestAllTypes(){} + | ..............^ + +I: TestAllTypes{}() +=====> +E/A: ERROR: :1:15: mismatched input '(' expecting {, '==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', '.', '-', '?', '+', '*', '/', '%%'} + | TestAllTypes{}() + | ..............^ +E/P: ERROR: :1:15: Syntax error: unexpected token after expression + | TestAllTypes{}() + | ..............^ + I: TestAllTypes(){single_int32: 1, single_int64: 2} =====> -E: ERROR: :1:15: mismatched input '{' expecting {, '==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', '.', '-', '?', '+', '*', '/', '%%'} +E/A: ERROR: :1:15: mismatched input '{' expecting {, '==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', '.', '-', '?', '+', '*', '/', '%%'} + | TestAllTypes(){single_int32: 1, single_int64: 2} + | ..............^ +E/P: ERROR: :1:15: Syntax error: unexpected token after expression | TestAllTypes(){single_int32: 1, single_int64: 2} | ..............^ -I: { +I: 1 + 2 +3 + =====> -E: ERROR: :1:2: mismatched input '' expecting {'[', '{', '}', '(', '.', ',', '-', '!', '?', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} - | { +E/A: ERROR: :2:1: mismatched input '3' expecting {, '==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', '.', '-', '?', '+', '*', '/', '%%'} + | 3 + + | ^ +E/P: ERROR: :2:1: Syntax error: unexpected token after expression + | 3 + + | ^ + +I: {"a": 1}."a" +=====> +E/A: ERROR: :1:10: no viable alternative at input '."a"' + | {"a": 1}."a" + | .........^ +E/P: ERROR: :1:10: Syntax error: expected identifier after '.' + | {"a": 1}."a" + | .........^ + +I: self.true == 1 +=====> +E/A: ERROR: :1:6: no viable alternative at input '.true' + | self.true == 1 + | .....^ +E/P: ERROR: :1:6: Syntax error: expected identifier after '.' + | self.true == 1 + | .....^ + +I: {a} +=====> +E/A: ERROR: :1:3: mismatched input '}' expecting {'==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', '.', '-', '?', ':', '+', '*', '/', '%%'} + | {a} + | ..^ +E/P: ERROR: :1:3: Syntax error: expected ':' in map entry + | {a} + | ..^ + +I: {:a} +=====> +E/A: ERROR: :1:2: extraneous input ':' expecting {'[', '{', '}', '(', '.', ',', '-', '!', '?', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | {:a} | .^ +ERROR: :1:4: mismatched input '}' expecting {'==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', '.', '-', '?', ':', '+', '*', '/', '%%'} + | {:a} + | ...^ +E/P: ERROR: :1:2: Syntax error: unexpected token + | {:a} + | .^ +ERROR: :1:3: Syntax error: expected ':' in map entry + | {:a} + | ..^ + +I: func{{a}} +=====> +E/A: ERROR: :1:6: extraneous input '{' expecting {'}', ',', '?', IDENTIFIER, ESC_IDENTIFIER} + | func{{a}} + | .....^ +ERROR: :1:8: mismatched input '}' expecting ':' + | func{{a}} + | .......^ +ERROR: :1:9: extraneous input '}' expecting + | func{{a}} + | ........^ +E/P: ERROR: :1:6: Syntax error: expected struct field name + | func{{a}} + | .....^ +ERROR: :1:9: Syntax error: unexpected token after expression + | func{{a}} + | ........^ + +I: msg{:a} +=====> +E/A: ERROR: :1:5: extraneous input ':' expecting {'}', ',', '?', IDENTIFIER, ESC_IDENTIFIER} + | msg{:a} + | ....^ +ERROR: :1:7: mismatched input '}' expecting ':' + | msg{:a} + | ......^ +E/P: ERROR: :1:5: Syntax error: expected struct field name + | msg{:a} + | ....^ + +I: ind[a{b}] +=====> +E/A: ERROR: :1:8: mismatched input '}' expecting ':' + | ind[a{b}] + | .......^ +E/P: ERROR: :1:8: Syntax error: expected ':' in struct field + | ind[a{b}] + | .......^ + +I: x{?. +=====> +E/A: ERROR: :1:4: mismatched input '.' expecting {IDENTIFIER, ESC_IDENTIFIER} + | x{?. + | ...^ +ERROR: :1:4: unsupported identifier + | x{?. + | ...^ +E/P: ERROR: :1:4: Syntax error: expected struct field name + | x{?. + | ...^ +ERROR: :1:5: Syntax error: expected '}' + | x{?. + | ....^ + +I: x{. +=====> +E/A: ERROR: :1:3: mismatched input '.' expecting {'}', ',', '?', IDENTIFIER, ESC_IDENTIFIER} + | x{. + | ..^ +E/P: ERROR: :1:3: Syntax error: expected struct field name + | x{. + | ..^ +ERROR: :1:4: Syntax error: expected '}' + | x{. + | ...^ I: t{>C} =====> -E: ERROR: :1:3: extraneous input '>' expecting {'}', ',', '?', IDENTIFIER, ESC_IDENTIFIER} +E/A: ERROR: :1:3: extraneous input '>' expecting {'}', ',', '?', IDENTIFIER, ESC_IDENTIFIER} | t{>C} | ..^ ERROR: :1:5: mismatched input '}' expecting ':' | t{>C} | ....^ +E/P: ERROR: :1:3: Syntax error: expected struct field name + | t{>C} + | ..^ I: has([(has(( =====> -E: ERROR: :1:4: invalid argument to has() macro +E/A: ERROR: :1:4: invalid argument to has() macro | has([(has(( | ...^ ERROR: :1:12: mismatched input '' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} | has([(has(( | ...........^ +E/P: ERROR: :1:4: invalid argument to has() macro + | has([(has(( + | ...^ +ERROR: :1:10: invalid argument to has() macro + | has([(has(( + | .........^ +ERROR: :1:12: Syntax error: mismatched input '' expecting expression + | has([(has(( + | ...........^ +ERROR: :1:12: Syntax error: mismatched input expecting ')' + | has([(has(( + | ...........^ +ERROR: :1:12: Syntax error: mismatched input expecting ')' + | has([(has(( + | ...........^ +ERROR: :1:12: Syntax error: mismatched input expecting ')' + | has([(has(( + | ...........^ +ERROR: :1:12: Syntax error: expected ']' + | has([(has(( + | ...........^ +ERROR: :1:12: Syntax error: mismatched input expecting ')' + | has([(has(( + | ...........^ + +I: 1.all(2, 3) +=====> +E/A: ERROR: :1:7: The argument must be a simple name + | 1.all(2, 3) + | ......^ +E/P: ERROR: :1:7: The argument must be a simple name + | 1.all(2, 3) + | ......^ + +I: 1.exists(2, 3) +=====> +E/A: ERROR: :1:10: The argument must be a simple name + | 1.exists(2, 3) + | .........^ +E/P: ERROR: :1:10: The argument must be a simple name + | 1.exists(2, 3) + | .........^ + +I: [].all(__result__, x) +=====> +E/A: ERROR: :1:8: The iteration variable __result__ overwrites accumulator variable + | [].all(__result__, x) + | .......^ +E/P: ERROR: :1:8: The iteration variable __result__ overwrites accumulator variable + | [].all(__result__, x) + | .......^ + +I: [].exists(__result__, x) +=====> +E/A: ERROR: :1:11: The iteration variable __result__ overwrites accumulator variable + | [].exists(__result__, x) + | ..........^ +E/P: ERROR: :1:11: The iteration variable __result__ overwrites accumulator variable + | [].exists(__result__, x) + | ..........^ + +I: [].exists_one(__result__, x) +=====> +E/A: ERROR: :1:15: The iteration variable __result__ overwrites accumulator variable + | [].exists_one(__result__, x) + | ..............^ +E/P: ERROR: :1:15: The iteration variable __result__ overwrites accumulator variable + | [].exists_one(__result__, x) + | ..............^ + +I: [].map(__result__, x, x) +=====> +E/A: ERROR: :1:8: The iteration variable __result__ overwrites accumulator variable + | [].map(__result__, x, x) + | .......^ +E/P: ERROR: :1:8: The iteration variable __result__ overwrites accumulator variable + | [].map(__result__, x, x) + | .......^ + +I: [].filter(__result__, x) +=====> +E/A: ERROR: :1:11: The iteration variable __result__ overwrites accumulator variable + | [].filter(__result__, x) + | ..........^ +E/P: ERROR: :1:11: The iteration variable __result__ overwrites accumulator variable + | [].filter(__result__, x) + | ..........^ + +I: [].all(.x, x) +=====> +E/A: ERROR: :1:9: The argument must be a simple name + | [].all(.x, x) + | ........^ +E/P: ERROR: :1:8: The argument must be a simple name + | [].all(.x, x) + | .......^ + +I: [].exists(.x, x) +=====> +E/A: ERROR: :1:12: The argument must be a simple name + | [].exists(.x, x) + | ...........^ +E/P: ERROR: :1:11: The argument must be a simple name + | [].exists(.x, x) + | ..........^ + +I: [].exists_one(.x, x) +=====> +E/A: ERROR: :1:16: The argument must be a simple name + | [].exists_one(.x, x) + | ...............^ +E/P: ERROR: :1:15: The argument must be a simple name + | [].exists_one(.x, x) + | ..............^ + +I: [].map(.x, x, x) +=====> +E/A: ERROR: :1:9: The argument must be a simple name + | [].map(.x, x, x) + | ........^ +E/P: ERROR: :1:8: The argument must be a simple name + | [].map(.x, x, x) + | .......^ + +I: [].filter(.x, x) +=====> +E/A: ERROR: :1:12: The argument must be a simple name + | [].filter(.x, x) + | ...........^ +E/P: ERROR: :1:11: The argument must be a simple name + | [].filter(.x, x) + | ..........^ I: a.?b && a[?b] =====> -E: ERROR: :1:2: unsupported syntax '.?' +E/A: ERROR: :1:2: unsupported syntax '.?' | a.?b && a[?b] | .^ ERROR: :1:10: unsupported syntax '[?' | a.?b && a[?b] | .........^ +E/P: ERROR: :1:2: unsupported syntax '.?' + | a.?b && a[?b] + | .^ +ERROR: :1:10: unsupported syntax '?' + | a.?b && a[?b] + | .........^ + +I: [?a, ?b] +=====> +E/A: ERROR: :1:2: unsupported syntax '?' + | [?a, ?b] + | .^ +ERROR: :1:6: unsupported syntax '?' + | [?a, ?b] + | .....^ +E/P: ERROR: :1:2: unsupported syntax '?' + | [?a, ?b] + | .^ +ERROR: :1:6: unsupported syntax '?' + | [?a, ?b] + | .....^ I: Msg{?field: value} && {?'key': value} =====> -E: ERROR: :1:5: unsupported syntax '?' +E/A: ERROR: :1:5: unsupported syntax '?' + | Msg{?field: value} && {?'key': value} + | ....^ +ERROR: :1:24: unsupported syntax '?' + | Msg{?field: value} && {?'key': value} + | .......................^ +E/P: ERROR: :1:5: unsupported syntax '?' | Msg{?field: value} && {?'key': value} | ....^ ERROR: :1:24: unsupported syntax '?' | Msg{?field: value} && {?'key': value} | .......................^ -I: [?a, ?b] +I: a.`b-c` =====> -E: ERROR: :1:2: unsupported syntax '?' - | [?a, ?b] - | .^ -ERROR: :1:6: unsupported syntax '?' - | [?a, ?b] +E/A: ERROR: :1:3: unsupported syntax '`' + | a.`b-c` + | ..^ +E/P: ERROR: :1:3: unsupported syntax '`' + | a.`b-c` + | ..^ + +I: a.`b.c` +=====> +E/A: ERROR: :1:3: unsupported syntax '`' + | a.`b.c` + | ..^ +E/P: ERROR: :1:3: unsupported syntax '`' + | a.`b.c` + | ..^ + +I: a.`in` +=====> +E/A: ERROR: :1:3: unsupported syntax '`' + | a.`in` + | ..^ +E/P: ERROR: :1:3: unsupported syntax '`' + | a.`in` + | ..^ + +I: a.`/foo` +=====> +E/A: ERROR: :1:3: unsupported syntax '`' + | a.`/foo` + | ..^ +E/P: ERROR: :1:3: unsupported syntax '`' + | a.`/foo` + | ..^ + +I: Message{`in`: true} +=====> +E/A: ERROR: :1:9: unsupported syntax '`' + | Message{`in`: true} + | ........^ +E/P: ERROR: :1:9: unsupported syntax '`' + | Message{`in`: true} + | ........^ + +I: foo.`bar` +=====> +E/A: ERROR: :1:5: unsupported syntax '`' + | foo.`bar` + | ....^ +E/P: ERROR: :1:5: unsupported syntax '`' + | foo.`bar` + | ....^ + +I: Struct{`bar`: false} +=====> +E/A: ERROR: :1:8: unsupported syntax '`' + | Struct{`bar`: false} + | .......^ +E/P: ERROR: :1:8: unsupported syntax '`' + | Struct{`bar`: false} + | .......^ + +I: has(.`.` +=====> +E/A: ERROR: :1:6: no viable alternative at input '.`.`' + | has(.`.` + | .....^ +ERROR: :1:6: unsupported syntax '`' + | has(.`.` + | .....^ +ERROR: :1:9: missing ')' at '' + | has(.`.` + | ........^ +E/P: ERROR: :1:4: invalid argument to has() macro + | has(.`.` + | ...^ +ERROR: :1:6: unexpected quoted identifier + | has(.`.` | .....^ +ERROR: :1:9: Syntax error: mismatched input expecting ')' + | has(.`.` + | ........^ + +I: `b-c` +=====> +E/A: ERROR: :1:1: mismatched input '`b-c`' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | `b-c` + | ^ +E/P: ERROR: :1:1: unexpected quoted identifier + | `b-c` + | ^ + +I: `b-c`() +=====> +E/A: ERROR: :1:1: extraneous input '`b-c`' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | `b-c`() + | ^ +ERROR: :1:7: mismatched input ')' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | `b-c`() + | ......^ +E/P: ERROR: :1:1: unexpected quoted identifier + | `b-c`() + | ^ + +I: a.`$b` +=====> +E/A: ERROR: :1:3: token recognition error at: '`$' + | a.`$b` + | ..^ +ERROR: :1:6: token recognition error at: '`' + | a.`$b` + | .....^ +E/P: ERROR: :1:3: unexpected quoted identifier + | a.`$b` + | ..^ + +I: a.`b.c`() +=====> +E/A: ERROR: :1:8: mismatched input '(' expecting {, '==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', '.', '-', '?', '+', '*', '/', '%%'} + | a.`b.c`() + | .......^ +E/P: ERROR: :1:3: unexpected quoted identifier + | a.`b.c`() + | ..^ I: `bar` =====> -E: ERROR: :1:1: mismatched input '`bar`' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} +E/A: ERROR: :1:1: mismatched input '`bar`' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | `bar` + | ^ +E/P: ERROR: :1:1: unexpected quoted identifier | `bar` | ^ I: foo.`` =====> -E: ERROR: :1:5: token recognition error at: '``' +E/A: ERROR: :1:5: token recognition error at: '``' | foo.`` | ....^ ERROR: :1:7: no viable alternative at input '.' | foo.`` | ......^ +E/P: ERROR: :1:5: unexpected quoted identifier + | foo.`` + | ....^ I: foo.`$bar` =====> -E: ERROR: :1:5: token recognition error at: '`$' +E/A: ERROR: :1:5: token recognition error at: '`$' | foo.`$bar` | ....^ ERROR: :1:10: token recognition error at: '`' | foo.`$bar` | .........^ +E/P: ERROR: :1:5: unexpected quoted identifier + | foo.`$bar` + | ....^ -I: foo.`bar` +I: [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['too many']]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] =====> -E: ERROR: :1:5: unsupported syntax '`' - | foo.`bar` +E/A: Expression recursion limit exceeded. limit: 250 +E/P: ERROR: :1:251: Expression recursion limit exceeded. limit: 250 + | [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['too many']]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] + | ..........................................................................................................................................................................................................................................................^ +ERROR: :1:251: Syntax error: expected ']' + | [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['too many']]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] + | ..........................................................................................................................................................................................................................................................^ + +I: [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[ +»»»[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['too many']]]]]]]]]]]]]]]]]]]]]]]]]]]] +»»»]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] +=====> +E/A: Expression recursion limit exceeded. limit: 32 +E/P: ERROR: :1:33: Expression recursion limit exceeded. limit: 32 + | [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[ + | ................................^ +ERROR: :1:33: Syntax error: expected ']' + | [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[ + | ................................^ + +I: [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['not fine']]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] +=====> +E/A: Expression recursion limit exceeded. limit: 32 +E/P: ERROR: :1:33: Expression recursion limit exceeded. limit: 32 + | [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['not fine']]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] + | ................................^ +ERROR: :1:33: Syntax error: expected ']' + | [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[['not fine']]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] + | ................................^ + +I: a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.A.B.C.D.E.F.G.H +=====> +E/A: Expression recursion limit exceeded. limit: 32 +E/P: ERROR: :1:66: Expression recursion limit exceeded. limit: 32 + | a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.A.B.C.D.E.F.G.H + | .................................................................^ + +I: a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] +»» [21][22][23][24][25][26][27][28][29][30][31][32][33] +=====> +E/A: Expression recursion limit exceeded. limit: 32 +E/P: ERROR: :2:56: Expression recursion limit exceeded. limit: 32 + | [21][22][23][24][25][26][27][28][29][30][31][32][33] + | .......................................................^ + +I: 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 +»»+ 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 +»»+ 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 30 +»»+ 31 + 32 + 33 + 34 +=====> +E/A: Expression recursion limit exceeded. limit: 32 +E/P: ERROR: :4:18: Expression recursion limit exceeded. limit: 32 + | + 31 + 32 + 33 + 34 + | .................^ + +I: a < 1 < 2 < 3 < 4 < 5 < 6 < 7 < 8 < 9 < 10 < 11 +»» < 12 < 13 < 14 < 15 < 16 < 17 < 18 < 19 < 20 < 21 +»»» < 22 < 23 < 24 < 25 < 26 < 27 < 28 < 29 < 30 < 31 +»»» < 32 < 33 +=====> +E/A: Expression recursion limit exceeded. limit: 32 +E/P: ERROR: :4:11: Expression recursion limit exceeded. limit: 32 + | < 32 < 33 + | ..........^ + +I: y!=y!=y!=y!=y!=y!=y!=y!=y!=-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y +»»!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y +»»!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y +»»!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y +»»!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y +»»!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y +=====> +E/A: Expression recursion limit exceeded. limit: 32 +E/P: ERROR: :2:63: Expression recursion limit exceeded. limit: 32 + | !=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y + | ..............................................................^ + +I: a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != +»»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] +=====> +E/A: Expression recursion limit exceeded. limit: 32 +E/P: ERROR: :13:76: Expression recursion limit exceeded. limit: 32 + | a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != + | ...........................................................................^ + +I: true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : 1 +=====> +E/A: Expression recursion limit exceeded. limit: 32 +E/P: ERROR: :1:353: Expression recursion limit exceeded. limit: 32 + | true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : true ? 1 : 1 + | ................................................................................................................................................................................................................................................................................................................................................................^ + +I: !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x +=====> +E/A: ERROR: :1:3: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x + | ..^ +ERROR: :1:5: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x | ....^ +ERROR: :1:7: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x + | ......^ +ERROR: :1:9: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x + | ........^ +ERROR: :1:11: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x + | ..........^ +ERROR: :1:13: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x + | ............^ +ERROR: :1:15: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x + | ..............^ +ERROR: :1:17: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x + | ................^ +ERROR: :1:19: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x + | ..................^ +ERROR: :1:21: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x + | ....................^ +ERROR: :1:23: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x + | ......................^ +ERROR: :1:25: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x + | ........................^ +ERROR: :1:27: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x + | ..........................^ +ERROR: :1:29: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x + | ............................^ +ERROR: :1:31: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x + | ..............................^ +ERROR: :1:33: no viable alternative at input '-!' + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x + | ................................^ +E/P: ERROR: :1:33: Expression recursion limit exceeded. limit: 32 + | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x + | ................................^ -I: Struct{`bar`: false} +I: 123456 =====> -E: ERROR: :1:8: unsupported syntax '`' - | Struct{`bar`: false} - | .......^ +E/A: ERROR: :-1:0: expression code point size exceeds limit: size: 6, limit 5 +E/P: ERROR: :-1:0: expression code point size exceeds limit: size: 6, limit 5 -I: has(.`.` +I: 1 + 2 + 3 =====> -E: ERROR: :1:6: no viable alternative at input '.`.`' - | has(.`.` - | .....^ -ERROR: :1:6: unsupported syntax '`' - | has(.`.` +E/A: ERROR: :1:5: expression node limit (2) exceeded + | 1 + 2 + 3 + | ....^ +E/P: ERROR: :1:5: expression node limit (2) exceeded + | 1 + 2 + 3 + | ....^ + +I: [?, ?, ?] +=====> +E/A: More than 2 parse errors. +E/P: ERROR: :1:3: Syntax error: unexpected token + | [?, ?, ?] + | ..^ +ERROR: :1:6: Syntax error: unexpected token + | [?, ?, ?] | .....^ -ERROR: :1:9: missing ')' at '' - | has(.`.` - | ........^ \ No newline at end of file +ERROR: :-1:0: More than 2 parse errors. + +I: [1 2 3 a b c] +=====> +E/A: ERROR: :1:4: mismatched input '2' expecting {']', ','} + | [1 2 3 a b c] + | ...^ +E/P: ERROR: :1:4: Syntax error: expected ']' + | [1 2 3 a b c] + | ...^ +ERROR: :1:13: Syntax error: unexpected token after expression + | [1 2 3 a b c] + | ............^ \ No newline at end of file diff --git a/parser/src/test/resources/parser_literals.baseline b/parser/src/test/resources/parser_literals.baseline new file mode 100644 index 000000000..f4716e927 --- /dev/null +++ b/parser/src/test/resources/parser_literals.baseline @@ -0,0 +1,649 @@ +I: null +=====> +P: null^#1:NullValue# +L: null^#1[1,0]# + +I: true +=====> +P: true^#1:bool# +L: true^#1[1,0]# + +I: false +=====> +P: false^#1:bool# +L: false^#1[1,0]# + +I: 0 +=====> +P: 0^#1:int64# +L: 0^#1[1,0]# + +I: 42 +=====> +P: 42^#1:int64# +L: 42^#1[1,0]# + +I: 0xF +=====> +P: 15^#1:int64# +L: 15^#1[1,0]# + +I: 0x2A +=====> +P: 42^#1:int64# +L: 42^#1[1,0]# + +I: -1 +=====> +P: -1^#1:int64# +L: -1^#1[1,1]# + +I: -42 +=====> +P: -42^#1:int64# +L: -42^#1[1,1]# + +I: 0xFFFFFFFFFFFFFFFFF +=====> +E/A: ERROR: :1:1: invalid int literal: 0xFFFFFFFFFFFFFFFFF + | 0xFFFFFFFFFFFFFFFFF + | ^ +E/P: ERROR: :1:1: Syntax error: invalid int literal: 0xFFFFFFFFFFFFFFFFF + | 0xFFFFFFFFFFFFFFFFF + | ^ + +I: 9223372036854775807 +=====> +P: 9223372036854775807^#1:int64# +L: 9223372036854775807^#1[1,0]# + +I: -9223372036854775808 +=====> +P: -9223372036854775808^#1:int64# +L: -9223372036854775808^#1[1,1]# + +I: -(9223372036854775808) +=====> +E/A: ERROR: :1:3: invalid int literal: 9223372036854775808 + | -(9223372036854775808) + | ..^ +E/P: ERROR: :1:3: Syntax error: invalid int literal: 9223372036854775808 + | -(9223372036854775808) + | ..^ + +I: 123a +=====> +E/A: ERROR: :1:4: extraneous input 'a' expecting + | 123a + | ...^ +E/P: ERROR: :1:1: Syntax error: int literal has unexpected trailing characters + | 123a + | ^ + +I: 0u +=====> +P: 0u^#1:uint64# +L: 0u^#1[1,0]# + +I: 23u +=====> +P: 23u^#1:uint64# +L: 23u^#1[1,0]# + +I: 24u +=====> +P: 24u^#1:uint64# +L: 24u^#1[1,0]# + +I: 0xAu +=====> +P: 10u^#1:uint64# +L: 10u^#1[1,0]# + +I: -0xA +=====> +P: -10^#1:int64# +L: -10^#1[1,1]# + +I: 0xA +=====> +P: 10^#1:int64# +L: 10^#1[1,0]# + +I: 0xFu +=====> +P: 15u^#1:uint64# +L: 15u^#1[1,0]# + +I: 0xFFFFFFFFFFFFFFFFFu +=====> +E/A: ERROR: :1:1: invalid uint literal: 0xFFFFFFFFFFFFFFFFFu + | 0xFFFFFFFFFFFFFFFFFu + | ^ +E/P: ERROR: :1:1: Syntax error: invalid uint literal: 0xFFFFFFFFFFFFFFFFFu + | 0xFFFFFFFFFFFFFFFFFu + | ^ + +I: 123u_ +=====> +E/A: ERROR: :1:5: extraneous input '_' expecting + | 123u_ + | ....^ +E/P: ERROR: :1:1: Syntax error: uint literal has unexpected trailing characters + | 123u_ + | ^ + +I: 3.14 +=====> +P: 3.14^#1:double# +L: 3.14^#1[1,0]# + +I: 23.39 +=====> +P: 23.39^#1:double# +L: 23.39^#1[1,0]# + +I: 1. +=====> +E/A: ERROR: :1:3: no viable alternative at input '.' + | 1. + | ..^ +E/P: ERROR: :1:3: Syntax error: expected identifier after '.' + | 1. + | ..^ + +I: 1e+5 +=====> +P: 100000.0^#1:double# +L: 100000.0^#1[1,0]# + +I: 1e-5 +=====> +P: 0.00001^#1:double# +L: 0.00001^#1[1,0]# + +I: 2.5e+10 +=====> +P: 25000000000.0^#1:double# +L: 25000000000.0^#1[1,0]# + +I: 2.5e-10 +=====> +P: 0.0^#1:double# +L: 0.0^#1[1,0]# + +I: 1.99e90000009 +=====> +P: Infinity^#1:double# +L: Infinity^#1[1,0]# + +I: 1e +=====> +E/A: ERROR: :1:2: extraneous input 'e' expecting + | 1e + | .^ +E/P: ERROR: :1:1: Syntax error: floating point literal missing digits after exponent separator + | 1e + | ^ + +I: 1e+ +=====> +E/A: ERROR: :1:2: mismatched input 'e' expecting {, '==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', '.', '-', '?', '+', '*', '/', '%%'} + | 1e+ + | .^ +E/P: ERROR: :1:1: Syntax error: floating point literal missing digits after exponent separator + | 1e+ + | ^ + +I: 1e- +=====> +E/A: ERROR: :1:2: mismatched input 'e' expecting {, '==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', '.', '-', '?', '+', '*', '/', '%%'} + | 1e- + | .^ +E/P: ERROR: :1:1: Syntax error: floating point literal missing digits after exponent separator + | 1e- + | ^ + +I: 2.5e +=====> +E/A: ERROR: :1:4: extraneous input 'e' expecting + | 2.5e + | ...^ +E/P: ERROR: :1:1: Syntax error: floating point literal missing digits after exponent separator + | 2.5e + | ^ + +I: 2.5e+ +=====> +E/A: ERROR: :1:4: mismatched input 'e' expecting {, '==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', '.', '-', '?', '+', '*', '/', '%%'} + | 2.5e+ + | ...^ +E/P: ERROR: :1:1: Syntax error: floating point literal missing digits after exponent separator + | 2.5e+ + | ^ + +I: 2.5e- +=====> +E/A: ERROR: :1:4: mismatched input 'e' expecting {, '==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', '.', '-', '?', '+', '*', '/', '%%'} + | 2.5e- + | ...^ +E/P: ERROR: :1:1: Syntax error: floating point literal missing digits after exponent separator + | 2.5e- + | ^ + +I: ((1e)) +=====> +E/A: ERROR: :1:4: extraneous input 'e' expecting ')' + | ((1e)) + | ...^ +E/P: ERROR: :1:3: Syntax error: floating point literal missing digits after exponent separator + | ((1e)) + | ..^ + +I: 0x123z +=====> +E/A: ERROR: :1:6: extraneous input 'z' expecting + | 0x123z + | .....^ +E/P: ERROR: :1:1: Syntax error: int literal has unexpected trailing characters + | 0x123z + | ^ + +I: 'hello' +=====> +P: "hello"^#1:string# +L: "hello"^#1[1,0]# + +I: "A" +=====> +P: "A"^#1:string# +L: "A"^#1[1,0]# + +I: '''hello +world''' +=====> +P: "hello\nworld"^#1:string# +L: "hello\nworld"^#1[1,0]# + +I: "\u2764" +=====> +P: "❤"^#1:string# +L: "❤"^#1[1,0]# + +I: "❤" +=====> +P: "❤"^#1:string# +L: "❤"^#1[1,0]# + +I: "\"" +=====> +P: "\""^#1:string# +L: "\""^#1[1,0]# + +I: "\xC3\XBF" +=====> +P: "ÿ"^#1:string# +L: "ÿ"^#1[1,0]# + +I: "\303\277" +=====> +P: "ÿ"^#1:string# +L: "ÿ"^#1[1,0]# + +I: "hi\u263A \u263Athere" +=====> +P: "hi☺ ☺there"^#1:string# +L: "hi☺ ☺there"^#1[1,0]# + +I: "\U000003A8\?" +=====> +P: "Ψ?"^#1:string# +L: "Ψ?"^#1[1,0]# + +I: "\a\b\f\n\r\t\v'\"\\\? Legal escapes" +=====> +P: "\a\b\f\n\r\t\v'\"\? Legal escapes"^#1:string# +L: "\a\b\f\n\r\t\v'\"\? Legal escapes"^#1[1,0]# + +I: """hello +world""" +=====> +P: "hello\nworld"^#1:string# +L: "hello\nworld"^#1[1,0]# + +I: r"""hello +world""" +=====> +P: "hello\nworld"^#1:string# +L: "hello\nworld"^#1[1,0]# + +I: """""" +=====> +P: ""^#1:string# +L: ""^#1[1,0]# + +I: '''''' +=====> +P: ""^#1:string# +L: ""^#1[1,0]# + +I: """hello\"""world""" +=====> +P: "hello\"\"\"world"^#1:string# +L: "hello\"\"\"world"^#1[1,0]# + +I: '''hello\'''world''' +=====> +P: "hello'''world"^#1:string# +L: "hello'''world"^#1[1,0]# + +I: "\xFh" +=====> +E/A: ERROR: :1:1: token recognition error at: '"\xFh' + | "\xFh" + | ^ +ERROR: :1:6: token recognition error at: '"' + | "\xFh" + | .....^ +ERROR: :1:7: mismatched input '' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | "\xFh" + | ......^ +E/P: ERROR: :1:1: Invalid hex escape sequence + | "\xFh" + | ^ + +I: "\a\b\f\n\r\t\v\'\"\\\? Illegal escape \>" +=====> +E/A: ERROR: :1:1: token recognition error at: '"\a\b\f\n\r\t\v\'\"\\\? Illegal escape \>' + | "\a\b\f\n\r\t\v\'\"\\\? Illegal escape \>" + | ^ +ERROR: :1:42: token recognition error at: '"' + | "\a\b\f\n\r\t\v\'\"\\\? Illegal escape \>" + | .........................................^ +ERROR: :1:43: mismatched input '' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | "\a\b\f\n\r\t\v\'\"\\\? Illegal escape \>" + | ..........................................^ +E/P: ERROR: :1:1: Illegal escape sequence + | "\a\b\f\n\r\t\v\'\"\\\? Illegal escape \>" + | ^ + +I: '😁' in ['😁', '😑', '😦'] +»»»&& in.😁 +=====> +E/A: ERROR: :2:7: extraneous input 'in' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | && in.😁 + | ......^ +ERROR: :2:10: token recognition error at: '😁' + | && in.😁 + | .........^ +ERROR: :2:11: no viable alternative at input '.' + | && in.😁 + | ..........^ +E/P: ERROR: :2:7: Syntax error: unexpected token + | && in.😁 + | ......^ +ERROR: :2:10: Syntax error: unexpected character + | && in.😁 + | .........^ + +I: """hello +world +=====> +E/A: ERROR: :1:3: token recognition error at: '"hello\n' + | """hello + | ..^ +ERROR: :2:1: extraneous input 'world' expecting + | world + | ^ +E/P: ERROR: :1:1: Syntax error: unterminated string literal + | """hello + | ^ + +I: '''hello +world +=====> +E/A: ERROR: :1:3: token recognition error at: ''hello\n' + | '''hello + | ..^ +ERROR: :2:1: extraneous input 'world' expecting + | world + | ^ +E/P: ERROR: :1:1: Syntax error: unterminated string literal + | '''hello + | ^ + +I: r"""hello +world +=====> +E/A: ERROR: :1:4: token recognition error at: '"hello\n' + | r"""hello + | ...^ +ERROR: :2:1: extraneous input 'world' expecting + | world + | ^ +E/P: ERROR: :1:1: Syntax error: unterminated string literal + | r"""hello + | ^ + +I: "hello +world" +=====> +E/A: ERROR: :1:1: token recognition error at: '"hello\n' + | "hello + | ^ +ERROR: :2:6: token recognition error at: '"' + | world" + | .....^ +E/P: ERROR: :1:1: Syntax error: unterminated string literal + | "hello + | ^ +ERROR: :2:1: Syntax error: unexpected token after expression + | world" + | ^ + +I: 'hello +world' +=====> +E/A: ERROR: :1:1: token recognition error at: ''hello\n' + | 'hello + | ^ +ERROR: :2:6: token recognition error at: ''' + | world' + | .....^ +E/P: ERROR: :1:1: Syntax error: unterminated string literal + | 'hello + | ^ +ERROR: :2:1: Syntax error: unexpected token after expression + | world' + | ^ + +I: r"hello +world" +=====> +E/A: ERROR: :1:2: token recognition error at: '"hello\n' + | r"hello + | .^ +ERROR: :2:1: extraneous input 'world' expecting + | world" + | ^ +ERROR: :2:6: token recognition error at: '"' + | world" + | .....^ +E/P: ERROR: :1:1: Syntax error: unterminated string literal + | r"hello + | ^ +ERROR: :2:1: Syntax error: unexpected token after expression + | world" + | ^ + +I: `hello +world` +=====> +E/A: ERROR: :1:1: token recognition error at: '`hello\n' + | `hello + | ^ +ERROR: :2:6: token recognition error at: '`' + | world` + | .....^ +E/P: ERROR: :1:1: Syntax error: unterminated quoted identifier + | `hello + | ^ +ERROR: :2:1: Syntax error: unexpected token after expression + | world` + | ^ + +I: "hello world" +=====> +E/A: ERROR: :1:1: token recognition error at: '"hello\r' + | "hello world" + | ^ +ERROR: :1:13: token recognition error at: '"' + | "hello world" + | ............^ +E/P: ERROR: :1:1: Syntax error: unterminated string literal + | "hello world" + | ^ +ERROR: :1:8: Syntax error: unexpected token after expression + | "hello world" + | .......^ + +I: 'unterminated +=====> +E/A: ERROR: :1:1: token recognition error at: ''unterminated' + | 'unterminated + | ^ +ERROR: :1:14: mismatched input '' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | 'unterminated + | .............^ +E/P: ERROR: :1:1: Syntax error: unterminated string literal + | 'unterminated + | ^ + +I: b'abc' +=====> +P: b"abc"^#1:bytes# +L: b"abc"^#1[1,0]# + +I: b"abc" +=====> +P: b"abc"^#1:bytes# +L: b"abc"^#1[1,0]# + +I: b"""hello +world +=====> +E/A: ERROR: :1:4: token recognition error at: '"hello\n' + | b"""hello + | ...^ +ERROR: :2:1: extraneous input 'world' expecting + | world + | ^ +E/P: ERROR: :1:1: Syntax error: unterminated bytes literal + | b"""hello + | ^ + +I: b"hello +world" +=====> +E/A: ERROR: :1:2: token recognition error at: '"hello\n' + | b"hello + | .^ +ERROR: :2:1: extraneous input 'world' expecting + | world" + | ^ +ERROR: :2:6: token recognition error at: '"' + | world" + | .....^ +E/P: ERROR: :1:1: Syntax error: unterminated bytes literal + | b"hello + | ^ +ERROR: :2:1: Syntax error: unexpected token after expression + | world" + | ^ + +I: rb"hello +world" +=====> +E/A: ERROR: :1:3: token recognition error at: '"hello\n' + | rb"hello + | ..^ +ERROR: :2:1: extraneous input 'world' expecting + | world" + | ^ +ERROR: :2:6: token recognition error at: '"' + | world" + | .....^ +E/P: ERROR: :1:1: Syntax error: unterminated bytes literal + | rb"hello + | ^ +ERROR: :2:1: Syntax error: unexpected token after expression + | world" + | ^ + +I: br'abc' +=====> +P: b"abc"^#1:bytes# +L: b"abc"^#1[1,0]# + +I: bR'abc' +=====> +P: b"abc"^#1:bytes# +L: b"abc"^#1[1,0]# + +I: Br'abc' +=====> +P: b"abc"^#1:bytes# +L: b"abc"^#1[1,0]# + +I: BR'abc' +=====> +P: b"abc"^#1:bytes# +L: b"abc"^#1[1,0]# + +I: rb'abc' +=====> +E/A: ERROR: :1:3: extraneous input ''abc'' expecting + | rb'abc' + | ..^ + +I: rB'abc' +=====> +E/A: ERROR: :1:3: extraneous input ''abc'' expecting + | rB'abc' + | ..^ + +I: Rb'abc' +=====> +E/A: ERROR: :1:3: extraneous input ''abc'' expecting + | Rb'abc' + | ..^ + +I: RB'abc' +=====> +E/A: ERROR: :1:3: extraneous input ''abc'' expecting + | RB'abc' + | ..^ + +I: br'a\'b' +=====> +E/A: ERROR: :1:1: String literal contains unescaped terminating quote ' + | br'a\'b' + | ^ +ERROR: :1:7: extraneous input 'b' expecting + | br'a\'b' + | ......^ +ERROR: :1:8: token recognition error at: ''' + | br'a\'b' + | .......^ +E/P: ERROR: :1:1: String literal contains unescaped terminating quote ' + | br'a\'b' + | ^ +ERROR: :1:7: Syntax error: unterminated bytes literal + | br'a\'b' + | ......^ + +I: rb'a\'b' +=====> +E/A: ERROR: :1:3: extraneous input ''a\'b'' expecting + | rb'a\'b' + | ..^ \ No newline at end of file diff --git a/parser/src/test/resources/parser_macros.baseline b/parser/src/test/resources/parser_macros.baseline new file mode 100644 index 000000000..e20bd07ec --- /dev/null +++ b/parser/src/test/resources/parser_macros.baseline @@ -0,0 +1,981 @@ +I: has(m.f) +=====> +P: m^#2:Expr.Ident#.f~test-only~^#4:Expr.Select# +L: m^#2[1,4]#.f~test-only~^#4[1,3]# +M: has( + m^#2:Expr.Ident#.f^#3:Expr.Select# +)^#0:Expr.Call# + +I: has(a.b) +=====> +P: a^#2:Expr.Ident#.b~test-only~^#4:Expr.Select# +L: a^#2[1,4]#.b~test-only~^#4[1,3]# +M: has( + a^#2:Expr.Ident#.b^#3:Expr.Select# +)^#0:Expr.Call# + +I: has(m) +=====> +E/A: ERROR: :1:4: invalid argument to has() macro + | has(m) + | ...^ +E/P: ERROR: :1:4: invalid argument to has() macro + | has(m) + | ...^ + +I: m.all(v, f) +=====> +P: __comprehension__( + // Variable + v, + // Target + m^#1:Expr.Ident#, + // Accumulator + @result, + // Init + true^#5:bool#, + // LoopCondition + @not_strictly_false( + @result^#6:Expr.Ident# + )^#7:Expr.Call#, + // LoopStep + _&&_( + @result^#8:Expr.Ident#, + f^#4:Expr.Ident# + )^#9:Expr.Call#, + // Result + @result^#10:Expr.Ident#)^#11:Expr.Comprehension# +L: __comprehension__( + // Variable + v, + // Target + m^#1[1,0]#, + // Accumulator + @result, + // Init + true^#5[1,5]#, + // LoopCondition + @not_strictly_false( + @result^#6[1,5]# + )^#7[1,5]#, + // LoopStep + _&&_( + @result^#8[1,5]#, + f^#4[1,9]# + )^#9[1,5]#, + // Result + @result^#10[1,5]#)^#11[1,5]# +M: m^#1:Expr.Ident#.all( + v^#3:Expr.Ident#, + f^#4:Expr.Ident# +)^#0:Expr.Call# + +I: [1, 2].all(x, x > 0) +=====> +P: __comprehension__( + // Variable + x, + // Target + [ + 1^#2:int64#, + 2^#3:int64# + ]^#1:Expr.CreateList#, + // Accumulator + @result, + // Init + true^#9:bool#, + // LoopCondition + @not_strictly_false( + @result^#10:Expr.Ident# + )^#11:Expr.Call#, + // LoopStep + _&&_( + @result^#12:Expr.Ident#, + _>_( + x^#6:Expr.Ident#, + 0^#8:int64# + )^#7:Expr.Call# + )^#13:Expr.Call#, + // Result + @result^#14:Expr.Ident#)^#15:Expr.Comprehension# +L: __comprehension__( + // Variable + x, + // Target + [ + 1^#2[1,1]#, + 2^#3[1,4]# + ]^#1[1,0]#, + // Accumulator + @result, + // Init + true^#9[1,10]#, + // LoopCondition + @not_strictly_false( + @result^#10[1,10]# + )^#11[1,10]#, + // LoopStep + _&&_( + @result^#12[1,10]#, + _>_( + x^#6[1,14]#, + 0^#8[1,18]# + )^#7[1,16]# + )^#13[1,10]#, + // Result + @result^#14[1,10]#)^#15[1,10]# +M: [ + 1^#2:int64#, + 2^#3:int64# +]^#1:Expr.CreateList#.all( + x^#5:Expr.Ident#, + _>_( + x^#6:Expr.Ident#, + 0^#8:int64# + )^#7:Expr.Call# +)^#0:Expr.Call# + +I: m.exists(v, f) +=====> +P: __comprehension__( + // Variable + v, + // Target + m^#1:Expr.Ident#, + // Accumulator + @result, + // Init + false^#5:bool#, + // LoopCondition + @not_strictly_false( + !_( + @result^#6:Expr.Ident# + )^#7:Expr.Call# + )^#8:Expr.Call#, + // LoopStep + _||_( + @result^#9:Expr.Ident#, + f^#4:Expr.Ident# + )^#10:Expr.Call#, + // Result + @result^#11:Expr.Ident#)^#12:Expr.Comprehension# +L: __comprehension__( + // Variable + v, + // Target + m^#1[1,0]#, + // Accumulator + @result, + // Init + false^#5[1,8]#, + // LoopCondition + @not_strictly_false( + !_( + @result^#6[1,8]# + )^#7[1,8]# + )^#8[1,8]#, + // LoopStep + _||_( + @result^#9[1,8]#, + f^#4[1,12]# + )^#10[1,8]#, + // Result + @result^#11[1,8]#)^#12[1,8]# +M: m^#1:Expr.Ident#.exists( + v^#3:Expr.Ident#, + f^#4:Expr.Ident# +)^#0:Expr.Call# + +I: m.exists_one(v, f) +=====> +P: __comprehension__( + // Variable + v, + // Target + m^#1:Expr.Ident#, + // Accumulator + @result, + // Init + 0^#5:int64#, + // LoopCondition + true^#6:bool#, + // LoopStep + _?_:_( + f^#4:Expr.Ident#, + _+_( + @result^#7:Expr.Ident#, + 1^#8:int64# + )^#9:Expr.Call#, + @result^#10:Expr.Ident# + )^#11:Expr.Call#, + // Result + _==_( + @result^#12:Expr.Ident#, + 1^#13:int64# + )^#14:Expr.Call#)^#15:Expr.Comprehension# +L: __comprehension__( + // Variable + v, + // Target + m^#1[1,0]#, + // Accumulator + @result, + // Init + 0^#5[1,12]#, + // LoopCondition + true^#6[1,12]#, + // LoopStep + _?_:_( + f^#4[1,16]#, + _+_( + @result^#7[1,12]#, + 1^#8[1,12]# + )^#9[1,12]#, + @result^#10[1,12]# + )^#11[1,12]#, + // Result + _==_( + @result^#12[1,12]#, + 1^#13[1,12]# + )^#14[1,12]#)^#15[1,12]# +M: m^#1:Expr.Ident#.exists_one( + v^#3:Expr.Ident#, + f^#4:Expr.Ident# +)^#0:Expr.Call# + +I: m.existsOne(v, f) +=====> +P: __comprehension__( + // Variable + v, + // Target + m^#1:Expr.Ident#, + // Accumulator + @result, + // Init + 0^#5:int64#, + // LoopCondition + true^#6:bool#, + // LoopStep + _?_:_( + f^#4:Expr.Ident#, + _+_( + @result^#7:Expr.Ident#, + 1^#8:int64# + )^#9:Expr.Call#, + @result^#10:Expr.Ident# + )^#11:Expr.Call#, + // Result + _==_( + @result^#12:Expr.Ident#, + 1^#13:int64# + )^#14:Expr.Call#)^#15:Expr.Comprehension# +L: __comprehension__( + // Variable + v, + // Target + m^#1[1,0]#, + // Accumulator + @result, + // Init + 0^#5[1,11]#, + // LoopCondition + true^#6[1,11]#, + // LoopStep + _?_:_( + f^#4[1,15]#, + _+_( + @result^#7[1,11]#, + 1^#8[1,11]# + )^#9[1,11]#, + @result^#10[1,11]# + )^#11[1,11]#, + // Result + _==_( + @result^#12[1,11]#, + 1^#13[1,11]# + )^#14[1,11]#)^#15[1,11]# +M: m^#1:Expr.Ident#.existsOne( + v^#3:Expr.Ident#, + f^#4:Expr.Ident# +)^#0:Expr.Call# + +I: [].existsOne(__result__, __result__) +=====> +E/A: ERROR: :1:14: The iteration variable __result__ overwrites accumulator variable + | [].existsOne(__result__, __result__) + | .............^ +E/P: ERROR: :1:14: The iteration variable __result__ overwrites accumulator variable + | [].existsOne(__result__, __result__) + | .............^ + +I: m.map(v, f) +=====> +P: __comprehension__( + // Variable + v, + // Target + m^#1:Expr.Ident#, + // Accumulator + @result, + // Init + []^#5:Expr.CreateList#, + // LoopCondition + true^#6:bool#, + // LoopStep + _+_( + @result^#7:Expr.Ident#, + [ + f^#4:Expr.Ident# + ]^#8:Expr.CreateList# + )^#9:Expr.Call#, + // Result + @result^#10:Expr.Ident#)^#11:Expr.Comprehension# +L: __comprehension__( + // Variable + v, + // Target + m^#1[1,0]#, + // Accumulator + @result, + // Init + []^#5[1,5]#, + // LoopCondition + true^#6[1,5]#, + // LoopStep + _+_( + @result^#7[1,5]#, + [ + f^#4[1,9]# + ]^#8[1,5]# + )^#9[1,5]#, + // Result + @result^#10[1,5]#)^#11[1,5]# +M: m^#1:Expr.Ident#.map( + v^#3:Expr.Ident#, + f^#4:Expr.Ident# +)^#0:Expr.Call# + +I: m.map(v, p, f) +=====> +P: __comprehension__( + // Variable + v, + // Target + m^#1:Expr.Ident#, + // Accumulator + @result, + // Init + []^#6:Expr.CreateList#, + // LoopCondition + true^#7:bool#, + // LoopStep + _?_:_( + p^#4:Expr.Ident#, + _+_( + @result^#8:Expr.Ident#, + [ + f^#5:Expr.Ident# + ]^#9:Expr.CreateList# + )^#10:Expr.Call#, + @result^#11:Expr.Ident# + )^#12:Expr.Call#, + // Result + @result^#13:Expr.Ident#)^#14:Expr.Comprehension# +L: __comprehension__( + // Variable + v, + // Target + m^#1[1,0]#, + // Accumulator + @result, + // Init + []^#6[1,5]#, + // LoopCondition + true^#7[1,5]#, + // LoopStep + _?_:_( + p^#4[1,9]#, + _+_( + @result^#8[1,5]#, + [ + f^#5[1,12]# + ]^#9[1,5]# + )^#10[1,5]#, + @result^#11[1,5]# + )^#12[1,5]#, + // Result + @result^#13[1,5]#)^#14[1,5]# +M: m^#1:Expr.Ident#.map( + v^#3:Expr.Ident#, + p^#4:Expr.Ident#, + f^#5:Expr.Ident# +)^#0:Expr.Call# + +I: m.map(__result__, __result__) +=====> +E/A: ERROR: :1:7: The iteration variable __result__ overwrites accumulator variable + | m.map(__result__, __result__) + | ......^ +E/P: ERROR: :1:7: The iteration variable __result__ overwrites accumulator variable + | m.map(__result__, __result__) + | ......^ + +I: m.filter(v, p) +=====> +P: __comprehension__( + // Variable + v, + // Target + m^#1:Expr.Ident#, + // Accumulator + @result, + // Init + []^#5:Expr.CreateList#, + // LoopCondition + true^#6:bool#, + // LoopStep + _?_:_( + p^#4:Expr.Ident#, + _+_( + @result^#7:Expr.Ident#, + [ + v^#3:Expr.Ident# + ]^#8:Expr.CreateList# + )^#9:Expr.Call#, + @result^#10:Expr.Ident# + )^#11:Expr.Call#, + // Result + @result^#12:Expr.Ident#)^#13:Expr.Comprehension# +L: __comprehension__( + // Variable + v, + // Target + m^#1[1,0]#, + // Accumulator + @result, + // Init + []^#5[1,8]#, + // LoopCondition + true^#6[1,8]#, + // LoopStep + _?_:_( + p^#4[1,12]#, + _+_( + @result^#7[1,8]#, + [ + v^#3[1,9]# + ]^#8[1,8]# + )^#9[1,8]#, + @result^#10[1,8]# + )^#11[1,8]#, + // Result + @result^#12[1,8]#)^#13[1,8]# +M: m^#1:Expr.Ident#.filter( + v^#3:Expr.Ident#, + p^#4:Expr.Ident# +)^#0:Expr.Call# + +I: m.filter(__result__, false) +=====> +E/A: ERROR: :1:10: The iteration variable __result__ overwrites accumulator variable + | m.filter(__result__, false) + | .........^ +E/P: ERROR: :1:10: The iteration variable __result__ overwrites accumulator variable + | m.filter(__result__, false) + | .........^ + +I: m.filter(a.b, false) +=====> +E/A: ERROR: :1:11: The argument must be a simple name + | m.filter(a.b, false) + | ..........^ +E/P: ERROR: :1:11: The argument must be a simple name + | m.filter(a.b, false) + | ..........^ + +I: x.filter(y, y.filter(z, z > 0)) +=====> +P: __comprehension__( + // Variable + y, + // Target + x^#1:Expr.Ident#, + // Accumulator + @result, + // Init + []^#19:Expr.CreateList#, + // LoopCondition + true^#20:bool#, + // LoopStep + _?_:_( + __comprehension__( + // Variable + z, + // Target + y^#4:Expr.Ident#, + // Accumulator + @result, + // Init + []^#10:Expr.CreateList#, + // LoopCondition + true^#11:bool#, + // LoopStep + _?_:_( + _>_( + z^#7:Expr.Ident#, + 0^#9:int64# + )^#8:Expr.Call#, + _+_( + @result^#12:Expr.Ident#, + [ + z^#6:Expr.Ident# + ]^#13:Expr.CreateList# + )^#14:Expr.Call#, + @result^#15:Expr.Ident# + )^#16:Expr.Call#, + // Result + @result^#17:Expr.Ident#)^#18:Expr.Comprehension#, + _+_( + @result^#21:Expr.Ident#, + [ + y^#3:Expr.Ident# + ]^#22:Expr.CreateList# + )^#23:Expr.Call#, + @result^#24:Expr.Ident# + )^#25:Expr.Call#, + // Result + @result^#26:Expr.Ident#)^#27:Expr.Comprehension# +L: __comprehension__( + // Variable + y, + // Target + x^#1[1,0]#, + // Accumulator + @result, + // Init + []^#19[1,8]#, + // LoopCondition + true^#20[1,8]#, + // LoopStep + _?_:_( + __comprehension__( + // Variable + z, + // Target + y^#4[1,12]#, + // Accumulator + @result, + // Init + []^#10[1,20]#, + // LoopCondition + true^#11[1,20]#, + // LoopStep + _?_:_( + _>_( + z^#7[1,24]#, + 0^#9[1,28]# + )^#8[1,26]#, + _+_( + @result^#12[1,20]#, + [ + z^#6[1,21]# + ]^#13[1,20]# + )^#14[1,20]#, + @result^#15[1,20]# + )^#16[1,20]#, + // Result + @result^#17[1,20]#)^#18[1,20]#, + _+_( + @result^#21[1,8]#, + [ + y^#3[1,9]# + ]^#22[1,8]# + )^#23[1,8]#, + @result^#24[1,8]# + )^#25[1,8]#, + // Result + @result^#26[1,8]#)^#27[1,8]# +M: x^#1:Expr.Ident#.filter( + y^#3:Expr.Ident#, + ^#18:filter# +)^#0:Expr.Call#, +y^#4:Expr.Ident#.filter( + z^#6:Expr.Ident#, + _>_( + z^#7:Expr.Ident#, + 0^#9:int64# + )^#8:Expr.Call# +)^#0:Expr.Call# + +I: has(a.b).filter(c, c) +=====> +P: __comprehension__( + // Variable + c, + // Target + a^#2:Expr.Ident#.b~test-only~^#4:Expr.Select#, + // Accumulator + @result, + // Init + []^#8:Expr.CreateList#, + // LoopCondition + true^#9:bool#, + // LoopStep + _?_:_( + c^#7:Expr.Ident#, + _+_( + @result^#10:Expr.Ident#, + [ + c^#6:Expr.Ident# + ]^#11:Expr.CreateList# + )^#12:Expr.Call#, + @result^#13:Expr.Ident# + )^#14:Expr.Call#, + // Result + @result^#15:Expr.Ident#)^#16:Expr.Comprehension# +L: __comprehension__( + // Variable + c, + // Target + a^#2[1,4]#.b~test-only~^#4[1,3]#, + // Accumulator + @result, + // Init + []^#8[1,15]#, + // LoopCondition + true^#9[1,15]#, + // LoopStep + _?_:_( + c^#7[1,19]#, + _+_( + @result^#10[1,15]#, + [ + c^#6[1,16]# + ]^#11[1,15]# + )^#12[1,15]#, + @result^#13[1,15]# + )^#14[1,15]#, + // Result + @result^#15[1,15]#)^#16[1,15]# +M: ^#4:has#.filter( + c^#6:Expr.Ident#, + c^#7:Expr.Ident# +)^#0:Expr.Call#, +has( + a^#2:Expr.Ident#.b^#3:Expr.Select# +)^#0:Expr.Call# + +I: x.filter(y, y.exists(z, has(z.a)) && y.exists(z, has(z.b))) +=====> +P: __comprehension__( + // Variable + y, + // Target + x^#1:Expr.Ident#, + // Accumulator + @result, + // Init + []^#35:Expr.CreateList#, + // LoopCondition + true^#36:bool#, + // LoopStep + _?_:_( + _&&_( + __comprehension__( + // Variable + z, + // Target + y^#4:Expr.Ident#, + // Accumulator + @result, + // Init + false^#11:bool#, + // LoopCondition + @not_strictly_false( + !_( + @result^#12:Expr.Ident# + )^#13:Expr.Call# + )^#14:Expr.Call#, + // LoopStep + _||_( + @result^#15:Expr.Ident#, + z^#8:Expr.Ident#.a~test-only~^#10:Expr.Select# + )^#16:Expr.Call#, + // Result + @result^#17:Expr.Ident#)^#18:Expr.Comprehension#, + __comprehension__( + // Variable + z, + // Target + y^#20:Expr.Ident#, + // Accumulator + @result, + // Init + false^#27:bool#, + // LoopCondition + @not_strictly_false( + !_( + @result^#28:Expr.Ident# + )^#29:Expr.Call# + )^#30:Expr.Call#, + // LoopStep + _||_( + @result^#31:Expr.Ident#, + z^#24:Expr.Ident#.b~test-only~^#26:Expr.Select# + )^#32:Expr.Call#, + // Result + @result^#33:Expr.Ident#)^#34:Expr.Comprehension# + )^#19:Expr.Call#, + _+_( + @result^#37:Expr.Ident#, + [ + y^#3:Expr.Ident# + ]^#38:Expr.CreateList# + )^#39:Expr.Call#, + @result^#40:Expr.Ident# + )^#41:Expr.Call#, + // Result + @result^#42:Expr.Ident#)^#43:Expr.Comprehension# +L: __comprehension__( + // Variable + y, + // Target + x^#1[1,0]#, + // Accumulator + @result, + // Init + []^#35[1,8]#, + // LoopCondition + true^#36[1,8]#, + // LoopStep + _?_:_( + _&&_( + __comprehension__( + // Variable + z, + // Target + y^#4[1,12]#, + // Accumulator + @result, + // Init + false^#11[1,20]#, + // LoopCondition + @not_strictly_false( + !_( + @result^#12[1,20]# + )^#13[1,20]# + )^#14[1,20]#, + // LoopStep + _||_( + @result^#15[1,20]#, + z^#8[1,28]#.a~test-only~^#10[1,27]# + )^#16[1,20]#, + // Result + @result^#17[1,20]#)^#18[1,20]#, + __comprehension__( + // Variable + z, + // Target + y^#20[1,37]#, + // Accumulator + @result, + // Init + false^#27[1,45]#, + // LoopCondition + @not_strictly_false( + !_( + @result^#28[1,45]# + )^#29[1,45]# + )^#30[1,45]#, + // LoopStep + _||_( + @result^#31[1,45]#, + z^#24[1,53]#.b~test-only~^#26[1,52]# + )^#32[1,45]#, + // Result + @result^#33[1,45]#)^#34[1,45]# + )^#19[1,34]#, + _+_( + @result^#37[1,8]#, + [ + y^#3[1,9]# + ]^#38[1,8]# + )^#39[1,8]#, + @result^#40[1,8]# + )^#41[1,8]#, + // Result + @result^#42[1,8]#)^#43[1,8]# +M: x^#1:Expr.Ident#.filter( + y^#3:Expr.Ident#, + _&&_( + ^#18:exists#, + ^#34:exists# + )^#19:Expr.Call# +)^#0:Expr.Call#, +y^#20:Expr.Ident#.exists( + z^#22:Expr.Ident#, + ^#26:has# +)^#0:Expr.Call#, +has( + z^#24:Expr.Ident#.b^#25:Expr.Select# +)^#0:Expr.Call#, +y^#4:Expr.Ident#.exists( + z^#6:Expr.Ident#, + ^#10:has# +)^#0:Expr.Call#, +has( + z^#8:Expr.Ident#.a^#9:Expr.Select# +)^#0:Expr.Call# + +I: (has(a.b) || has(c.d)).string() +=====> +P: _||_( + a^#2:Expr.Ident#.b~test-only~^#4:Expr.Select#, + c^#7:Expr.Ident#.d~test-only~^#9:Expr.Select# +)^#5:Expr.Call#.string()^#10:Expr.Call# +L: _||_( + a^#2[1,5]#.b~test-only~^#4[1,4]#, + c^#7[1,17]#.d~test-only~^#9[1,16]# +)^#5[1,10]#.string()^#10[1,29]# +M: has( + c^#7:Expr.Ident#.d^#8:Expr.Select# +)^#0:Expr.Call#, +has( + a^#2:Expr.Ident#.b^#3:Expr.Select# +)^#0:Expr.Call# + +I: has(a.b).asList().exists(c, c) +=====> +P: __comprehension__( + // Variable + c, + // Target + a^#2:Expr.Ident#.b~test-only~^#4:Expr.Select#.asList()^#5:Expr.Call#, + // Accumulator + @result, + // Init + false^#9:bool#, + // LoopCondition + @not_strictly_false( + !_( + @result^#10:Expr.Ident# + )^#11:Expr.Call# + )^#12:Expr.Call#, + // LoopStep + _||_( + @result^#13:Expr.Ident#, + c^#8:Expr.Ident# + )^#14:Expr.Call#, + // Result + @result^#15:Expr.Ident#)^#16:Expr.Comprehension# +L: __comprehension__( + // Variable + c, + // Target + a^#2[1,4]#.b~test-only~^#4[1,3]#.asList()^#5[1,15]#, + // Accumulator + @result, + // Init + false^#9[1,24]#, + // LoopCondition + @not_strictly_false( + !_( + @result^#10[1,24]# + )^#11[1,24]# + )^#12[1,24]#, + // LoopStep + _||_( + @result^#13[1,24]#, + c^#8[1,28]# + )^#14[1,24]#, + // Result + @result^#15[1,24]#)^#16[1,24]# +M: a^#2:Expr.Ident#.b~test-only~^#4:has#.asList()^#5:Expr.Call#.exists( + c^#7:Expr.Ident#, + c^#8:Expr.Ident# +)^#0:Expr.Call#, +has( + a^#2:Expr.Ident#.b^#3:Expr.Select# +)^#0:Expr.Call# + +I: [has(a.b), has(c.d)].exists(e, e) +=====> +P: __comprehension__( + // Variable + e, + // Target + [ + a^#3:Expr.Ident#.b~test-only~^#5:Expr.Select#, + c^#7:Expr.Ident#.d~test-only~^#9:Expr.Select# + ]^#1:Expr.CreateList#, + // Accumulator + @result, + // Init + false^#13:bool#, + // LoopCondition + @not_strictly_false( + !_( + @result^#14:Expr.Ident# + )^#15:Expr.Call# + )^#16:Expr.Call#, + // LoopStep + _||_( + @result^#17:Expr.Ident#, + e^#12:Expr.Ident# + )^#18:Expr.Call#, + // Result + @result^#19:Expr.Ident#)^#20:Expr.Comprehension# +L: __comprehension__( + // Variable + e, + // Target + [ + a^#3[1,5]#.b~test-only~^#5[1,4]#, + c^#7[1,15]#.d~test-only~^#9[1,14]# + ]^#1[1,0]#, + // Accumulator + @result, + // Init + false^#13[1,27]#, + // LoopCondition + @not_strictly_false( + !_( + @result^#14[1,27]# + )^#15[1,27]# + )^#16[1,27]#, + // LoopStep + _||_( + @result^#17[1,27]#, + e^#12[1,31]# + )^#18[1,27]#, + // Result + @result^#19[1,27]#)^#20[1,27]# +M: [ + a^#3:Expr.Ident#.b~test-only~^#5:has#, + c^#7:Expr.Ident#.d~test-only~^#9:has# +]^#1:Expr.CreateList#.exists( + e^#11:Expr.Ident#, + e^#12:Expr.Ident# +)^#0:Expr.Call#, +has( + c^#7:Expr.Ident#.d^#8:Expr.Select# +)^#0:Expr.Call#, +has( + a^#3:Expr.Ident#.b^#4:Expr.Select# +)^#0:Expr.Call# + +I: noop_macro(123) +=====> +P: noop_macro( + 123^#2:int64# +)^#1:Expr.Call# +L: noop_macro( + 123^#2[1,11]# +)^#1[1,10]# + +I: get_constant_macro() +=====> +P: 10^#1:int64# +L: 10^#1[NO_POS]# +M: get_constant_macro()^#0:Expr.Call# \ No newline at end of file diff --git a/parser/src/test/resources/pratt_parser_core_syntax.baseline b/parser/src/test/resources/pratt_parser_core_syntax.baseline index 278f27eb8..02f44e87c 100644 --- a/parser/src/test/resources/pratt_parser_core_syntax.baseline +++ b/parser/src/test/resources/pratt_parser_core_syntax.baseline @@ -219,7 +219,7 @@ L: { I: foo{ } =====> P: foo{}^#1:Expr.CreateStruct# -L: foo{}^#1[1,0]# +L: foo{}^#1[1,3]# I: foo{ a:b } =====> @@ -228,7 +228,7 @@ P: foo{ }^#1:Expr.CreateStruct# L: foo{ a:b^#3[1,7]#^#2[1,6]# -}^#1[1,0]# +}^#1[1,3]# I: foo{ a:b, c:d } =====> @@ -239,7 +239,7 @@ P: foo{ L: foo{ a:b^#3[1,7]#^#2[1,6]#, c:d^#5[1,12]#^#4[1,11]# -}^#1[1,0]# +}^#1[1,3]# I: SomeMessage{foo: 5, bar: "xyz"} =====> @@ -250,7 +250,7 @@ P: SomeMessage{ L: SomeMessage{ foo:5^#3[1,17]#^#2[1,15]#, bar:"xyz"^#5[1,25]#^#4[1,23]# -}^#1[1,0]# +}^#1[1,11]# I: TestAllTypes{single_int32: 1, single_int64: 2} =====> @@ -261,7 +261,7 @@ P: TestAllTypes{ L: TestAllTypes{ single_int32:1^#3[1,27]#^#2[1,25]#, single_int64:2^#5[1,44]#^#4[1,42]# -}^#1[1,0]# +}^#1[1,12]# I: MyType{foo: 1, bar: 'baz'} =====> @@ -272,7 +272,7 @@ P: MyType{ L: MyType{ foo:1^#3[1,12]#^#2[1,10]#, bar:"baz"^#5[1,20]#^#4[1,18]# -}^#1[1,0]# +}^#1[1,6]# I: Message{`in`: true} =====> @@ -281,7 +281,7 @@ P: Message{ }^#1:Expr.CreateStruct# L: Message{ in:true^#3[1,14]#^#2[1,12]# -}^#1[1,0]# +}^#1[1,7]# I: Msg{?field: value} =====> @@ -290,7 +290,41 @@ P: Msg{ }^#1:Expr.CreateStruct# L: Msg{ ?field:value^#3[1,12]#^#2[1,10]# -}^#1[1,0]# +}^#1[1,3]# + +I: foo.bar.MyType{ } +=====> +P: foo.bar.MyType{}^#1:Expr.CreateStruct# +L: foo.bar.MyType{}^#1[1,14]# + +I: foo.bar.MyType{ a:b } +=====> +P: foo.bar.MyType{ + a:b^#3:Expr.Ident#^#2:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: foo.bar.MyType{ + a:b^#3[1,18]#^#2[1,17]# +}^#1[1,14]# + +I: .foo.bar.MyType{ a:b } +=====> +P: .foo.bar.MyType{ + a:b^#3:Expr.Ident#^#2:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: .foo.bar.MyType{ + a:b^#3[1,19]#^#2[1,18]# +}^#1[1,15]# + +I: a.b.c.d.Message{ foo: 1, bar: 'baz' } +=====> +P: a.b.c.d.Message{ + foo:1^#3:int64#^#2:Expr.CreateStruct.Entry#, + bar:"baz"^#5:string#^#4:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: a.b.c.d.Message{ + foo:1^#3[1,22]#^#2[1,20]#, + bar:"baz"^#5[1,30]#^#4[1,28]# +}^#1[1,15]# I: a.b =====> @@ -310,7 +344,7 @@ P: _?._( )^#2:Expr.Call# L: _?._( a^#1[1,0]#, - "b"^#3[1,3]# + "b"^#3[1,0]# )^#2[1,1]# I: a.`b-c` @@ -597,7 +631,7 @@ P: _-_( )^#2:Expr.Call# L: _-_( 4^#1[1,0]#, - -4^#3[1,2]# + -4^#3[1,3]# )^#2[1,1]# I: 4--4.1 @@ -608,7 +642,7 @@ P: _-_( )^#2:Expr.Call# L: _-_( 4^#1[1,0]#, - -4.1^#3[1,2]# + -4.1^#3[1,3]# )^#2[1,1]# I: "abc" + "def" @@ -805,29 +839,29 @@ I: a && b =====> P: _&&_( a^#1:Expr.Ident#, - b^#2:Expr.Ident# -)^#3:Expr.Call# + b^#3:Expr.Ident# +)^#2:Expr.Call# L: _&&_( a^#1[1,0]#, - b^#2[1,5]# -)^#3[1,2]# + b^#3[1,5]# +)^#2[1,2]# I: a && b && c =====> P: _&&_( _&&_( a^#1:Expr.Ident#, - b^#2:Expr.Ident# - )^#3:Expr.Call#, - c^#4:Expr.Ident# -)^#5:Expr.Call# + b^#3:Expr.Ident# + )^#2:Expr.Call#, + c^#5:Expr.Ident# +)^#4:Expr.Call# L: _&&_( _&&_( a^#1[1,0]#, - b^#2[1,5]# - )^#3[1,2]#, - c^#4[1,10]# -)^#5[1,7]# + b^#3[1,5]# + )^#2[1,2]#, + c^#5[1,10]# +)^#4[1,7]# I: a && b && c && d && e && f && g =====> @@ -835,40 +869,40 @@ P: _&&_( _&&_( _&&_( a^#1:Expr.Ident#, - b^#2:Expr.Ident# - )^#3:Expr.Call#, + b^#3:Expr.Ident# + )^#2:Expr.Call#, _&&_( - c^#4:Expr.Ident#, - d^#6:Expr.Ident# - )^#7:Expr.Call# - )^#5:Expr.Call#, + c^#5:Expr.Ident#, + d^#7:Expr.Ident# + )^#6:Expr.Call# + )^#4:Expr.Call#, _&&_( _&&_( - e^#8:Expr.Ident#, - f^#10:Expr.Ident# - )^#11:Expr.Call#, - g^#12:Expr.Ident# - )^#13:Expr.Call# -)^#9:Expr.Call# + e^#9:Expr.Ident#, + f^#11:Expr.Ident# + )^#10:Expr.Call#, + g^#13:Expr.Ident# + )^#12:Expr.Call# +)^#8:Expr.Call# L: _&&_( _&&_( _&&_( a^#1[1,0]#, - b^#2[1,5]# - )^#3[1,2]#, + b^#3[1,5]# + )^#2[1,2]#, _&&_( - c^#4[1,10]#, - d^#6[1,15]# - )^#7[1,12]# - )^#5[1,7]#, + c^#5[1,10]#, + d^#7[1,15]# + )^#6[1,12]# + )^#4[1,7]#, _&&_( _&&_( - e^#8[1,20]#, - f^#10[1,25]# - )^#11[1,22]#, - g^#12[1,30]# - )^#13[1,27]# -)^#9[1,17]# + e^#9[1,20]#, + f^#11[1,25]# + )^#10[1,22]#, + g^#13[1,30]# + )^#12[1,27]# +)^#8[1,17]# I: a > 5 && a < 10 =====> @@ -878,31 +912,31 @@ P: _&&_( 5^#3:int64# )^#2:Expr.Call#, _<_( - a^#4:Expr.Ident#, - 10^#6:int64# - )^#5:Expr.Call# -)^#7:Expr.Call# + a^#5:Expr.Ident#, + 10^#7:int64# + )^#6:Expr.Call# +)^#4:Expr.Call# L: _&&_( _>_( a^#1[1,0]#, 5^#3[1,4]# )^#2[1,2]#, _<_( - a^#4[1,9]#, - 10^#6[1,13]# - )^#5[1,11]# -)^#7[1,6]# + a^#5[1,9]#, + 10^#7[1,13]# + )^#6[1,11]# +)^#4[1,6]# I: a || b =====> P: _||_( a^#1:Expr.Ident#, - b^#2:Expr.Ident# -)^#3:Expr.Call# + b^#3:Expr.Ident# +)^#2:Expr.Call# L: _||_( a^#1[1,0]#, - b^#2[1,5]# -)^#3[1,2]# + b^#3[1,5]# +)^#2[1,2]# I: a || b || c || d || e || f =====> @@ -910,34 +944,34 @@ P: _||_( _||_( _||_( a^#1:Expr.Ident#, - b^#2:Expr.Ident# - )^#3:Expr.Call#, - c^#4:Expr.Ident# - )^#5:Expr.Call#, + b^#3:Expr.Ident# + )^#2:Expr.Call#, + c^#5:Expr.Ident# + )^#4:Expr.Call#, _||_( _||_( - d^#6:Expr.Ident#, - e^#8:Expr.Ident# - )^#9:Expr.Call#, - f^#10:Expr.Ident# - )^#11:Expr.Call# -)^#7:Expr.Call# + d^#7:Expr.Ident#, + e^#9:Expr.Ident# + )^#8:Expr.Call#, + f^#11:Expr.Ident# + )^#10:Expr.Call# +)^#6:Expr.Call# L: _||_( _||_( _||_( a^#1[1,0]#, - b^#2[1,5]# - )^#3[1,2]#, - c^#4[1,10]# - )^#5[1,7]#, + b^#3[1,5]# + )^#2[1,2]#, + c^#5[1,10]# + )^#4[1,7]#, _||_( _||_( - d^#6[1,15]#, - e^#8[1,20]# - )^#9[1,17]#, - f^#10[1,25]# - )^#11[1,22]# -)^#7[1,12]# + d^#7[1,15]#, + e^#9[1,20]# + )^#8[1,17]#, + f^#11[1,25]# + )^#10[1,22]# +)^#6[1,12]# I: a < 5 || a > 10 =====> @@ -947,20 +981,20 @@ P: _||_( 5^#3:int64# )^#2:Expr.Call#, _>_( - a^#4:Expr.Ident#, - 10^#6:int64# - )^#5:Expr.Call# -)^#7:Expr.Call# + a^#5:Expr.Ident#, + 10^#7:int64# + )^#6:Expr.Call# +)^#4:Expr.Call# L: _||_( _<_( a^#1[1,0]#, 5^#3[1,4]# )^#2[1,2]#, _>_( - a^#4[1,9]#, - 10^#6[1,13]# - )^#5[1,11]# -)^#7[1,6]# + a^#5[1,9]#, + 10^#7[1,13]# + )^#6[1,11]# +)^#4[1,6]# I: a && b && c && d || e && f && g && h =====> @@ -968,46 +1002,46 @@ P: _||_( _&&_( _&&_( a^#1:Expr.Ident#, - b^#2:Expr.Ident# - )^#3:Expr.Call#, + b^#3:Expr.Ident# + )^#2:Expr.Call#, _&&_( - c^#4:Expr.Ident#, - d^#6:Expr.Ident# - )^#7:Expr.Call# - )^#5:Expr.Call#, + c^#5:Expr.Ident#, + d^#7:Expr.Ident# + )^#6:Expr.Call# + )^#4:Expr.Call#, _&&_( _&&_( - e^#8:Expr.Ident#, - f^#9:Expr.Ident# + e^#9:Expr.Ident#, + f^#11:Expr.Ident# )^#10:Expr.Call#, _&&_( - g^#11:Expr.Ident#, - h^#13:Expr.Ident# + g^#13:Expr.Ident#, + h^#15:Expr.Ident# )^#14:Expr.Call# )^#12:Expr.Call# -)^#15:Expr.Call# +)^#8:Expr.Call# L: _||_( _&&_( _&&_( a^#1[1,0]#, - b^#2[1,5]# - )^#3[1,2]#, + b^#3[1,5]# + )^#2[1,2]#, _&&_( - c^#4[1,10]#, - d^#6[1,15]# - )^#7[1,12]# - )^#5[1,7]#, + c^#5[1,10]#, + d^#7[1,15]# + )^#6[1,12]# + )^#4[1,7]#, _&&_( _&&_( - e^#8[1,20]#, - f^#9[1,25]# + e^#9[1,20]#, + f^#11[1,25]# )^#10[1,22]#, _&&_( - g^#11[1,30]#, - h^#13[1,35]# + g^#13[1,30]#, + h^#15[1,35]# )^#14[1,32]# )^#12[1,27]# -)^#15[1,17]# +)^#8[1,17]# I: a?b:c =====> @@ -1042,11 +1076,11 @@ P: _?_:_( _&&_( false^#1:bool#, !_( - true^#3:bool# - )^#2:Expr.Call# - )^#4:Expr.Call#, - false^#5:bool# - )^#6:Expr.Call#, + true^#4:bool# + )^#3:Expr.Call# + )^#2:Expr.Call#, + false^#6:bool# + )^#5:Expr.Call#, 2^#8:int64#, 3^#9:int64# )^#7:Expr.Call# @@ -1055,11 +1089,11 @@ L: _?_:_( _&&_( false^#1[1,0]#, !_( - true^#3[1,10]# - )^#2[1,9]# - )^#4[1,6]#, - false^#5[1,18]# - )^#6[1,15]#, + true^#4[1,10]# + )^#3[1,9]# + )^#2[1,6]#, + false^#6[1,18]# + )^#5[1,15]#, 2^#8[1,26]#, 3^#9[1,30]# )^#7[1,24]# @@ -1139,23 +1173,23 @@ P: _&&_( 0^#5:int64# )^#4:Expr.Call#, _[?_]( - a^#6:Expr.Ident#, - c^#8:Expr.Ident# - )^#7:Expr.Call# -)^#9:Expr.Call# + a^#7:Expr.Ident#, + c^#9:Expr.Ident# + )^#8:Expr.Call# +)^#6:Expr.Call# L: _&&_( _[?_]( _?._( a^#1[1,0]#, - "b"^#3[1,3]# + "b"^#3[1,0]# )^#2[1,1]#, 0^#5[1,6]# )^#4[1,4]#, _[?_]( - a^#6[1,12]#, - c^#8[1,15]# - )^#7[1,13]# -)^#9[1,9]# + a^#7[1,12]#, + c^#9[1,15]# + )^#8[1,13]# +)^#6[1,9]# I: // comment a @@ -1205,4 +1239,4 @@ P: [ L: [ 1^#2[2,2]#, 2^#3[3,2]# -]^#1[1,0]# +]^#1[1,0]# \ No newline at end of file diff --git a/parser/src/test/resources/pratt_parser_errors.baseline b/parser/src/test/resources/pratt_parser_errors.baseline index e88dd385e..d21e65ab3 100644 --- a/parser/src/test/resources/pratt_parser_errors.baseline +++ b/parser/src/test/resources/pratt_parser_errors.baseline @@ -21,7 +21,7 @@ E: ERROR: :1:5: Syntax error: unexpected character I: ó ¢ »»ó 0  -»»0"""\""\"""\""\"""\""\"""\""\"""\"\"""\""\"""\""\"""\""\"""\"!\"""\""\"""\""\" +»»\u007f0"""\""\"""\""\"""\""\"""\""\"""\"\"""\""\"""\""\"""\""\"""\"!\"""\""\"""\""\" =====> E: ERROR: :1:1: Syntax error: unexpected character | ó ¢ @@ -413,34 +413,34 @@ ERROR: :1:33: Syntax error: expected ']' I: a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.A.B.C.D.E.F.G.H =====> -E: ERROR: :1:62: Expression recursion limit exceeded. limit: 32 +E: ERROR: :1:66: Expression recursion limit exceeded. limit: 32 | a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.A.B.C.D.E.F.G.H - | .............................................................^ + | .................................................................^ I: a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] »» [21][22][23][24][25][26][27][28][29][30][31][32][33] =====> -E: ERROR: :2:48: Expression recursion limit exceeded. limit: 32 +E: ERROR: :2:56: Expression recursion limit exceeded. limit: 32 | [21][22][23][24][25][26][27][28][29][30][31][32][33] - | ...............................................^ + | .......................................................^ I: 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 »»+ 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 »»+ 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 30 »»+ 31 + 32 + 33 + 34 =====> -E: ERROR: :4:8: Expression recursion limit exceeded. limit: 32 +E: ERROR: :4:18: Expression recursion limit exceeded. limit: 32 | + 31 + 32 + 33 + 34 - | .......^ + | .................^ I: a < 1 < 2 < 3 < 4 < 5 < 6 < 7 < 8 < 9 < 10 < 11 »» < 12 < 13 < 14 < 15 < 16 < 17 < 18 < 19 < 20 < 21 »»» < 22 < 23 < 24 < 25 < 26 < 27 < 28 < 29 < 30 < 31 »»» < 32 < 33 =====> -E: ERROR: :3:51: Expression recursion limit exceeded. limit: 32 - | < 22 < 23 < 24 < 25 < 26 < 27 < 28 < 29 < 30 < 31 - | ..................................................^ +E: ERROR: :4:11: Expression recursion limit exceeded. limit: 32 + | < 32 < 33 + | ..........^ I: y!=y!=y!=y!=y!=y!=y!=y!=y!=-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y »»!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y @@ -449,9 +449,9 @@ I: y!=y!=y!=y!=y!=y!=y!=y!=y!=-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y »»!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y »»!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y =====> -E: ERROR: :2:55: Expression recursion limit exceeded. limit: 32 +E: ERROR: :2:63: Expression recursion limit exceeded. limit: 32 | !=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y!=-y!=-y!=-y-y - | ......................................................^ + | ..............................................................^ I: a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != »»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != @@ -468,7 +468,7 @@ I: a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != »»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != »»a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] =====> -E: ERROR: :11:76: Expression recursion limit exceeded. limit: 32 +E: ERROR: :13:76: Expression recursion limit exceeded. limit: 32 | a[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20] != | ...........................................................................^ @@ -480,9 +480,9 @@ E: ERROR: :1:353: Expression recursion limit exceeded. limit: 32 I: !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x =====> -E: ERROR: :1:31: Expression recursion limit exceeded. limit: 32 +E: ERROR: :1:33: Expression recursion limit exceeded. limit: 32 | !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!x - | ..............................^ + | ................................^ I: 123456 =====> diff --git a/parser/src/test/resources/pratt_parser_literals.baseline b/parser/src/test/resources/pratt_parser_literals.baseline index 5a3b7ec77..f6ce2f6b0 100644 --- a/parser/src/test/resources/pratt_parser_literals.baseline +++ b/parser/src/test/resources/pratt_parser_literals.baseline @@ -36,16 +36,16 @@ L: 42^#1[1,0]# I: -1 =====> P: -1^#1:int64# -L: -1^#1[1,0]# +L: -1^#1[1,1]# I: -42 =====> P: -42^#1:int64# -L: -42^#1[1,0]# +L: -42^#1[1,1]# I: 0xFFFFFFFFFFFFFFFFF =====> -E: ERROR: :1:1: Syntax error: invalid int literal +E: ERROR: :1:1: Syntax error: invalid int literal: 0xFFFFFFFFFFFFFFFFF | 0xFFFFFFFFFFFFFFFFF | ^ @@ -57,11 +57,11 @@ L: 9223372036854775807^#1[1,0]# I: -9223372036854775808 =====> P: -9223372036854775808^#1:int64# -L: -9223372036854775808^#1[1,0]# +L: -9223372036854775808^#1[1,1]# I: -(9223372036854775808) =====> -E: ERROR: :1:3: Syntax error: invalid int literal +E: ERROR: :1:3: Syntax error: invalid int literal: 9223372036854775808 | -(9223372036854775808) | ..^ @@ -88,7 +88,7 @@ L: 15u^#1[1,0]# I: 0xFFFFFFFFFFFFFFFFFu =====> -E: ERROR: :1:1: Syntax error: invalid uint literal +E: ERROR: :1:1: Syntax error: invalid uint literal: 0xFFFFFFFFFFFFFFFFFu | 0xFFFFFFFFFFFFFFFFFu | ^ @@ -136,9 +136,8 @@ L: 0.0^#1[1,0]# I: 1.99e90000009 =====> -E: ERROR: :1:1: Syntax error: invalid double literal - | 1.99e90000009 - | ^ +P: Infinity^#1:double# +L: Infinity^#1[1,0]# I: 1e =====> diff --git a/parser/src/test/resources/pratt_parser_macros.baseline b/parser/src/test/resources/pratt_parser_macros.baseline index dabf57e31..688a355d5 100644 --- a/parser/src/test/resources/pratt_parser_macros.baseline +++ b/parser/src/test/resources/pratt_parser_macros.baseline @@ -636,25 +636,25 @@ P: __comprehension__( // Variable z, // Target - y^#19:Expr.Ident#, + y^#20:Expr.Ident#, // Accumulator @result, // Init - false^#26:bool#, + false^#27:bool#, // LoopCondition @not_strictly_false( !_( - @result^#27:Expr.Ident# - )^#28:Expr.Call# - )^#29:Expr.Call#, + @result^#28:Expr.Ident# + )^#29:Expr.Call# + )^#30:Expr.Call#, // LoopStep _||_( - @result^#30:Expr.Ident#, - z^#23:Expr.Ident#.b~test-only~^#25:Expr.Select# - )^#31:Expr.Call#, + @result^#31:Expr.Ident#, + z^#24:Expr.Ident#.b~test-only~^#26:Expr.Select# + )^#32:Expr.Call#, // Result - @result^#32:Expr.Ident#)^#33:Expr.Comprehension# - )^#34:Expr.Call#, + @result^#33:Expr.Ident#)^#34:Expr.Comprehension# + )^#19:Expr.Call#, _+_( @result^#37:Expr.Ident#, [ @@ -705,25 +705,25 @@ L: __comprehension__( // Variable z, // Target - y^#19[1,37]#, + y^#20[1,37]#, // Accumulator @result, // Init - false^#26[1,45]#, + false^#27[1,45]#, // LoopCondition @not_strictly_false( !_( - @result^#27[1,45]# - )^#28[1,45]# - )^#29[1,45]#, + @result^#28[1,45]# + )^#29[1,45]# + )^#30[1,45]#, // LoopStep _||_( - @result^#30[1,45]#, - z^#23[1,53]#.b~test-only~^#25[1,52]# - )^#31[1,45]#, + @result^#31[1,45]#, + z^#24[1,53]#.b~test-only~^#26[1,52]# + )^#32[1,45]#, // Result - @result^#32[1,45]#)^#33[1,45]# - )^#34[1,34]#, + @result^#33[1,45]#)^#34[1,45]# + )^#19[1,34]#, _+_( @result^#37[1,8]#, [ @@ -738,15 +738,15 @@ M: x^#1:Expr.Ident#.filter( y^#3:Expr.Ident#, _&&_( ^#18:exists#, - ^#33:exists# - )^#34:Expr.Call# + ^#34:exists# + )^#19:Expr.Call# )^#0:Expr.Call#, -y^#19:Expr.Ident#.exists( - z^#21:Expr.Ident#, - ^#25:has# +y^#20:Expr.Ident#.exists( + z^#22:Expr.Ident#, + ^#26:has# )^#0:Expr.Call#, has( - z^#23:Expr.Ident#.b^#24:Expr.Select# + z^#24:Expr.Ident#.b^#25:Expr.Select# )^#0:Expr.Call#, y^#4:Expr.Ident#.exists( z^#6:Expr.Ident#, @@ -760,14 +760,14 @@ I: (has(a.b) || has(c.d)).string() =====> P: _||_( a^#2:Expr.Ident#.b~test-only~^#4:Expr.Select#, - c^#6:Expr.Ident#.d~test-only~^#8:Expr.Select# -)^#9:Expr.Call#.string()^#10:Expr.Call# + c^#7:Expr.Ident#.d~test-only~^#9:Expr.Select# +)^#5:Expr.Call#.string()^#10:Expr.Call# L: _||_( a^#2[1,5]#.b~test-only~^#4[1,4]#, - c^#6[1,17]#.d~test-only~^#8[1,16]# -)^#9[1,10]#.string()^#10[1,29]# + c^#7[1,17]#.d~test-only~^#9[1,16]# +)^#5[1,10]#.string()^#10[1,29]# M: has( - c^#6:Expr.Ident#.d^#7:Expr.Select# + c^#7:Expr.Ident#.d^#8:Expr.Select# )^#0:Expr.Call#, has( a^#2:Expr.Ident#.b^#3:Expr.Select# @@ -819,7 +819,7 @@ L: __comprehension__( )^#14[1,24]#, // Result @result^#15[1,24]#)^#16[1,24]# -M: ^#4:has#.asList()^#5:Expr.Call#.exists( +M: a^#2:Expr.Ident#.b~test-only~^#4:has#.asList()^#5:Expr.Call#.exists( c^#7:Expr.Ident#, c^#8:Expr.Ident# )^#0:Expr.Call#, diff --git a/parser/src/test/resources/source_info.baseline b/parser/src/test/resources/source_info.baseline index 153a49822..1e4f6a686 100644 --- a/parser/src/test/resources/source_info.baseline +++ b/parser/src/test/resources/source_info.baseline @@ -140,4 +140,4 @@ macro_calls { } } } -} +} \ No newline at end of file From 045e5760f895716c5cfea035bfc3fda71a79319f Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Thu, 10 Sep 2026 19:03:46 -0700 Subject: [PATCH 195/204] Add plumbing for executor, async evaluation option. Define program API contracts PiperOrigin-RevId: 979522474 --- publish/BUILD.bazel | 1 + runtime/BUILD.bazel | 11 ++ .../src/main/java/dev/cel/runtime/BUILD.bazel | 36 +++++ .../runtime/CelAsyncEvaluationOptions.java | 141 ++++++++++++++++++ .../main/java/dev/cel/runtime/CelRuntime.java | 7 + .../dev/cel/runtime/CelRuntimeBuilder.java | 17 +++ .../java/dev/cel/runtime/CelRuntimeImpl.java | 60 +++++++- .../dev/cel/runtime/CelRuntimeLegacyImpl.java | 36 ++++- .../main/java/dev/cel/runtime/Program.java | 30 ++++ .../java/dev/cel/runtime/ProgramImpl.java | 45 ++++++ .../java/dev/cel/runtime/planner/BUILD.bazel | 2 + .../cel/runtime/planner/PlannedProgram.java | 33 ++++ .../CelAsyncEvaluationOptionsTest.java | 120 +++++++++++++++ .../cel/runtime/CelRuntimeLegacyImplTest.java | 48 +++++- 14 files changed, 581 insertions(+), 6 deletions(-) create mode 100644 runtime/src/main/java/dev/cel/runtime/CelAsyncEvaluationOptions.java create mode 100644 runtime/src/test/java/dev/cel/runtime/CelAsyncEvaluationOptionsTest.java diff --git a/publish/BUILD.bazel b/publish/BUILD.bazel index 17089eb82..2fb948cea 100644 --- a/publish/BUILD.bazel +++ b/publish/BUILD.bazel @@ -32,6 +32,7 @@ RUNTIME_TARGETS = [ "//runtime/src/main/java/dev/cel/runtime:async_call", "//runtime/src/main/java/dev/cel/runtime:async_drain_strategy", "//runtime/src/main/java/dev/cel/runtime:async_observer", + "//runtime/src/main/java/dev/cel/runtime:async_options", "//runtime/src/main/java/dev/cel/runtime:base", "//runtime/src/main/java/dev/cel/runtime:interpreter", "//runtime/src/main/java/dev/cel/runtime:late_function_binding", diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index fbdbb1107..e1acc4261 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -12,6 +12,7 @@ java_library( ":async_call", ":async_drain_strategy", ":async_observer", + ":async_options", ":descriptor_message_provider", ":evaluation_exception", ":function_overload", @@ -417,3 +418,13 @@ cel_android_library( name = "async_observer_android", exports = ["//runtime/src/main/java/dev/cel/runtime:async_observer_android"], ) + +java_library( + name = "async_options", + exports = ["//runtime/src/main/java/dev/cel/runtime:async_options"], +) + +cel_android_library( + name = "async_options_android", + exports = ["//runtime/src/main/java/dev/cel/runtime:async_options_android"], +) diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 145341889..9518e1601 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -821,6 +821,7 @@ java_library( tags = [ ], deps = [ + ":async_options", ":descriptor_type_resolver", ":dispatcher", ":evaluation_exception", @@ -871,6 +872,7 @@ java_library( tags = [ ], deps = [ + ":async_options", ":descriptor_message_provider", ":descriptor_type_resolver", ":dispatcher", @@ -926,6 +928,7 @@ java_library( ], deps = [ ":activation", + ":async_options", ":evaluation_exception", ":evaluation_listener", ":function_binding", @@ -950,6 +953,7 @@ java_library( "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", + "@maven//:org_jspecify_jspecify", ], ) @@ -1363,6 +1367,36 @@ cel_android_library( ], ) +java_library( + name = "async_options", + srcs = ["CelAsyncEvaluationOptions.java"], + tags = [ + ], + deps = [ + ":async_drain_strategy", + ":async_observer", + "//:auto_value", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + ], +) + +cel_android_library( + name = "async_options_android", + srcs = ["CelAsyncEvaluationOptions.java"], + tags = [ + ], + deps = [ + ":async_drain_strategy_android", + ":async_observer_android", + "//:auto_value", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + ], +) + java_library( name = "program", srcs = ["Program.java"], @@ -1374,6 +1408,7 @@ java_library( ":partial_vars", ":variable_resolver", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", ], ) @@ -1388,6 +1423,7 @@ cel_android_library( ":partial_vars_android", ":variable_resolver", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", ], ) diff --git a/runtime/src/main/java/dev/cel/runtime/CelAsyncEvaluationOptions.java b/runtime/src/main/java/dev/cel/runtime/CelAsyncEvaluationOptions.java new file mode 100644 index 000000000..19609e923 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/CelAsyncEvaluationOptions.java @@ -0,0 +1,141 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import com.google.auto.value.AutoValue; +import javax.annotation.concurrent.ThreadSafe; +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicLong; + +/** Options for configuring asynchronous CEL evaluation. */ +@AutoValue +@ThreadSafe +public abstract class CelAsyncEvaluationOptions { + + private static final int DEFAULT_MAX_CONCURRENCY = 100; + private static final int DEFAULT_MAX_ITERATIONS = 1_000; + + /** + * Maximum number of concurrent async function calls in-flight simultaneously. A value <= 0 + * indicates unbounded concurrency. + */ + public abstract int maxConcurrency(); + + /** Strategy governing when to trigger re-evaluation after async call completions. */ + public abstract CelAsyncDrainStrategy drainStrategy(); + + /** Safety cap on the maximum number of AST re-evaluation passes before aborting. */ + public abstract int maxIterations(); + + /** + * Returns the custom configured {@link ScheduledExecutorService}, if present. + * + *

If absent, {@link #resolveScheduledExecutorService()} falls back to an internal, shared + * single-threaded daemon scheduler. + */ + public abstract Optional scheduledExecutorService(); + + /** Returns the configured lifecycle observer, if present. */ + public abstract Optional observer(); + + /** + * Resolves the {@link ScheduledExecutorService} used for debounce timers, falling back to a + * shared, lazily initialized single-threaded daemon scheduler (named {@code + * cel-async-debounce-*}) if not custom-configured. + * + *

The scheduler is used exclusively as an alarm clock to trigger continuation wakeups; it does + * not execute CEL evaluation tasks. + */ + public ScheduledExecutorService resolveScheduledExecutorService() { + return scheduledExecutorService().orElse(DefaultDebounceSchedulerHolder.INSTANCE); + } + + public abstract Builder toBuilder(); + + /** + * Returns a new {@link Builder} initialized with standard default options: + * + *

    + *
  • Maximum concurrency: 100 in-flight calls + *
  • Maximum iterations: 1,000 evaluation passes + *
  • Drain strategy: {@link CelAsyncDrainStrategy#drainReady()} (100-microsecond debounce + * window) + *
  • Scheduled executor service: A shared, lazily initialized single-threaded daemon scheduler + * used exclusively for debounce timer wakeups. + *
+ */ + public static Builder newBuilder() { + return new AutoValue_CelAsyncEvaluationOptions.Builder() + .setMaxConcurrency(DEFAULT_MAX_CONCURRENCY) + .setDrainStrategy(CelAsyncDrainStrategy.drainReady()) + .setMaxIterations(DEFAULT_MAX_ITERATIONS); + } + + /** + * Returns a new {@link Builder} initialized with standard default options. + * + *

Equivalent to calling {@link #newBuilder()}. + */ + public static Builder builder() { + return newBuilder(); + } + + /** + * Returns a {@link CelAsyncEvaluationOptions} instance with the {@link #newBuilder() default + * configuration}. + */ + public static CelAsyncEvaluationOptions defaultOptions() { + return newBuilder().build(); + } + + private static final class DefaultDebounceSchedulerHolder { + private static final AtomicLong counter = new AtomicLong(); + private static final ScheduledExecutorService INSTANCE = + Executors.newSingleThreadScheduledExecutor( + r -> { + Thread t = new Thread(r); + t.setName("cel-async-debounce-" + counter.getAndIncrement()); + t.setDaemon(true); + return t; + }); + } + + /** Builder for {@link CelAsyncEvaluationOptions}. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setMaxConcurrency(int maxConcurrency); + + public abstract Builder setDrainStrategy(CelAsyncDrainStrategy drainStrategy); + + public abstract Builder setMaxIterations(int maxIterations); + + /** + * Sets a custom {@link ScheduledExecutorService} for debounce timers. + * + *

If not set, defaults to an internal, shared single-threaded daemon scheduler. + */ + public abstract Builder setScheduledExecutorService( + ScheduledExecutorService scheduledExecutorService); + + public abstract Builder setObserver(CelAsyncObserver observer); + + public abstract CelAsyncEvaluationOptions build(); + } + + // Package-private constructor prevents extension outside package while allowing AutoValue. + CelAsyncEvaluationOptions() {} +} diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntime.java b/runtime/src/main/java/dev/cel/runtime/CelRuntime.java index 1e7fdcac8..e9c6ca20a 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntime.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntime.java @@ -14,6 +14,7 @@ package dev.cel.runtime; +import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.Immutable; import javax.annotation.concurrent.ThreadSafe; @@ -42,6 +43,12 @@ interface Program extends dev.cel.runtime.Program { /** Evaluate the expression using {@code message} fields as the source of input variables. */ Object eval(Message message) throws CelEvaluationException; + /** + * Evaluate the expression asynchronously using {@code message} fields as the source of input + * variables. + */ + ListenableFuture evalAsync(Message message); + /** * Trace evaluates a compiled program without any variables and invokes the listener as * evaluation progresses through the AST. diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java index 00f6e3bf7..feacf5e37 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java @@ -14,6 +14,7 @@ package dev.cel.runtime; +import com.google.common.util.concurrent.ListeningExecutorService; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CheckReturnValue; import com.google.protobuf.DescriptorProtos.FileDescriptorSet; @@ -214,6 +215,22 @@ public interface CelRuntimeBuilder { @CanIgnoreReturnValue CelRuntimeBuilder setContainer(CelContainer container); + /** + * Sets options to use for asynchronous evaluation. + * + *

If not configured, defaults to {@link CelAsyncEvaluationOptions#defaultOptions()}. + */ + @CanIgnoreReturnValue + CelRuntimeBuilder setAsyncEvaluationOptions(CelAsyncEvaluationOptions asyncEvaluationOptions); + + /** + * Sets the executor to use for asynchronous evaluation. + * + *

This executor is required when evaluating expressions asynchronously via {@link + * Program#evalAsync}. + */ + @CanIgnoreReturnValue + CelRuntimeBuilder setAsyncExecutor(ListeningExecutorService asyncExecutor); /** Build a new instance of the {@code CelRuntime}. */ @CheckReturnValue diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index 5cda25800..857434ba2 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -20,6 +20,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.Immutable; import com.google.protobuf.DescriptorProtos; @@ -95,6 +97,16 @@ public abstract class CelRuntimeImpl implements CelRuntime { @AutoValue.CopyAnnotations abstract @Nullable ExtensionRegistry extensionRegistry(); + // CelAsyncEvaluationOptions is an immutable value object configuring asynchronous evaluation. + @SuppressWarnings("Immutable") + @AutoValue.CopyAnnotations + abstract CelAsyncEvaluationOptions asyncEvaluationOptions(); + + // The executor service is an externally managed, thread-safe asynchronous execution pool. + @SuppressWarnings("Immutable") + @AutoValue.CopyAnnotations + abstract Optional asyncExecutor(); + @Override public Program createProgram(CelAbstractSyntaxTree ast) throws CelEvaluationException { return toRuntimeProgram(planner().plan(ast)); @@ -162,6 +174,44 @@ public Object eval(PartialVars partialVars) throws CelEvaluationException { return program.eval(partialVars); } + @Override + public ListenableFuture evalAsync() { + return program.evalAsync(); + } + + @Override + public ListenableFuture evalAsync(Map mapValue) { + return program.evalAsync(mapValue); + } + + @Override + public ListenableFuture evalAsync( + Map mapValue, CelFunctionResolver lateBoundFunctionResolver) { + return program.evalAsync(mapValue, lateBoundFunctionResolver); + } + + @Override + public ListenableFuture evalAsync(Message message) { + throw new UnsupportedOperationException( + "evalAsync is not supported by this Program implementation."); + } + + @Override + public ListenableFuture evalAsync(CelVariableResolver resolver) { + return program.evalAsync(resolver); + } + + @Override + public ListenableFuture evalAsync( + CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { + return program.evalAsync(resolver, lateBoundFunctionResolver); + } + + @Override + public ListenableFuture evalAsync(PartialVars partialVars) { + return program.evalAsync(partialVars); + } + @Override public Object trace(CelEvaluationListener listener) throws CelEvaluationException { return ((PlannedProgram) program) @@ -253,7 +303,8 @@ public static Builder newBuilder() { .setFunctionBindings(ImmutableMap.of()) .setStandardFunctions(CelStandardFunctions.newBuilder().build()) .setContainer(CelContainer.newBuilder().build()) - .setExtensionRegistry(ExtensionRegistry.getEmptyRegistry()); + .setExtensionRegistry(ExtensionRegistry.getEmptyRegistry()) + .setAsyncEvaluationOptions(CelAsyncEvaluationOptions.defaultOptions()); } /** Builder for {@link CelRuntimeImpl}. */ @@ -280,6 +331,13 @@ public abstract static class Builder implements CelRuntimeBuilder { @Override public abstract Builder setContainer(CelContainer container); + @Override + public abstract Builder setAsyncEvaluationOptions( + CelAsyncEvaluationOptions asyncEvaluationOptions); + + @Override + public abstract Builder setAsyncExecutor(ListeningExecutorService asyncExecutor); + abstract CelOptions options(); abstract CelContainer container(); diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java index 428c6dba5..144de7e9d 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java @@ -20,6 +20,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.ListeningExecutorService; import com.google.errorprone.annotations.CanIgnoreReturnValue; import javax.annotation.concurrent.ThreadSafe; import com.google.protobuf.DescriptorProtos.FileDescriptorSet; @@ -84,6 +85,8 @@ public final class CelRuntimeLegacyImpl implements CelRuntime { private final ImmutableSet celRuntimeLibraries; private final ImmutableList celFunctionBindings; + private final CelAsyncEvaluationOptions asyncEvaluationOptions; + private final @Nullable ListeningExecutorService asyncExecutor; @Override public CelRuntime.Program createProgram(CelAbstractSyntaxTree ast) { @@ -101,7 +104,8 @@ public CelRuntimeBuilder toRuntimeBuilder() { .setExtensionRegistry(extensionRegistry) .addFileTypes(fileDescriptors) .addLibraries(celRuntimeLibraries) - .addFunctionBindings(celFunctionBindings); + .addFunctionBindings(celFunctionBindings) + .setAsyncEvaluationOptions(asyncEvaluationOptions); if (customTypeFactory != null) { builder.setTypeFactory(customTypeFactory); @@ -111,6 +115,9 @@ public CelRuntimeBuilder toRuntimeBuilder() { builder.setStandardFunctions(overriddenStandardFunctions); } + if (asyncExecutor != null) { + builder.setAsyncExecutor(asyncExecutor); + } return builder; } @@ -132,6 +139,8 @@ public static final class Builder implements CelRuntimeBuilder { @VisibleForTesting Function customTypeFactory; @VisibleForTesting CelStandardFunctions overriddenStandardFunctions; + @VisibleForTesting CelAsyncEvaluationOptions asyncEvaluationOptions; + @VisibleForTesting @Nullable ListeningExecutorService asyncExecutor; private CelOptions options; @@ -257,6 +266,19 @@ public CelRuntimeBuilder setContainer(CelContainer container) { "This method is not supported for the legacy runtime"); } + @Override + public CelRuntimeBuilder setAsyncEvaluationOptions( + CelAsyncEvaluationOptions asyncEvaluationOptions) { + this.asyncEvaluationOptions = checkNotNull(asyncEvaluationOptions); + return this; + } + + @Override + public CelRuntimeBuilder setAsyncExecutor(ListeningExecutorService asyncExecutor) { + this.asyncExecutor = checkNotNull(asyncExecutor); + return this; + } + /** Build a new {@code CelRuntimeLegacyImpl} instance from the builder config. */ @Override public CelRuntimeLegacyImpl build() { @@ -357,7 +379,9 @@ public CelRuntimeLegacyImpl build() { overriddenStandardFunctions, fileDescriptors, runtimeLibraries, - ImmutableList.copyOf(customFunctionBindings.values())); + ImmutableList.copyOf(customFunctionBindings.values()), + asyncEvaluationOptions, + asyncExecutor); } private ImmutableSet newStandardFunctionBindings( @@ -432,6 +456,8 @@ private Builder() { this.celRuntimeLibraries = ImmutableSet.builder(); this.extensionRegistry = ExtensionRegistry.getEmptyRegistry(); this.customTypeFactory = null; + this.asyncEvaluationOptions = CelAsyncEvaluationOptions.defaultOptions(); + this.asyncExecutor = null; } } @@ -444,7 +470,9 @@ private CelRuntimeLegacyImpl( @Nullable CelStandardFunctions overriddenStandardFunctions, ImmutableSet fileDescriptors, ImmutableSet celRuntimeLibraries, - ImmutableList celFunctionBindings) { + ImmutableList celFunctionBindings, + CelAsyncEvaluationOptions asyncEvaluationOptions, + @Nullable ListeningExecutorService asyncExecutor) { this.interpreter = interpreter; this.options = options; this.standardEnvironmentEnabled = standardEnvironmentEnabled; @@ -454,5 +482,7 @@ private CelRuntimeLegacyImpl( this.fileDescriptors = fileDescriptors; this.celRuntimeLibraries = celRuntimeLibraries; this.celFunctionBindings = celFunctionBindings; + this.asyncEvaluationOptions = asyncEvaluationOptions; + this.asyncExecutor = asyncExecutor; } } diff --git a/runtime/src/main/java/dev/cel/runtime/Program.java b/runtime/src/main/java/dev/cel/runtime/Program.java index e808a373c..c9df239eb 100644 --- a/runtime/src/main/java/dev/cel/runtime/Program.java +++ b/runtime/src/main/java/dev/cel/runtime/Program.java @@ -14,6 +14,7 @@ package dev.cel.runtime; +import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.Immutable; import java.util.Map; @@ -46,4 +47,33 @@ Object eval(CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionR /** Evaluate a compiled program with unknown attribute patterns {@code partialVars}. */ Object eval(PartialVars partialVars) throws CelEvaluationException; + + /** Evaluate the expression asynchronously without any variables. */ + ListenableFuture evalAsync(); + + /** + * Evaluate the expression asynchronously using a {@code mapValue} as the source of input + * variables. + */ + ListenableFuture evalAsync(Map mapValue); + + /** + * Evaluate the expression asynchronously using a {@code mapValue} as the source of input + * variables and late-bound functions {@code lateBoundFunctionResolver}. + */ + ListenableFuture evalAsync( + Map mapValue, CelFunctionResolver lateBoundFunctionResolver); + + /** Evaluate the expression asynchronously with a custom variable {@code resolver}. */ + ListenableFuture evalAsync(CelVariableResolver resolver); + + /** + * Evaluate the expression asynchronously with a custom variable {@code resolver} and late-bound + * functions {@code lateBoundFunctionResolver}. + */ + ListenableFuture evalAsync( + CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver); + + /** Evaluate the expression asynchronously with unknown attribute patterns {@code partialVars}. */ + ListenableFuture evalAsync(PartialVars partialVars); } diff --git a/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java b/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java index 2543a9525..cc6795561 100644 --- a/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java @@ -16,6 +16,7 @@ import com.google.auto.value.AutoValue; import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.Immutable; import com.google.protobuf.Message; import dev.cel.common.CelOptions; @@ -68,6 +69,50 @@ public Object eval(PartialVars partialVars) throws CelEvaluationException { /* listener= */ Optional.empty()); } + @Override + public ListenableFuture evalAsync() { + throw new UnsupportedOperationException( + "evalAsync is not supported by the legacy interpreter."); + } + + @Override + public ListenableFuture evalAsync(Map mapValue) { + throw new UnsupportedOperationException( + "evalAsync is not supported by the legacy interpreter."); + } + + @Override + public ListenableFuture evalAsync( + Map mapValue, CelFunctionResolver lateBoundFunctionResolver) { + throw new UnsupportedOperationException( + "evalAsync is not supported by the legacy interpreter."); + } + + @Override + public ListenableFuture evalAsync(Message message) { + throw new UnsupportedOperationException( + "evalAsync is not supported by the legacy interpreter."); + } + + @Override + public ListenableFuture evalAsync(CelVariableResolver resolver) { + throw new UnsupportedOperationException( + "evalAsync is not supported by the legacy interpreter."); + } + + @Override + public ListenableFuture evalAsync( + CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { + throw new UnsupportedOperationException( + "evalAsync is not supported by the legacy interpreter."); + } + + @Override + public ListenableFuture evalAsync(PartialVars partialVars) { + throw new UnsupportedOperationException( + "evalAsync is not supported by the legacy interpreter."); + } + @Override public Object trace(CelEvaluationListener listener) throws CelEvaluationException { return evalInternal(Activation.EMPTY, listener); diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index ca7665953..d4dbb1659 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -92,6 +92,7 @@ java_library( "//runtime:resolved_overload", "//runtime:variable_resolver", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", "@maven//:org_jspecify_jspecify", ], ) @@ -622,6 +623,7 @@ cel_android_library( "//runtime/src/main/java/dev/cel/runtime:program_android", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", ], ) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java index 1470e4909..f7f3d7f01 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java @@ -15,6 +15,7 @@ package dev.cel.runtime.planner; import com.google.auto.value.AutoValue; +import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.Immutable; import dev.cel.common.CelOptions; import dev.cel.common.annotations.Internal; @@ -129,6 +130,38 @@ public Object eval(PartialVars partialVars) throws CelEvaluationException { /* listener= */ null); } + @Override + public ListenableFuture evalAsync() { + throw new UnsupportedOperationException("evalAsync is not supported by PlannedProgram."); + } + + @Override + public ListenableFuture evalAsync(Map mapValue) { + throw new UnsupportedOperationException("evalAsync is not supported by PlannedProgram."); + } + + @Override + public ListenableFuture evalAsync( + Map mapValue, CelFunctionResolver lateBoundFunctionResolver) { + throw new UnsupportedOperationException("evalAsync is not supported by PlannedProgram."); + } + + @Override + public ListenableFuture evalAsync(CelVariableResolver resolver) { + throw new UnsupportedOperationException("evalAsync is not supported by PlannedProgram."); + } + + @Override + public ListenableFuture evalAsync( + CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { + throw new UnsupportedOperationException("evalAsync is not supported by PlannedProgram."); + } + + @Override + public ListenableFuture evalAsync(PartialVars partialVars) { + throw new UnsupportedOperationException("evalAsync is not supported by PlannedProgram."); + } + public Object evalOrThrow( PlannedInterpretable interpretable, GlobalResolver resolver, diff --git a/runtime/src/test/java/dev/cel/runtime/CelAsyncEvaluationOptionsTest.java b/runtime/src/test/java/dev/cel/runtime/CelAsyncEvaluationOptionsTest.java new file mode 100644 index 000000000..fc26513e7 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/CelAsyncEvaluationOptionsTest.java @@ -0,0 +1,120 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime; + +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import java.time.Duration; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import org.jspecify.annotations.Nullable; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class CelAsyncEvaluationOptionsTest { + + private ScheduledExecutorService customScheduler; + + @After + public void tearDown() { + if (customScheduler != null) { + customScheduler.shutdown(); + } + } + + @Test + public void defaultOptions_returnsDefaultValues() { + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.defaultOptions(); + + assertThat(options.maxConcurrency()).isEqualTo(100); + assertThat(options.maxIterations()).isEqualTo(1_000); + assertThat(options.drainStrategy()).isNotNull(); + assertThat(options.observer()).isEmpty(); + assertThat(options.scheduledExecutorService()).isEmpty(); + assertThat(options.resolveScheduledExecutorService()).isNotNull(); + } + + @Test + public void resolveScheduledExecutorService_defaultScheduler_runsAsDaemonThread() + throws Exception { + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.defaultOptions(); + + Future isDaemonFuture = + options.resolveScheduledExecutorService().submit(() -> Thread.currentThread().isDaemon()); + + assertThat(isDaemonFuture.get(5, SECONDS)).isTrue(); + } + + @Test + public void builder_validations() { + CelAsyncEvaluationOptions.Builder builder = CelAsyncEvaluationOptions.builder(); + + assertThrows(NullPointerException.class, () -> builder.setDrainStrategy(null)); + assertThrows(NullPointerException.class, () -> builder.setObserver(null)); + assertThrows(NullPointerException.class, () -> builder.setScheduledExecutorService(null)); + } + + @Test + public void builder_nonPositiveMaxConcurrency_roundTripsCleanly() { + CelAsyncEvaluationOptions unboundedZero = + CelAsyncEvaluationOptions.builder().setMaxConcurrency(0).build(); + CelAsyncEvaluationOptions unboundedNegative = + CelAsyncEvaluationOptions.builder().setMaxConcurrency(-1).build(); + + assertThat(unboundedZero.maxConcurrency()).isEqualTo(0); + assertThat(unboundedNegative.maxConcurrency()).isEqualTo(-1); + } + + @Test + public void builder_customValuesAndRoundTrip() { + CelAsyncDrainStrategy drainStrategy = CelAsyncDrainStrategy.drainReady(Duration.ofMillis(25)); + CelAsyncObserver observer = + new CelAsyncObserver() { + @Override + public void onCallStarted(CelAsyncCall call, ImmutableList args) {} + + @Override + public void onCallFinished( + CelAsyncCall call, @Nullable Object result, @Nullable Throwable error) {} + }; + customScheduler = Executors.newSingleThreadScheduledExecutor(); + + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setMaxConcurrency(8) + .setMaxIterations(50) + .setDrainStrategy(drainStrategy) + .setObserver(observer) + .setScheduledExecutorService(customScheduler) + .build(); + + assertThat(options.maxConcurrency()).isEqualTo(8); + assertThat(options.maxIterations()).isEqualTo(50); + assertThat(options.drainStrategy()).isSameInstanceAs(drainStrategy); + assertThat(options.observer()).hasValue(observer); + assertThat(options.scheduledExecutorService()).hasValue(customScheduler); + assertThat(options.resolveScheduledExecutorService()).isSameInstanceAs(customScheduler); + + CelAsyncEvaluationOptions copy = options.toBuilder().build(); + assertThat(copy).isEqualTo(options); + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java b/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java index fec5fab41..5bef0c61e 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java @@ -15,7 +15,11 @@ package dev.cel.runtime; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService; +import static org.junit.Assert.assertThrows; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.ListeningExecutorService; import com.google.protobuf.Message; import dev.cel.common.CelException; import dev.cel.common.exceptions.CelDivideByZeroException; @@ -23,8 +27,8 @@ import dev.cel.compiler.CelCompilerFactory; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.runtime.CelStandardFunctions.StandardFunction; +import java.util.Optional; import java.util.function.Function; -import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -37,7 +41,7 @@ public void evalException() throws CelException { CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build(); CelRuntime runtime = CelRuntimeFactory.standardCelRuntimeBuilder().build(); CelRuntime.Program program = runtime.createProgram(compiler.compile("1/0").getAst()); - CelEvaluationException e = Assert.assertThrows(CelEvaluationException.class, program::eval); + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); assertThat(e).hasCauseThat().isInstanceOf(CelDivideByZeroException.class); } @@ -120,4 +124,44 @@ public void toRuntimeBuilder_optionalProperties() { assertThat(newRuntimeBuilder.overriddenStandardFunctions) .isEqualTo(overriddenStandardFunctions); } + + @Test + public void toRuntimeBuilder_asyncProperties_copied() { + ListeningExecutorService executor = newDirectExecutorService(); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.newBuilder().setMaxConcurrency(5).build(); + CelRuntimeBuilder celRuntimeBuilder = + CelRuntimeFactory.standardCelRuntimeBuilder() + .setAsyncEvaluationOptions(options) + .setAsyncExecutor(executor); + CelRuntime celRuntime = celRuntimeBuilder.build(); + + CelRuntimeLegacyImpl.Builder newRuntimeBuilder = + (CelRuntimeLegacyImpl.Builder) celRuntime.toRuntimeBuilder(); + + assertThat(newRuntimeBuilder.asyncEvaluationOptions).isEqualTo(options); + assertThat(newRuntimeBuilder.asyncExecutor).isEqualTo(executor); + } + + @Test + public void evalAsync_legacyInterpreter_throwsUnsupportedOperationException() throws Exception { + CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + CelRuntime runtime = CelRuntimeFactory.standardCelRuntimeBuilder().build(); + CelRuntime.Program program = runtime.createProgram(compiler.compile("1 + 1").getAst()); + CelVariableResolver resolver = name -> Optional.of(1L); + + assertThrows(UnsupportedOperationException.class, program::evalAsync); + assertThrows(UnsupportedOperationException.class, () -> program.evalAsync(ImmutableMap.of())); + assertThrows( + UnsupportedOperationException.class, + () -> program.evalAsync(ImmutableMap.of(), CelFunctionResolver.EMPTY)); + assertThrows( + UnsupportedOperationException.class, + () -> program.evalAsync(TestAllTypes.getDefaultInstance())); + assertThrows(UnsupportedOperationException.class, () -> program.evalAsync(resolver)); + assertThrows( + UnsupportedOperationException.class, + () -> program.evalAsync(resolver, CelFunctionResolver.EMPTY)); + assertThrows(UnsupportedOperationException.class, () -> program.evalAsync((PartialVars) null)); + } } From abff7d1a570b46a24102f82c08ad01650d9e991e Mon Sep 17 00:00:00 2001 From: Tristan Swadell Date: Fri, 11 Sep 2026 14:00:06 -0700 Subject: [PATCH 196/204] Internal build change PiperOrigin-RevId: 980006129 --- .../src/main/java/dev/cel/verifier/tools/CelVerifierTool.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierTool.java b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierTool.java index 963e966eb..a4210d246 100644 --- a/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierTool.java +++ b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierTool.java @@ -307,6 +307,9 @@ public Integer call() { } public static void main(String[] args) { + if (System.getProperty("z3.skipLibraryLoad") == null) { + System.setProperty("z3.skipLibraryLoad", "true"); + } int exitCode = new CommandLine(new CelVerifierTool()).execute(args); System.exit(exitCode); } From 33da350d02e2b14bc76f622fca2b9687c90c509a Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Fri, 11 Sep 2026 18:34:32 -0700 Subject: [PATCH 197/204] Optimize CEL PrattParser and Lexer performance Improve parsing throughput and reduce memory allocations across CEL expressions. Measured with CelParserBenchmark (parseOnly, built -c opt), comparing three parsers back to back in one session: ANTLR, the Pratt parser before this change, and the Pratt parser after it. Objects allocated per parse: | Case | ANTLR | Pratt before | Pratt after | Pratt vs ANTLR | Delta this CL | | :--- | ---: | ---: | ---: | ---: | ---: | | SMOKE_TEST | 357 | 131 | 123 | 2.9x smaller | -6.1% | | CHAINED_ORS | 968 | 374 | 350 | 2.8x smaller | -6.4% | | LIST_COMPREHENSION | 512 | 218 | 166 | 3.1x smaller | -23.9% | | MESSAGE_CREATION | 1,253 | 502 | 427 | 2.9x smaller | -14.9% | | LONG_LIST | 81,794 | 19,310 | 19,271 | 4.2x smaller | -0.2% | Bytes allocated per parse: | Case | ANTLR | Pratt before | Pratt after | Pratt vs ANTLR | Delta this CL | | :--- | ---: | ---: | ---: | ---: | ---: | | SMOKE_TEST | 12,256 | 3,776 | 3,608 | 3.4x smaller | -4.4% | | CHAINED_ORS | 32,160 | 10,288 | 9,864 | 3.3x smaller | -4.1% | | LIST_COMPREHENSION | 17,320 | 6,120 | 4,736 | 3.7x smaller | -22.6% | | MESSAGE_CREATION | 43,128 | 13,888 | 12,240 | 3.5x smaller | -11.9% | | LONG_LIST | 2,907,488 | 553,160 | 568,080 | 5.1x smaller | +2.7% | Wall clock, mean of 3 caliper trial medians: | Case | ANTLR | Pratt before | Pratt after | Pratt vs ANTLR | Delta this CL | | :--- | ---: | ---: | ---: | ---: | ---: | | SMOKE_TEST | 4,940 ns | 733 ns | 692 ns | 7.1x faster | -5.7% | | CHAINED_ORS | 14,641 ns | 2,090 ns | 2,041 ns | 7.2x faster | -2.3% | | LIST_COMPREHENSION | 7,514 ns | 1,640 ns | 1,218 ns | 6.2x faster | -25.7% | | MESSAGE_CREATION | 20,979 ns | 3,962 ns | 3,487 ns | 6.0x faster | -12.0% | | LONG_LIST | 1,616,631 ns | 144,980 ns | 142,583 ns | 11.3x faster | -1.7% | LONG_LIST is the one case that allocates slightly more than before. It is an extreme outlier (1,000 list elements, ~20k objects per parse) and the +2.7% comes from letting the positions map grow from its default capacity instead of presizing it; presizing cost more on every other case, so the tradeoff is worth it. The map is removed entirely later in this series. PiperOrigin-RevId: 980131070 --- .../src/main/java/dev/cel/parser/BUILD.bazel | 1 + .../src/main/java/dev/cel/parser/Lexer.java | 112 +++-- .../main/java/dev/cel/parser/PrattParser.java | 381 +++++++++--------- .../dev/cel/parser/CelParserImplTest.java | 12 + .../parser/CelParserParameterizedTest.java | 1 + .../java/dev/cel/parser/PrattParserTest.java | 1 + .../resources/parser_core_syntax.baseline | 71 ++++ .../pratt_parser_core_syntax.baseline | 71 ++++ 8 files changed, 395 insertions(+), 255 deletions(-) diff --git a/parser/src/main/java/dev/cel/parser/BUILD.bazel b/parser/src/main/java/dev/cel/parser/BUILD.bazel index 848209380..55183446f 100644 --- a/parser/src/main/java/dev/cel/parser/BUILD.bazel +++ b/parser/src/main/java/dev/cel/parser/BUILD.bazel @@ -118,6 +118,7 @@ java_library( "//common:source_location", "//common/ast", "//common/internal", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:org_jspecify_jspecify", ], diff --git a/parser/src/main/java/dev/cel/parser/Lexer.java b/parser/src/main/java/dev/cel/parser/Lexer.java index 894cda9ce..32b6ebfab 100644 --- a/parser/src/main/java/dev/cel/parser/Lexer.java +++ b/parser/src/main/java/dev/cel/parser/Lexer.java @@ -29,8 +29,6 @@ final class Lexer { enum TokenType { ERROR("error"), END("end"), - WHITESPACE("whitespace"), - COMMENT("comment"), // Keywords NULL("null"), @@ -98,11 +96,17 @@ static final class Token { final TokenType type; final int start; final int end; + final @Nullable String text; Token(TokenType type, int start, int end) { + this(type, start, end, null); + } + + Token(TokenType type, int start, int end, @Nullable String text) { this.type = type; this.start = start; this.end = end; + this.text = text; } @Override @@ -149,35 +153,28 @@ static final class LexerError { .buildOrThrow(); private final CelCodePointArray content; + private final int size; private int position; private LexerError error; Lexer(CelCodePointArray content) { this.content = content; + this.size = content.size(); this.position = 0; this.error = null; } Token lex() { + consumeWhitespaceAndComments(); int start = position; - if (position >= content.size()) { + if (position >= size) { return makeToken(TokenType.END, start, start); } int c = content.get(position); switch (c) { - case '\f': - case '\n': - case ' ': - case '\r': - case 0x0B: // \v (vertical tab) - case '\t': - { - consumeWhitespace(); - return makeToken(TokenType.WHITESPACE, start, position); - } case '.': { - if (position + 1 < content.size() && isDigit(content.get(position + 1))) { + if (position + 1 < size && isDigit(content.get(position + 1))) { return consumeNumericLiteral(); } advance(1); @@ -283,10 +280,6 @@ Token lex() { case '/': { advance(1); - if (consume('/')) { - consumeLine(); - return makeToken(TokenType.COMMENT, start, position); - } return makeToken(TokenType.SLASH, start, position); } case '&': @@ -381,6 +374,10 @@ private Token makeToken(TokenType type, int start, int end) { return new Token(type, start, end); } + private Token makeToken(TokenType type, int start, int end, @Nullable String text) { + return new Token(type, start, end, text); + } + private Token setError(int start, int end, String message) { this.error = new LexerError(start, end, message); return new Token(TokenType.ERROR, start, end); @@ -391,7 +388,7 @@ private void advance(int n) { } private boolean match(int c) { - return position < content.size() && content.get(position) == c; + return position < size && content.get(position) == c; } private boolean consume(int c) { @@ -403,7 +400,7 @@ private boolean consume(int c) { } private boolean consumeIf(IntPredicate predicate) { - if (position < content.size()) { + if (position < size) { int cp = content.get(position); if (predicate.test(cp)) { advance(1); @@ -414,7 +411,7 @@ private boolean consumeIf(IntPredicate predicate) { } private void consumeLine() { - while (position < content.size()) { + while (position < size) { if (content.get(position) == '\n') { advance(1); return; @@ -423,8 +420,8 @@ private void consumeLine() { } } - private void consumeWhitespace() { - while (position < content.size()) { + private void consumeWhitespaceAndComments() { + while (position < size) { int c = content.get(position); switch (c) { case '\f': @@ -433,8 +430,15 @@ private void consumeWhitespace() { case '\r': case 11: // \v case '\t': - advance(1); + position++; break; + case '/': + if (position + 1 < size && content.get(position + 1) == '/') { + consumeLine(); + break; + } else { + return; + } default: return; } @@ -442,29 +446,19 @@ private void consumeWhitespace() { } private boolean consumeDigits() { - boolean advanced = false; - while (position < content.size()) { - int c = content.get(position); - if (!isDigit(c)) { - break; - } - advance(1); - advanced = true; + int start = position; + while (position < size && isDigit(content.get(position))) { + position++; } - return advanced; + return position > start; } private boolean consumeHexDigits() { - boolean advanced = false; - while (position < content.size()) { - int c = content.get(position); - if (!isHexDigit(c)) { - break; - } - advance(1); - advanced = true; + int start = position; + while (position < size && isHexDigit(content.get(position))) { + position++; } - return advanced; + return position > start; } private TokenType consumeIntegralSuffix() { @@ -486,7 +480,7 @@ private Token consumeQuotedIdent() { private boolean consumeUntilAfter(int c, boolean isRaw) { int pos = position; boolean escaped = false; - while (pos < content.size()) { + while (pos < size) { int cc = content.get(pos); if (cc == '\n' || cc == '\r') { position = pos; @@ -503,20 +497,20 @@ private boolean consumeUntilAfter(int c, boolean isRaw) { } pos++; } - position = content.size(); + position = size; return false; } private boolean consumeUntilAfterTripleQuote(int quote, boolean isRaw) { int pos = position; boolean escaped = false; - while (pos < content.size()) { + while (pos < size) { int cc = content.get(pos); if (!isRaw && cc == '\\') { escaped = !escaped; } else { if ((isRaw || !escaped) - && pos + 2 < content.size() + && pos + 2 < size && cc == quote && content.get(pos + 1) == quote && content.get(pos + 2) == quote) { @@ -527,16 +521,14 @@ private boolean consumeUntilAfterTripleQuote(int quote, boolean isRaw) { } pos++; } - position = content.size(); + position = size; return false; } private Token consumeStringLiteral(int start, int quote, boolean isBytes, boolean isRaw) { advance(1); boolean isTripleQuote = - position + 1 < content.size() - && content.get(position) == quote - && content.get(position + 1) == quote; + position + 1 < size && content.get(position) == quote && content.get(position + 1) == quote; if (isTripleQuote) { advance(2); if (!consumeUntilAfterTripleQuote(quote, isRaw)) { @@ -556,7 +548,7 @@ private Token consumeStringLiteral(int start, int quote, boolean isBytes, boolea private @Nullable Token consumePrefixedStringLiteral() { int start = position; - if (position >= content.size()) { + if (position >= size) { return null; } int c = content.get(position); @@ -566,7 +558,7 @@ private Token consumeStringLiteral(int start, int quote, boolean isBytes, boolea return null; } int lookahead = 1; - if (position + 1 < content.size()) { + if (position + 1 < size) { int c2 = content.get(position + 1); if (isBytes ? (c2 == 'r' || c2 == 'R') : (c2 == 'b' || c2 == 'B')) { isBytes = true; @@ -574,7 +566,7 @@ private Token consumeStringLiteral(int start, int quote, boolean isBytes, boolea lookahead = 2; } } - if (position + lookahead < content.size()) { + if (position + lookahead < size) { int quote = content.get(position + lookahead); if (quote == '"' || quote == '\'') { advance(lookahead); @@ -612,9 +604,9 @@ private Token consumeNumericLiteral() { return makeToken(tokenType, start, position); } consumeDigits(); - if (position < content.size() + if (position < size && content.get(position) == '.' - && position + 1 < content.size() + && position + 1 < size && isDigit(content.get(position + 1))) { floatingPoint = true; advance(1); @@ -639,12 +631,8 @@ && isDigit(content.get(position + 1))) { private Token consumeIdent() { int start = position; - while (position < content.size()) { - int c = content.get(position); - if (!isIdentTrailing(c)) { - break; - } - advance(1); + while (position < size && isIdentTrailing(content.get(position))) { + position++; } int end = position; String word = content.slice(start, end).toString(); @@ -652,6 +640,6 @@ private Token consumeIdent() { if (keywordType != null) { return makeToken(keywordType, start, end); } - return makeToken(TokenType.IDENT, start, end); + return makeToken(TokenType.IDENT, start, end, word); } } diff --git a/parser/src/main/java/dev/cel/parser/PrattParser.java b/parser/src/main/java/dev/cel/parser/PrattParser.java index 17ce514d5..3ddb0baad 100644 --- a/parser/src/main/java/dev/cel/parser/PrattParser.java +++ b/parser/src/main/java/dev/cel/parser/PrattParser.java @@ -30,6 +30,7 @@ import java.text.ParseException; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -41,6 +42,9 @@ final class PrattParser { private static final String ACCUMULATOR_NAME = "@result"; private static final CelExpr ERROR = CelExpr.newBuilder().setConstant(Constants.ERROR).build(); + private static final Lexer.Token END_TOKEN = new Lexer.Token(Lexer.TokenType.END, 0, 0); + /** Most logical chains are short; 8 avoids resizing for the overwhelming majority. */ + private static final int INITIAL_CHAIN_CAPACITY = 8; private static final class BinaryOpInfo { final int precedence; @@ -56,72 +60,49 @@ private static final class BinaryOpInfo { } } - private static final BinaryOpInfo LOGICAL_OR_OP = - new BinaryOpInfo(1, Operator.LOGICAL_OR.getFunction(), true, Lexer.TokenType.LOGICAL_OR); - private static final BinaryOpInfo LOGICAL_AND_OP = - new BinaryOpInfo(2, Operator.LOGICAL_AND.getFunction(), true, Lexer.TokenType.LOGICAL_AND); - private static final BinaryOpInfo LESS_OP = - new BinaryOpInfo(3, Operator.LESS.getFunction(), false, Lexer.TokenType.LESS); - private static final BinaryOpInfo LESS_EQUAL_OP = - new BinaryOpInfo(3, Operator.LESS_EQUALS.getFunction(), false, Lexer.TokenType.LESS_EQUAL); - private static final BinaryOpInfo GREATER_OP = - new BinaryOpInfo(3, Operator.GREATER.getFunction(), false, Lexer.TokenType.GREATER); - private static final BinaryOpInfo GREATER_EQUAL_OP = - new BinaryOpInfo( - 3, Operator.GREATER_EQUALS.getFunction(), false, Lexer.TokenType.GREATER_EQUAL); - private static final BinaryOpInfo EQUAL_EQUAL_OP = - new BinaryOpInfo(3, Operator.EQUALS.getFunction(), false, Lexer.TokenType.EQUAL_EQUAL); - private static final BinaryOpInfo EXCLAMATION_EQUAL_OP = - new BinaryOpInfo( - 3, Operator.NOT_EQUALS.getFunction(), false, Lexer.TokenType.EXCLAMATION_EQUAL); - private static final BinaryOpInfo IN_OP = - new BinaryOpInfo(3, Operator.IN.getFunction(), false, Lexer.TokenType.IN); - private static final BinaryOpInfo PLUS_OP = - new BinaryOpInfo(4, Operator.ADD.getFunction(), false, Lexer.TokenType.PLUS); - private static final BinaryOpInfo MINUS_OP = - new BinaryOpInfo(4, Operator.SUBTRACT.getFunction(), false, Lexer.TokenType.MINUS); - private static final BinaryOpInfo ASTERISK_OP = - new BinaryOpInfo(5, Operator.MULTIPLY.getFunction(), false, Lexer.TokenType.ASTERISK); - private static final BinaryOpInfo SLASH_OP = - new BinaryOpInfo(5, Operator.DIVIDE.getFunction(), false, Lexer.TokenType.SLASH); - private static final BinaryOpInfo PERCENT_OP = - new BinaryOpInfo(5, Operator.MODULO.getFunction(), false, Lexer.TokenType.PERCENT); - private static final BinaryOpInfo DEFAULT_OP = - new BinaryOpInfo(0, "", false, Lexer.TokenType.ERROR); - - private static BinaryOpInfo getBinaryOpInfo(Lexer.TokenType type) { - switch (type) { - case LOGICAL_OR: - return LOGICAL_OR_OP; - case LOGICAL_AND: - return LOGICAL_AND_OP; - case LESS: - return LESS_OP; - case LESS_EQUAL: - return LESS_EQUAL_OP; - case GREATER: - return GREATER_OP; - case GREATER_EQUAL: - return GREATER_EQUAL_OP; - case EQUAL_EQUAL: - return EQUAL_EQUAL_OP; - case EXCLAMATION_EQUAL: - return EXCLAMATION_EQUAL_OP; - case IN: - return IN_OP; - case PLUS: - return PLUS_OP; - case MINUS: - return MINUS_OP; - case ASTERISK: - return ASTERISK_OP; - case SLASH: - return SLASH_OP; - case PERCENT: - return PERCENT_OP; - default: - return DEFAULT_OP; - } + private static final BinaryOpInfo[] binaryOps = initBinaryOps(); + + // Safe and desirable to use .ordinal() here: + // 1. Safe: This lookup table is strictly private and internal to PrattParser, never serialized or + // persisted. The array is sized to TokenType.values().length, so indexing by ordinal is + // guaranteed to be within bounds even if enum members change. + // 2. Desirable: Expression parsing checks binary operator info on every token in the input; + // direct array indexing by ordinal provides O(1) lookup with zero hashing, indirection, + // or boxing overhead on this critical hot path. + @SuppressWarnings("EnumOrdinal") + private static BinaryOpInfo[] initBinaryOps() { + BinaryOpInfo[] ops = new BinaryOpInfo[Lexer.TokenType.values().length]; + ops[Lexer.TokenType.LOGICAL_OR.ordinal()] = + new BinaryOpInfo(1, Operator.LOGICAL_OR.getFunction(), true, Lexer.TokenType.LOGICAL_OR); + ops[Lexer.TokenType.LOGICAL_AND.ordinal()] = + new BinaryOpInfo(2, Operator.LOGICAL_AND.getFunction(), true, Lexer.TokenType.LOGICAL_AND); + ops[Lexer.TokenType.LESS.ordinal()] = + new BinaryOpInfo(3, Operator.LESS.getFunction(), false, Lexer.TokenType.LESS); + ops[Lexer.TokenType.LESS_EQUAL.ordinal()] = + new BinaryOpInfo(3, Operator.LESS_EQUALS.getFunction(), false, Lexer.TokenType.LESS_EQUAL); + ops[Lexer.TokenType.GREATER.ordinal()] = + new BinaryOpInfo(3, Operator.GREATER.getFunction(), false, Lexer.TokenType.GREATER); + ops[Lexer.TokenType.GREATER_EQUAL.ordinal()] = + new BinaryOpInfo( + 3, Operator.GREATER_EQUALS.getFunction(), false, Lexer.TokenType.GREATER_EQUAL); + ops[Lexer.TokenType.EQUAL_EQUAL.ordinal()] = + new BinaryOpInfo(3, Operator.EQUALS.getFunction(), false, Lexer.TokenType.EQUAL_EQUAL); + ops[Lexer.TokenType.EXCLAMATION_EQUAL.ordinal()] = + new BinaryOpInfo( + 3, Operator.NOT_EQUALS.getFunction(), false, Lexer.TokenType.EXCLAMATION_EQUAL); + ops[Lexer.TokenType.IN.ordinal()] = + new BinaryOpInfo(3, Operator.IN.getFunction(), false, Lexer.TokenType.IN); + ops[Lexer.TokenType.PLUS.ordinal()] = + new BinaryOpInfo(4, Operator.ADD.getFunction(), false, Lexer.TokenType.PLUS); + ops[Lexer.TokenType.MINUS.ordinal()] = + new BinaryOpInfo(4, Operator.SUBTRACT.getFunction(), false, Lexer.TokenType.MINUS); + ops[Lexer.TokenType.ASTERISK.ordinal()] = + new BinaryOpInfo(5, Operator.MULTIPLY.getFunction(), false, Lexer.TokenType.ASTERISK); + ops[Lexer.TokenType.SLASH.ordinal()] = + new BinaryOpInfo(5, Operator.DIVIDE.getFunction(), false, Lexer.TokenType.SLASH); + ops[Lexer.TokenType.PERCENT.ordinal()] = + new BinaryOpInfo(5, Operator.MODULO.getFunction(), false, Lexer.TokenType.PERCENT); + return ops; } private static final class UnaryOp { @@ -138,10 +119,9 @@ private static final class UnaryOp { private final ImmutableMap macros; private final Lexer lexer; private final Map positions; - private final Map macroCalls; + private Map macroCalls = ImmutableMap.of(); + private PrattMacroExprFactory macroExprFactory; private final List issues; - private final PrattMacroExprFactory macroExprFactory; - private Lexer.Token currentToken; private Lexer.Token peekToken; private int recursionDepth; @@ -184,11 +164,9 @@ private PrattParser(CelSource source, CelOptions options, Map this.macros = ImmutableMap.copyOf(macros); this.lexer = new Lexer(source.getContent()); this.positions = new HashMap<>(); - this.macroCalls = new HashMap<>(); this.issues = new ArrayList<>(); - this.macroExprFactory = new PrattMacroExprFactory(); this.nextId = 1; - initTokenStream(); + peekToken = nextSignificantToken(true); } CelExpr run() { @@ -215,11 +193,10 @@ private boolean isRecoveryLimitExceeded() { return errorCount > options.maxParseErrorRecoveryLimit(); } - private void initTokenStream() { - peekToken = nextSignificantToken(true); - } - private String getTokenText(Lexer.Token tok) { + if (tok.text != null) { + return tok.text; + } if (tok.start >= 0 && tok.end >= tok.start && tok.end <= source.getContent().size()) { return source.getContent().slice(tok.start, tok.end).toString(); } @@ -227,28 +204,20 @@ private String getTokenText(Lexer.Token tok) { } private Lexer.Token nextSignificantToken(boolean reportError) { - if (isRecoveryLimitExceeded()) { - return new Lexer.Token(Lexer.TokenType.END, 0, 0); - } - while (true) { - Lexer.Token tok = lexer.lex(); - if (tok.type == Lexer.TokenType.WHITESPACE || tok.type == Lexer.TokenType.COMMENT) { - continue; + Lexer.Token tok = lexer.lex(); + if (tok.type == Lexer.TokenType.ERROR && reportError) { + reportSyntaxError(tok, lexer.getError().message); + if (isRecoveryLimitExceeded()) { + return END_TOKEN; } - if (tok.type == Lexer.TokenType.ERROR && reportError) { - reportSyntaxError(tok, lexer.getError().message); - if (isRecoveryLimitExceeded()) { - return new Lexer.Token(Lexer.TokenType.END, 0, 0); - } - } - return tok; } + return tok; } private Lexer.Token nextToken() { currentToken = peekToken; if (isRecoveryLimitExceeded()) { - peekToken = new Lexer.Token(Lexer.TokenType.END, 0, 0); + peekToken = END_TOKEN; return currentToken; } if (peekToken.type != Lexer.TokenType.END) { @@ -283,7 +252,7 @@ private boolean expect(Lexer.TokenType type, String msg) { private void synchronizeOnDelimiter() { if (isRecoveryLimitExceeded()) { - peekToken = new Lexer.Token(Lexer.TokenType.END, 0, 0); + peekToken = END_TOKEN; return; } while (peekToken.type != Lexer.TokenType.END) { @@ -356,7 +325,7 @@ private void reportError(CelSourceLocation loc, String msg) { CelIssue.formatError( CelSourceLocation.NONE, String.format("More than %d parse errors.", options.maxParseErrorRecoveryLimit()))); - peekToken = new Lexer.Token(Lexer.TokenType.END, 0, 0); + peekToken = END_TOKEN; } if (errorCount <= options.maxParseErrorRecoveryLimit()) { issues.add(CelIssue.formatError(loc, msg)); @@ -369,33 +338,37 @@ private void reportSyntaxError(Lexer.Token token, String msg) { private boolean checkRecursion(int chainDepth, Lexer.Token token) { if (recursionDepth + chainDepth > options.maxParseRecursionDepth()) { - if (!recursionLimitExceeded) { - recursionLimitExceeded = true; - reportError( - token.start, - String.format( - "Expression recursion limit exceeded. limit: %d", - options.maxParseRecursionDepth())); - } + reportRecursionLimit(token.start); return true; } return false; } + private void reportRecursionLimit(int position) { + if (!recursionLimitExceeded) { + recursionLimitExceeded = true; + reportError( + position, + String.format( + "Expression recursion limit exceeded. limit: %d", options.maxParseRecursionDepth())); + } + } + private CelExpr parseExpr() { - if (recursionLimitExceeded || isRecoveryLimitExceeded()) { + if (recursionLimitExceeded || errorCount > options.maxParseErrorRecoveryLimit()) { return ERROR; } - recursionDepth++; - if (checkRecursion(0, peekToken)) { - recursionDepth--; + if (recursionDepth >= options.maxParseRecursionDepth()) { + reportRecursionLimit(peekToken.start); return ERROR; } + recursionDepth++; CelExpr expr = parseBinaryAndTernary(0); recursionDepth--; return expr; } + @SuppressWarnings("EnumOrdinal") // Using ordinal for O(1) binary operator lookup table private CelExpr parseBinaryAndTernary(int minPrec) { CelExpr lhs = parseSelectorChain(); int chainDepth = currentLhsDepth; @@ -406,8 +379,8 @@ private CelExpr parseBinaryAndTernary(int minPrec) { continue; } - BinaryOpInfo opInfo = getBinaryOpInfo(tok); - if (opInfo.precedence < minPrec || opInfo.precedence == 0) { + BinaryOpInfo opInfo = binaryOps[tok.ordinal()]; + if (opInfo == null || opInfo.precedence < minPrec) { break; } @@ -417,7 +390,8 @@ private CelExpr parseBinaryAndTernary(int minPrec) { } Lexer.Token opTok = nextToken(); - if (checkRecursion(chainDepth, opTok)) { + if (recursionDepth + chainDepth > options.maxParseRecursionDepth()) { + reportRecursionLimit(opTok.start); return ERROR; } chainDepth++; @@ -449,51 +423,60 @@ private CelExpr parseTernary(CelExpr lhs) { .build(); } - private CelExpr buildBinaryCall(long opId, String opName, CelExpr lhs, CelExpr rhs) { - return CelExpr.newBuilder() - .setId(opId) - .setCall(CelExpr.CelCall.newBuilder().setFunction(opName).addArgs(lhs).addArgs(rhs).build()) - .build(); - } - private CelExpr parseBalancedLogicalChain(CelExpr lhs, BinaryOpInfo opInfo) { - List terms = new ArrayList<>(); - List ops = new ArrayList<>(); - terms.add(lhs); + Lexer.Token opTok = nextToken(); + long opId = nextId(opTok.start); + CelExpr rhs = parseBinaryAndTernary(opInfo.precedence + 1); + if (peekToken.type != opInfo.type) { + return buildBinaryCall(opId, opInfo.name, lhs, rhs); + } + + CelExpr[] terms = new CelExpr[INITIAL_CHAIN_CAPACITY]; + long[] ops = new long[INITIAL_CHAIN_CAPACITY]; + terms[0] = lhs; + terms[1] = rhs; + ops[0] = opId; + int opsCount = 1; + int termsCount = 2; + while (peekToken.type == opInfo.type) { - Lexer.Token opTok = nextToken(); - long opId = nextId(opTok); - CelExpr rhs = parseBinaryAndTernary(opInfo.precedence + 1); - ops.add(opId); - terms.add(rhs); + opTok = nextToken(); + opId = nextId(opTok.start); + rhs = parseBinaryAndTernary(opInfo.precedence + 1); + if (termsCount == terms.length) { + int newCapacity = terms.length * 2; + ops = Arrays.copyOf(ops, newCapacity); + terms = Arrays.copyOf(terms, newCapacity); + } + ops[opsCount++] = opId; + terms[termsCount++] = rhs; } - return balancedTree(opInfo.name, terms, ops, 0, ops.size() - 1); + return balancedTree(opInfo.name, terms, ops, 0, opsCount - 1); } - private CelExpr balancedTree(String op, List terms, List ops, int lo, int hi) { + private CelExpr balancedTree(String op, CelExpr[] terms, long[] ops, int lo, int hi) { int mid = (lo + hi + 1) / 2; - CelExpr left; - if (mid == lo) { - left = terms.get(mid); - } else { - left = balancedTree(op, terms, ops, lo, mid - 1); - } - CelExpr right; - if (mid == hi) { - right = terms.get(mid + 1); - } else { - right = balancedTree(op, terms, ops, mid + 1, hi); - } + CelExpr left = (mid == lo) ? terms[mid] : balancedTree(op, terms, ops, lo, mid - 1); + CelExpr right = (mid == hi) ? terms[mid + 1] : balancedTree(op, terms, ops, mid + 1, hi); + return buildBinaryCall(ops[mid], op, left, right); + } + + private static CelExpr buildBinaryCall(long id, String function, CelExpr lhs, CelExpr rhs) { return CelExpr.newBuilder() - .setId(ops.get(mid)) - .setCall(CelExpr.CelCall.newBuilder().setFunction(op).addArgs(left).addArgs(right).build()) + .setId(id) + .setCall( + CelExpr.CelCall.newBuilder().setFunction(function).addArgs(lhs).addArgs(rhs).build()) .build(); } private CelExpr parseSelectorChain() { - CelExpr lhs = parseUnary(); - currentLhsDepth = 0; Lexer.TokenType tok = peekToken.type; + CelExpr lhs = + (tok == Lexer.TokenType.EXCLAMATION || tok == Lexer.TokenType.MINUS) + ? parseUnaryOps() + : parsePrimary(); + currentLhsDepth = 0; + tok = peekToken.type; if (tok == Lexer.TokenType.DOT || tok == Lexer.TokenType.LEFT_BRACKET || tok == Lexer.TokenType.LEFT_BRACE) { @@ -538,10 +521,7 @@ private CelExpr parseSelectorChainTail(CelExpr initialLhs) { long opId = nextId(dotTok); CelExpr arg1 = lhs; CelExpr arg2 = - CelExpr.newBuilder() - .setId(nextId(getLeftmostPosition(lhs))) - .setConstant(CelConstant.ofValue(idText)) - .build(); + CelExpr.ofConstant(nextId(getLeftmostPosition(lhs)), CelConstant.ofValue(idText)); lhs = CelExpr.newBuilder() .setId(opId) @@ -572,12 +552,7 @@ private CelExpr parseSelectorChainTail(CelExpr initialLhs) { .build(); } } else { - lhs = - CelExpr.newBuilder() - .setId(nextId(dotTok)) - .setSelect( - CelExpr.CelSelect.newBuilder().setOperand(lhs).setField(idText).build()) - .build(); + lhs = CelExpr.ofSelect(nextId(dotTok), lhs, idText, /* isTestOnly= */ false); } } else if (tok == Lexer.TokenType.LEFT_BRACKET) { if (checkRecursion(chainDepth, peekToken)) { @@ -609,7 +584,7 @@ private CelExpr parseSelectorChainTail(CelExpr initialLhs) { .build()) .build(); } else if (tok == Lexer.TokenType.LEFT_BRACE) { - String structName = extractStructName(lhs).orElse(null); + String structName = extractStructName(lhs); if (structName == null) { break; } @@ -622,14 +597,6 @@ private CelExpr parseSelectorChainTail(CelExpr initialLhs) { return lhs; } - private CelExpr parseUnary() { - Lexer.TokenType tok = peekToken.type; - if (tok == Lexer.TokenType.EXCLAMATION || tok == Lexer.TokenType.MINUS) { - return parseUnaryOps(); - } - return parsePrimary(); - } - private CelExpr parseUnaryOps() { Lexer.Token op = nextToken(); Lexer.TokenType opType = op.type; @@ -783,10 +750,7 @@ private CelExpr parseIdentOrCall() { .build(); } long id = nextId(leadingDot ? firstTok : idTok); - return CelExpr.newBuilder() - .setId(id) - .setIdent(CelExpr.CelIdent.newBuilder().setName(name).build()) - .build(); + return CelExpr.ofIdent(id, name); } private CelExpr parsePrimary() { @@ -807,15 +771,13 @@ private CelExpr parsePrimary() { return expr; } case NULL: - return CelExpr.newBuilder().setId(nextId(nextToken())).setConstant(Constants.NULL).build(); + return CelExpr.ofConstant(nextId(nextToken()), Constants.NULL); case TRUE: case FALSE: { Lexer.Token tok = nextToken(); - return CelExpr.newBuilder() - .setId(nextId(tok)) - .setConstant(tok.type == Lexer.TokenType.TRUE ? Constants.TRUE : Constants.FALSE) - .build(); + return CelExpr.ofConstant( + nextId(tok), tok.type == Lexer.TokenType.TRUE ? Constants.TRUE : Constants.FALSE); } case INT: return parseIntLiteral(/* nodeId= */ -1, /* isNegative= */ false); @@ -990,7 +952,7 @@ private CelExpr parseIntLiteral(long nodeId, boolean isNegative) { long id = nodeId == -1 ? nextId(tok) : nodeId; try { CelConstant constExpr = Constants.parseInt(text); - return CelExpr.newBuilder().setId(id).setConstant(constExpr).build(); + return CelExpr.ofConstant(id, constExpr); } catch (ParseException e) { reportSyntaxError(tok, "invalid int literal: " + text); return CelExpr.newBuilder().setId(nextId(tok)).build(); @@ -1002,7 +964,7 @@ private CelExpr parseUintLiteral() { String value = getTokenText(tok); try { CelConstant constExpr = Constants.parseUint(value); - return CelExpr.newBuilder().setId(nextId(tok)).setConstant(constExpr).build(); + return CelExpr.ofConstant(nextId(tok), constExpr); } catch (ParseException e) { reportSyntaxError(tok, "invalid uint literal: " + value); return CelExpr.newBuilder().setId(nextId(tok)).build(); @@ -1015,7 +977,7 @@ private CelExpr parseDoubleLiteral(long nodeId, boolean isNegative) { long id = nodeId == -1 ? nextId(tok) : nodeId; try { CelConstant constExpr = Constants.parseDouble(text); - return CelExpr.newBuilder().setId(id).setConstant(constExpr).build(); + return CelExpr.ofConstant(id, constExpr); } catch (ParseException e) { reportSyntaxError(tok, "invalid double literal: " + text); return CelExpr.newBuilder().setId(nextId(tok)).build(); @@ -1027,7 +989,7 @@ private CelExpr parseStringLiteral() { String value = getTokenText(tok); try { CelConstant constExpr = Constants.parseString(value); - return CelExpr.newBuilder().setId(nextId(tok)).setConstant(constExpr).build(); + return CelExpr.ofConstant(nextId(tok), constExpr); } catch (ParseException e) { reportError(tok.start, e.getMessage()); return CelExpr.newBuilder().setId(nextId(tok)).build(); @@ -1039,7 +1001,7 @@ private CelExpr parseBytesLiteral() { String value = getTokenText(tok); try { CelConstant constExpr = Constants.parseBytes(value); - return CelExpr.newBuilder().setId(nextId(tok)).setConstant(constExpr).build(); + return CelExpr.ofConstant(nextId(tok), constExpr); } catch (ParseException e) { reportError(tok.start, e.getMessage()); return CelExpr.newBuilder().setId(nextId(tok)).build(); @@ -1084,52 +1046,53 @@ private static boolean isAsciiAlphanumeric(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'); } - private Optional extractStructName(CelExpr expr) { + private @Nullable String extractStructName(CelExpr expr) { if (expr.exprKind().getKind() == CelExpr.ExprKind.Kind.IDENT) { String name = expr.ident().name(); eraseId(expr.id()); - return Optional.of(name); + return name; } if (expr.exprKind().getKind() == CelExpr.ExprKind.Kind.SELECT) { if (expr.select().testOnly()) { - return Optional.empty(); + return null; } CelExpr operand = expr.select().operand(); eraseId(expr.id()); - return extractStructName(operand).map(prefix -> prefix + "." + expr.select().field()); + String prefix = extractStructName(operand); + return prefix != null ? prefix + "." + expr.select().field() : null; } - return Optional.empty(); + return null; } private int getLeftmostPosition(CelExpr expr) { - if (expr.exprKind().getKind() == CelExpr.ExprKind.Kind.IDENT) { - return positions.getOrDefault(expr.id(), 0); - } - if (expr.exprKind().getKind() == CelExpr.ExprKind.Kind.SELECT) { - return getLeftmostPosition(expr.select().operand()); + while (expr.exprKind().getKind() == CelExpr.ExprKind.Kind.SELECT) { + expr = expr.select().operand(); } return positions.getOrDefault(expr.id(), 0); } - private Optional lookupMacro(String id, int argCount, boolean receiverStyle) { + private @Nullable CelMacro lookupMacro(String id, int argCount, boolean receiverStyle) { + if (macros.isEmpty()) { + return null; + } String key = CelMacro.formatKey(id, argCount, receiverStyle); CelMacro macro = macros.get(key); if (macro != null) { - return Optional.of(macro); + return macro; } key = CelMacro.formatVarArgKey(id, receiverStyle); - return Optional.ofNullable(macros.get(key)); + return macros.get(key); } private Optional tryExpandMacro( long exprId, String function, @Nullable CelExpr target, ImmutableList args) { - if (function.isEmpty()) { + if (function.isEmpty() || macros.isEmpty()) { return Optional.empty(); } boolean isReceiver = (target != null); int argCount = args.size(); - Optional macro = lookupMacro(function, argCount, isReceiver); - if (!macro.isPresent()) { + CelMacro macro = lookupMacro(function, argCount, isReceiver); + if (macro == null) { return Optional.empty(); } if (nodeLimitExceeded) { @@ -1139,15 +1102,14 @@ private Optional tryExpandMacro( return Optional.empty(); } - Optional errorArg = args.stream().filter(ERROR::equals).findAny(); - if (errorArg.isPresent() || (target != null && target.equals(ERROR))) { + if ((target != null && target.equals(ERROR)) || hasError(args)) { eraseId(exprId); return Optional.of(ERROR); } int macroPosition = positions.getOrDefault(exprId, 0); CelExpr targetExpr = (target != null ? target : CelExpr.newBuilder().build()); - Optional expandedExpr = expandMacro(macroPosition, macro.get(), targetExpr, args); + Optional expandedExpr = expandMacro(macroPosition, macro, targetExpr, args); if (expandedExpr.isPresent()) { if (options.populateMacroCalls()) { @@ -1159,8 +1121,20 @@ private Optional tryExpandMacro( return Optional.empty(); } + private static boolean hasError(List args) { + for (int i = 0; i < args.size(); i++) { + if (args.get(i).equals(ERROR)) { + return true; + } + } + return false; + } + private Optional expandMacro( int position, CelMacro macro, CelExpr target, ImmutableList arguments) { + if (macroExprFactory == null) { + macroExprFactory = new PrattMacroExprFactory(); + } macroExprFactory.pushPosition(position); try { return macro.getExpander().expandMacro(macroExprFactory, target, arguments); @@ -1171,6 +1145,9 @@ private Optional expandMacro( private void recordMacroCall( long macroId, String function, CelExpr target, ImmutableList args) { + if (!(macroCalls instanceof HashMap)) { + macroCalls = new HashMap<>(); + } CelExpr.CelCall.Builder callExpr = CelExpr.CelCall.newBuilder().setFunction(function); if (target != null) { if (macroCalls.containsKey(target.id())) { @@ -1205,6 +1182,24 @@ private int countGroupingParentheses() { return 0; } + // Fast path: if the next non-whitespace character is not '(', leading open parens is 1. + int pos = peekToken.end; + while (pos < source.getContent().size()) { + int c = source.getContent().get(pos); + if (c != ' ' && c != '\t' && c != '\n' && c != '\r' && c != '\f' && c != 11) { + if (c == '/') { + // A comment might precede another '('. + break; + } + if (c == '(') { + break; + } + // Next significant token is definitely not '('. + return 1; + } + pos++; + } + int savedPos = lexer.savePosition(); try { int leadingOpenParens = 1; diff --git a/parser/src/test/java/dev/cel/parser/CelParserImplTest.java b/parser/src/test/java/dev/cel/parser/CelParserImplTest.java index 37501ec29..5b7f9defb 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserImplTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserImplTest.java @@ -17,6 +17,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; @@ -27,6 +28,7 @@ import dev.cel.common.CelValidationException; import dev.cel.common.CelValidationResult; import dev.cel.common.ast.CelExpr; +import java.util.Collections; import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -396,4 +398,14 @@ public void toParserBuilder_collectionProperties_copied() { assertThat(newParserBuilder.getMacros()).hasSize(1); assertThat(newParserBuilder.getParserLibraries().build()).hasSize(1); } + + @Test + public void parse_logicalChainLongerThanInitialCapacity_succeeds() { + CelParser parser = newParserBuilder().build(); + for (int operands = 2; operands <= 64; operands++) { + String expr = Joiner.on(" || ").join(Collections.nCopies(operands, "true")); + CelValidationResult result = parser.parse(expr); + assertThat(result.hasError()).isFalse(); + } + } } diff --git a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java index 7e19e24f8..0f9fb36b7 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java @@ -372,6 +372,7 @@ public void parser_core_syntax() { runTest("a || b || c || d || e || f"); runTest("a < 5 || a > 10"); runTest("a && b && c && d || e && f && g && h"); + runTest("a || b && c || d && e || f && g || h && i || j && k || l"); // Conditional operator runTest("a?b:c"); diff --git a/parser/src/test/java/dev/cel/parser/PrattParserTest.java b/parser/src/test/java/dev/cel/parser/PrattParserTest.java index e9394b362..710ff2fcf 100644 --- a/parser/src/test/java/dev/cel/parser/PrattParserTest.java +++ b/parser/src/test/java/dev/cel/parser/PrattParserTest.java @@ -295,6 +295,7 @@ public void pratt_parser_core_syntax() { runTest("a || b || c || d || e || f"); runTest("a < 5 || a > 10"); runTest("a && b && c && d || e && f && g && h"); + runTest("a || b && c || d && e || f && g || h && i || j && k || l"); // Conditional operator runTest("a?b:c"); diff --git a/parser/src/test/resources/parser_core_syntax.baseline b/parser/src/test/resources/parser_core_syntax.baseline index 7c05685f3..34997c9d8 100644 --- a/parser/src/test/resources/parser_core_syntax.baseline +++ b/parser/src/test/resources/parser_core_syntax.baseline @@ -1061,6 +1061,77 @@ L: _||_( )^#12[1,27]# )^#8[1,17]# +I: a || b && c || d && e || f && g || h && i || j && k || l +=====> +P: _||_( + _||_( + _||_( + a^#1:Expr.Ident#, + _&&_( + b^#3:Expr.Ident#, + c^#5:Expr.Ident# + )^#4:Expr.Call# + )^#2:Expr.Call#, + _||_( + _&&_( + d^#7:Expr.Ident#, + e^#9:Expr.Ident# + )^#8:Expr.Call#, + _&&_( + f^#11:Expr.Ident#, + g^#13:Expr.Ident# + )^#12:Expr.Call# + )^#10:Expr.Call# + )^#6:Expr.Call#, + _||_( + _||_( + _&&_( + h^#15:Expr.Ident#, + i^#17:Expr.Ident# + )^#16:Expr.Call#, + _&&_( + j^#19:Expr.Ident#, + k^#21:Expr.Ident# + )^#20:Expr.Call# + )^#18:Expr.Call#, + l^#23:Expr.Ident# + )^#22:Expr.Call# +)^#14:Expr.Call# +L: _||_( + _||_( + _||_( + a^#1[1,0]#, + _&&_( + b^#3[1,5]#, + c^#5[1,10]# + )^#4[1,7]# + )^#2[1,2]#, + _||_( + _&&_( + d^#7[1,15]#, + e^#9[1,20]# + )^#8[1,17]#, + _&&_( + f^#11[1,25]#, + g^#13[1,30]# + )^#12[1,27]# + )^#10[1,22]# + )^#6[1,12]#, + _||_( + _||_( + _&&_( + h^#15[1,35]#, + i^#17[1,40]# + )^#16[1,37]#, + _&&_( + j^#19[1,45]#, + k^#21[1,50]# + )^#20[1,47]# + )^#18[1,42]#, + l^#23[1,55]# + )^#22[1,52]# +)^#14[1,32]# + I: a?b:c =====> P: _?_:_( diff --git a/parser/src/test/resources/pratt_parser_core_syntax.baseline b/parser/src/test/resources/pratt_parser_core_syntax.baseline index 02f44e87c..fb9d94e58 100644 --- a/parser/src/test/resources/pratt_parser_core_syntax.baseline +++ b/parser/src/test/resources/pratt_parser_core_syntax.baseline @@ -1043,6 +1043,77 @@ L: _||_( )^#12[1,27]# )^#8[1,17]# +I: a || b && c || d && e || f && g || h && i || j && k || l +=====> +P: _||_( + _||_( + _||_( + a^#1:Expr.Ident#, + _&&_( + b^#3:Expr.Ident#, + c^#5:Expr.Ident# + )^#4:Expr.Call# + )^#2:Expr.Call#, + _||_( + _&&_( + d^#7:Expr.Ident#, + e^#9:Expr.Ident# + )^#8:Expr.Call#, + _&&_( + f^#11:Expr.Ident#, + g^#13:Expr.Ident# + )^#12:Expr.Call# + )^#10:Expr.Call# + )^#6:Expr.Call#, + _||_( + _||_( + _&&_( + h^#15:Expr.Ident#, + i^#17:Expr.Ident# + )^#16:Expr.Call#, + _&&_( + j^#19:Expr.Ident#, + k^#21:Expr.Ident# + )^#20:Expr.Call# + )^#18:Expr.Call#, + l^#23:Expr.Ident# + )^#22:Expr.Call# +)^#14:Expr.Call# +L: _||_( + _||_( + _||_( + a^#1[1,0]#, + _&&_( + b^#3[1,5]#, + c^#5[1,10]# + )^#4[1,7]# + )^#2[1,2]#, + _||_( + _&&_( + d^#7[1,15]#, + e^#9[1,20]# + )^#8[1,17]#, + _&&_( + f^#11[1,25]#, + g^#13[1,30]# + )^#12[1,27]# + )^#10[1,22]# + )^#6[1,12]#, + _||_( + _||_( + _&&_( + h^#15[1,35]#, + i^#17[1,40]# + )^#16[1,37]#, + _&&_( + j^#19[1,45]#, + k^#21[1,50]# + )^#20[1,47]# + )^#18[1,42]#, + l^#23[1,55]# + )^#22[1,52]# +)^#14[1,32]# + I: a?b:c =====> P: _?_:_( From 6124cb04c68b6079b692ef5fdaf7b6cc865a8b55 Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Mon, 14 Sep 2026 12:21:23 -0700 Subject: [PATCH 198/204] Avoid intermediate slice views when reading token text Lexing and parsing turn code point ranges into strings constantly: once for every identifier, keyword and literal. Both call sites spelled this as slice(i, j).toString(), which allocates an intermediate CelCodePointArray view solely to copy out of it and then discard it. Add CelCodePointArray.substring(i, j), which builds the String straight from the backing array, and implement it in each of the four subclasses. toString() becomes final and delegates to substring(0, size()), so the subclasses lose their near-duplicate toString() overrides. Lexer.consumeIdent and PrattParser.getTokenText call the new method. This removes exactly one 32-byte object per token whose text is materialized. Measured with CelParserBenchmark (parseOnly, built -c opt), comparing three parsers back to back in one session: ANTLR, the Pratt parser before this change, and the Pratt parser after it. Objects allocated per parse: | Case | ANTLR | Pratt before | Pratt after | Pratt vs ANTLR | Delta this CL | | :--- | ---: | ---: | ---: | ---: | ---: | | SMOKE_TEST | 357 | 123 | 120 | 3.0x smaller | -2.4% | | CHAINED_ORS | 968 | 349 | 339 | 2.9x smaller | -2.9% | | LIST_COMPREHENSION | 512 | 166 | 160 | 3.2x smaller | -3.6% | | MESSAGE_CREATION | 1,253 | 426 | 406 | 3.1x smaller | -4.7% | | LONG_LIST | 81,794 | 19,265 | 18,263 | 4.5x smaller | -5.2% | Bytes allocated per parse: | Case | ANTLR | Pratt before | Pratt after | Pratt vs ANTLR | Delta this CL | | :--- | ---: | ---: | ---: | ---: | ---: | | SMOKE_TEST | 12,256 | 3,608 | 3,512 | 3.5x smaller | -2.7% | | CHAINED_ORS | 32,160 | 9,912 | 9,592 | 3.4x smaller | -3.2% | | LIST_COMPREHENSION | 17,320 | 4,928 | 4,736 | 3.7x smaller | -3.9% | | MESSAGE_CREATION | 43,128 | 13,056 | 12,416 | 3.5x smaller | -4.9% | | LONG_LIST | 2,907,488 | 563,952 | 531,888 | 5.5x smaller | -5.7% | Wall clock, mean of 3 caliper trial medians: | Case | ANTLR | Pratt before | Pratt after | Pratt vs ANTLR | Delta this CL | | :--- | ---: | ---: | ---: | ---: | ---: | | SMOKE_TEST | 4,940 ns | 696 ns | 684 ns | 7.2x faster | -1.8% | | CHAINED_ORS | 14,641 ns | 2,008 ns | 2,006 ns | 7.3x faster | -0.1% | | LIST_COMPREHENSION | 7,514 ns | 1,250 ns | 1,217 ns | 6.2x faster | -2.7% | | MESSAGE_CREATION | 20,979 ns | 3,526 ns | 3,514 ns | 6.0x faster | -0.4% | | LONG_LIST | 1,616,631 ns | 140,500 ns | 146,640 ns | 11.0x faster | +4.4% | Wall clock is unchanged within measurement noise. The per-case deltas run from -2.7% to +4.4% and straddle zero, which is what a change that removes 3-5% of allocations and no actual work should look like. The LONG_LIST row reads as a regression, but that trial was noisy (per-trial medians 144.6us, 155.7us, 139.6us, against a much tighter 142.1us, 141.1us, 138.3us before) and its fastest observed parse, 134.6us, is below the 135.7us baseline. The win here is allocation volume and the GC pressure that follows from it. This is the first in a series of parser changes; the wall-clock improvements come later in that series. The ANTLR column is included for scale, and shows why the Pratt parser exists. ANTLR is slow enough on LONG_LIST that the case exceeds caliper's default 5 minute per-trial budget and has to be measured with a raised --time-limit. PiperOrigin-RevId: 981290051 --- .../common/internal/BasicCodePointArray.java | 11 +-- .../common/internal/CelCodePointArray.java | 12 ++- .../common/internal/EmptyCodePointArray.java | 13 +-- .../common/internal/Latin1CodePointArray.java | 11 +-- .../internal/SupplementalCodePointArray.java | 11 +-- .../internal/CelCodePointArrayTest.java | 88 +++++++++++++++++++ .../src/main/java/dev/cel/parser/Lexer.java | 2 +- .../main/java/dev/cel/parser/PrattParser.java | 2 +- 8 files changed, 127 insertions(+), 23 deletions(-) diff --git a/common/src/main/java/dev/cel/common/internal/BasicCodePointArray.java b/common/src/main/java/dev/cel/common/internal/BasicCodePointArray.java index a54fb65d7..482a4884f 100644 --- a/common/src/main/java/dev/cel/common/internal/BasicCodePointArray.java +++ b/common/src/main/java/dev/cel/common/internal/BasicCodePointArray.java @@ -58,13 +58,14 @@ public BasicCodePointArray slice(int i, int j) { } @Override - public int get(int index) { - checkElementIndex(index, size()); - return codePoints()[offset() + index] & 0xffff; + public String substring(int i, int j) { + checkPositionIndexes(i, j, size()); + return new String(codePoints(), offset() + i, j - i); } @Override - public final String toString() { - return new String(codePoints(), offset(), size()); + public int get(int index) { + checkElementIndex(index, size()); + return codePoints()[offset() + index] & 0xffff; } } diff --git a/common/src/main/java/dev/cel/common/internal/CelCodePointArray.java b/common/src/main/java/dev/cel/common/internal/CelCodePointArray.java index 1f3124c93..a50ce0eea 100644 --- a/common/src/main/java/dev/cel/common/internal/CelCodePointArray.java +++ b/common/src/main/java/dev/cel/common/internal/CelCodePointArray.java @@ -36,6 +36,14 @@ public abstract class CelCodePointArray { /** Returns a new {@link CelCodePointArray} that is a subview of this between [i, j). */ public abstract CelCodePointArray slice(int i, int j); + /** + * Returns the code points between [i, j) as a string. + * + *

Equivalent to {@code slice(i, j).toString()}, but does not materialize the intermediate + * view. Lexing and parsing call this for every literal and identifier. + */ + public abstract String substring(int i, int j); + /** Get the code point at the given index. */ public abstract int get(int index); @@ -55,7 +63,9 @@ public boolean isEmpty() { } @Override - public abstract String toString(); + public final String toString() { + return substring(0, size()); + } public static CelCodePointArray fromString(String text) { if (isNullOrEmpty(text)) { diff --git a/common/src/main/java/dev/cel/common/internal/EmptyCodePointArray.java b/common/src/main/java/dev/cel/common/internal/EmptyCodePointArray.java index 8bca7bf31..32434b02c 100644 --- a/common/src/main/java/dev/cel/common/internal/EmptyCodePointArray.java +++ b/common/src/main/java/dev/cel/common/internal/EmptyCodePointArray.java @@ -14,6 +14,8 @@ package dev.cel.common.internal; +import static com.google.common.base.Preconditions.checkPositionIndexes; + import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.DoNotCall; import com.google.errorprone.annotations.Immutable; @@ -51,6 +53,12 @@ public int get(int index) { String.format("index (%s) must not be greater than size (0)", index)); } + @Override + public String substring(int i, int j) { + checkPositionIndexes(i, j, 0); + return ""; + } + @Override public int size() { return 0; @@ -60,9 +68,4 @@ public int size() { public ImmutableList lineOffsets() { return ImmutableList.of(1); } - - @Override - public String toString() { - return ""; - } } diff --git a/common/src/main/java/dev/cel/common/internal/Latin1CodePointArray.java b/common/src/main/java/dev/cel/common/internal/Latin1CodePointArray.java index 42cc0445c..1a35ef87f 100644 --- a/common/src/main/java/dev/cel/common/internal/Latin1CodePointArray.java +++ b/common/src/main/java/dev/cel/common/internal/Latin1CodePointArray.java @@ -58,13 +58,14 @@ public Latin1CodePointArray slice(int i, int j) { } @Override - public int get(int index) { - checkElementIndex(index, size()); - return Byte.toUnsignedInt(codePoints()[offset() + index]); + public String substring(int i, int j) { + checkPositionIndexes(i, j, size()); + return new String(codePoints(), offset() + i, j - i, ISO_8859_1); } @Override - public final String toString() { - return new String(codePoints(), offset(), size(), ISO_8859_1); + public int get(int index) { + checkElementIndex(index, size()); + return Byte.toUnsignedInt(codePoints()[offset() + index]); } } diff --git a/common/src/main/java/dev/cel/common/internal/SupplementalCodePointArray.java b/common/src/main/java/dev/cel/common/internal/SupplementalCodePointArray.java index 0c9214410..f66cbf64b 100644 --- a/common/src/main/java/dev/cel/common/internal/SupplementalCodePointArray.java +++ b/common/src/main/java/dev/cel/common/internal/SupplementalCodePointArray.java @@ -59,13 +59,14 @@ public SupplementalCodePointArray slice(int i, int j) { } @Override - public int get(int index) { - checkElementIndex(index, size()); - return codePoints()[offset() + index]; + public String substring(int i, int j) { + checkPositionIndexes(i, j, size()); + return new String(codePoints(), offset() + i, j - i); } @Override - public final String toString() { - return new String(codePoints(), offset(), size()); + public int get(int index) { + checkElementIndex(index, size()); + return codePoints()[offset() + index]; } } diff --git a/common/src/test/java/dev/cel/common/internal/CelCodePointArrayTest.java b/common/src/test/java/dev/cel/common/internal/CelCodePointArrayTest.java index 7cb02c5a8..9340b623b 100644 --- a/common/src/test/java/dev/cel/common/internal/CelCodePointArrayTest.java +++ b/common/src/test/java/dev/cel/common/internal/CelCodePointArrayTest.java @@ -15,6 +15,7 @@ package dev.cel.common.internal; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; @@ -39,6 +40,93 @@ public void computeLineOffset( .inOrder(); } + @Test + public void substring_empty() { + CelCodePointArray empty = CelCodePointArray.fromString(""); + assertThat(empty).isInstanceOf(EmptyCodePointArray.class); + + assertThat(empty.substring(0, 0)).isEmpty(); + assertThrows(IndexOutOfBoundsException.class, () -> empty.substring(0, 1)); + assertThrows(IndexOutOfBoundsException.class, () -> empty.substring(-1, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> empty.substring(1, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> empty.substring(1, 1)); + } + + @Test + public void substring_latin1() { + CelCodePointArray latin1 = CelCodePointArray.fromString("hello world"); + assertThat(latin1).isInstanceOf(Latin1CodePointArray.class); + + assertThat(latin1.substring(0, 5)).isEqualTo("hello"); + assertThat(latin1.substring(6, 11)).isEqualTo("world"); + assertThat(latin1.substring(0, 11)).isEqualTo("hello world"); + assertThat(latin1.substring(3, 3)).isEmpty(); + + assertThrows(IndexOutOfBoundsException.class, () -> latin1.substring(-1, 5)); + assertThrows(IndexOutOfBoundsException.class, () -> latin1.substring(0, 12)); + assertThrows(IndexOutOfBoundsException.class, () -> latin1.substring(5, 4)); + + // Test on a sliced subview to ensure bounds are checked against size(), not the backing buffer + // length + CelCodePointArray sliced = latin1.slice(1, 4); // "ell", size = 3, buffer length = 11 + assertThat(sliced.substring(0, 3)).isEqualTo("ell"); + assertThat(sliced.substring(1, 2)).isEqualTo("l"); + + assertThrows(IndexOutOfBoundsException.class, () -> sliced.substring(-1, 2)); + assertThrows(IndexOutOfBoundsException.class, () -> sliced.substring(0, 4)); + assertThrows(IndexOutOfBoundsException.class, () -> sliced.substring(2, 1)); + } + + @Test + public void substring_basic() { + CelCodePointArray basic = CelCodePointArray.fromString("abc \uff20 def"); + assertThat(basic).isInstanceOf(BasicCodePointArray.class); + + assertThat(basic.substring(0, 3)).isEqualTo("abc"); + assertThat(basic.substring(4, 5)).isEqualTo("\uff20"); + assertThat(basic.substring(6, 9)).isEqualTo("def"); + assertThat(basic.substring(0, 9)).isEqualTo("abc \uff20 def"); + assertThat(basic.substring(3, 3)).isEmpty(); + + assertThrows(IndexOutOfBoundsException.class, () -> basic.substring(-1, 5)); + assertThrows(IndexOutOfBoundsException.class, () -> basic.substring(0, 10)); + assertThrows(IndexOutOfBoundsException.class, () -> basic.substring(5, 4)); + + // Test on a sliced subview to ensure bounds are checked against size(), not the backing buffer + // length + CelCodePointArray sliced = basic.slice(1, 5); // "bc \uff20", size = 4, buffer length = 9 + assertThat(sliced.substring(0, 4)).isEqualTo("bc \uff20"); + + assertThrows(IndexOutOfBoundsException.class, () -> sliced.substring(-1, 2)); + assertThrows(IndexOutOfBoundsException.class, () -> sliced.substring(0, 5)); + assertThrows(IndexOutOfBoundsException.class, () -> sliced.substring(2, 1)); + } + + @Test + public void substring_supplemental() { + CelCodePointArray supp = CelCodePointArray.fromString(" text 가나다 😦😁😑 "); + assertThat(supp).isInstanceOf(SupplementalCodePointArray.class); + + assertThat(supp.substring(0, 5)).isEqualTo(" text"); + assertThat(supp.substring(10, 13)).isEqualTo("😦😁😑"); + assertThat(supp.substring(0, supp.size())).isEqualTo(" text 가나다 😦😁😑 "); + assertThat(supp.substring(3, 3)).isEmpty(); + + assertThrows(IndexOutOfBoundsException.class, () -> supp.substring(-1, 5)); + int greaterThanSize = supp.size() + 1; + assertThrows(IndexOutOfBoundsException.class, () -> supp.substring(0, greaterThanSize)); + assertThrows(IndexOutOfBoundsException.class, () -> supp.substring(5, 4)); + + // Test on a sliced subview to ensure bounds are checked against size(), not the backing buffer + // length + CelCodePointArray sliced = supp.slice(1, 5); // "text", size = 4, buffer length = 15 + assertThat(sliced.substring(0, 4)).isEqualTo("text"); + + assertThrows(IndexOutOfBoundsException.class, () -> sliced.substring(-1, 2)); + assertThrows(IndexOutOfBoundsException.class, () -> sliced.substring(0, 5)); + assertThrows(IndexOutOfBoundsException.class, () -> sliced.substring(2, 1)); + } + @AutoValue abstract static class LineOffsetTestCase { abstract String text(); diff --git a/parser/src/main/java/dev/cel/parser/Lexer.java b/parser/src/main/java/dev/cel/parser/Lexer.java index 32b6ebfab..602a6ef00 100644 --- a/parser/src/main/java/dev/cel/parser/Lexer.java +++ b/parser/src/main/java/dev/cel/parser/Lexer.java @@ -635,7 +635,7 @@ private Token consumeIdent() { position++; } int end = position; - String word = content.slice(start, end).toString(); + String word = content.substring(start, end); TokenType keywordType = KEYWORDS.get(word); if (keywordType != null) { return makeToken(keywordType, start, end); diff --git a/parser/src/main/java/dev/cel/parser/PrattParser.java b/parser/src/main/java/dev/cel/parser/PrattParser.java index 3ddb0baad..dceaadad6 100644 --- a/parser/src/main/java/dev/cel/parser/PrattParser.java +++ b/parser/src/main/java/dev/cel/parser/PrattParser.java @@ -198,7 +198,7 @@ private String getTokenText(Lexer.Token tok) { return tok.text; } if (tok.start >= 0 && tok.end >= tok.start && tok.end <= source.getContent().size()) { - return source.getContent().slice(tok.start, tok.end).toString(); + return source.getContent().substring(tok.start, tok.end); } return ""; } From c9f8f60a9d0c17f4dc5a682ad827513a4984fd62 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Mon, 14 Sep 2026 15:23:40 -0700 Subject: [PATCH 199/204] Add async concurrency gate PiperOrigin-RevId: 981393659 --- runtime/planner/BUILD.bazel | 7 + .../dev/cel/runtime/planner/AsyncGate.java | 117 +++++++++ .../java/dev/cel/runtime/planner/BUILD.bazel | 13 + .../cel/runtime/planner/AsyncGateTest.java | 245 ++++++++++++++++++ .../java/dev/cel/runtime/planner/BUILD.bazel | 1 + 5 files changed, 383 insertions(+) create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/AsyncGate.java create mode 100644 runtime/src/test/java/dev/cel/runtime/planner/AsyncGateTest.java diff --git a/runtime/planner/BUILD.bazel b/runtime/planner/BUILD.bazel index 860d413a0..2781a2e22 100644 --- a/runtime/planner/BUILD.bazel +++ b/runtime/planner/BUILD.bazel @@ -21,3 +21,10 @@ java_library( visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime/planner:planned_program"], ) + +java_library( + name = "async_gate", + testonly = 1, + visibility = ["//:internal"], + exports = ["//runtime/src/main/java/dev/cel/runtime/planner:async_gate"], +) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/AsyncGate.java b/runtime/src/main/java/dev/cel/runtime/planner/AsyncGate.java new file mode 100644 index 000000000..43c6c3ea4 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/AsyncGate.java @@ -0,0 +1,117 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import com.google.errorprone.annotations.CheckReturnValue; +import javax.annotation.concurrent.ThreadSafe; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.jspecify.annotations.Nullable; + +/** + * Regulates the number of concurrent asynchronous function executions based on maxConcurrency. + * + *

A {@code maxConcurrency} value of {@code 0} or less represents unbounded concurrency (no limit + * on concurrent executions). + */ +@ThreadSafe +final class AsyncGate { + + /** Null when {@code maxConcurrency <= 0}, indicating unbounded concurrency (no throttling). */ + private final @Nullable Semaphore semaphore; + + private final AtomicInteger activeCalls; + private final AtomicBoolean cancelled; + + /** + * Creates an {@link AsyncGate} regulating concurrent asynchronous calls. + * + * @param maxConcurrency the maximum number of concurrent executions allowed. A value of {@code 0} + * or less indicates unbounded concurrency (no concurrency limit). + */ + static AsyncGate create(int maxConcurrency) { + return new AsyncGate(maxConcurrency); + } + + /** + * Attempts to acquire a concurrency slot for an asynchronous call. + * + *

Cancellation check is best-effort admission control. A thread may observe {@code + * cancelled.get() == false} and acquire a permit immediately before a concurrent {@link + * #cancel()} runs. Any call launched in this race window will complete safely into {@code + * AsyncCompletionCoordinator.callCompleted()}, where permits are released and results discarded. + * + * @return true if a slot was acquired; false if the gate is cancelled or at maximum concurrency. + */ + @CheckReturnValue + boolean tryAcquire() { + if (semaphore != null && !semaphore.tryAcquire()) { + return false; + } + // Best-effort check: if cancelled concurrently after this point, the launched task + // will complete as a no-op in the completion coordinator. + if (cancelled.get()) { + if (semaphore != null) { + semaphore.release(); + } + return false; + } + activeCalls.incrementAndGet(); + return true; + } + + /** Releases a previously acquired concurrency slot and decrements the active call count. */ + void release() { + while (true) { + int current = activeCalls.get(); + if (current <= 0) { + return; + } + if (activeCalls.compareAndSet(current, current - 1)) { + if (semaphore != null) { + semaphore.release(); + } + return; + } + } + } + + /** + * Cancels the gate, preventing future calls from acquiring permits. + * + *

Cancellation is best-effort admission control; tasks that acquired permits immediately prior + * to cancellation will execute and complete as no-ops in the completion coordinator. + */ + void cancel() { + cancelled.set(true); + } + + /** Returns true if the gate has been cancelled. */ + boolean isCancelled() { + return cancelled.get(); + } + + /** Returns the current number of active in-flight calls. */ + int activeCount() { + return activeCalls.get(); + } + + private AsyncGate(int maxConcurrency) { + this.semaphore = maxConcurrency > 0 ? new Semaphore(maxConcurrency) : null; + this.activeCalls = new AtomicInteger(); + this.cancelled = new AtomicBoolean(false); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index d4dbb1659..74d7d8d41 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -187,6 +187,19 @@ java_library( ], ) +java_library( + name = "async_gate", + srcs = ["AsyncGate.java"], + tags = [ + ], + deps = [ + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + java_library( name = "activation_wrapper", srcs = ["ActivationWrapper.java"], diff --git a/runtime/src/test/java/dev/cel/runtime/planner/AsyncGateTest.java b/runtime/src/test/java/dev/cel/runtime/planner/AsyncGateTest.java new file mode 100644 index 000000000..8a5c6cc13 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/planner/AsyncGateTest.java @@ -0,0 +1,245 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class AsyncGateTest { + + @Test + public void tryAcquire_withAvailablePermits_returnsTrueAndIncrementsActiveCount() { + AsyncGate gate = AsyncGate.create(2); + + boolean firstAcquired = gate.tryAcquire(); + boolean secondAcquired = gate.tryAcquire(); + + assertThat(firstAcquired).isTrue(); + assertThat(secondAcquired).isTrue(); + assertThat(gate.activeCount()).isEqualTo(2); + } + + @Test + public void tryAcquire_atMaxConcurrency_returnsFalseAndDoesNotIncrementActiveCount() { + AsyncGate gate = AsyncGate.create(1); + assertThat(gate.tryAcquire()).isTrue(); + + boolean acquired = gate.tryAcquire(); + + assertThat(acquired).isFalse(); + assertThat(gate.activeCount()).isEqualTo(1); + } + + @Test + public void tryAcquire_unbounded_alwaysSucceeds() { + AsyncGate gate = AsyncGate.create(0); + + boolean first = gate.tryAcquire(); + boolean second = gate.tryAcquire(); + boolean third = gate.tryAcquire(); + + assertThat(first).isTrue(); + assertThat(second).isTrue(); + assertThat(third).isTrue(); + assertThat(gate.activeCount()).isEqualTo(3); + } + + @Test + public void tryAcquire_negativeMaxConcurrency_treatedAsUnbounded() { + AsyncGate gate = AsyncGate.create(-1); + + boolean acquired = gate.tryAcquire(); + + assertThat(acquired).isTrue(); + assertThat(gate.activeCount()).isEqualTo(1); + } + + @Test + public void tryAcquire_whenCancelled_returnsFalse() { + AsyncGate gate = AsyncGate.create(2); + gate.cancel(); + + boolean acquired = gate.tryAcquire(); + + assertThat(acquired).isFalse(); + assertThat(gate.activeCount()).isEqualTo(0); + } + + @Test + public void tryAcquire_unboundedWhenCancelled_returnsFalse() { + AsyncGate gate = AsyncGate.create(0); + gate.cancel(); + + boolean acquired = gate.tryAcquire(); + + assertThat(acquired).isFalse(); + assertThat(gate.activeCount()).isEqualTo(0); + } + + @Test + public void release_decrementsActiveCountAndFreesPermit() { + AsyncGate gate = AsyncGate.create(2); + assertThat(gate.tryAcquire()).isTrue(); + assertThat(gate.tryAcquire()).isTrue(); + + gate.release(); + + assertThat(gate.activeCount()).isEqualTo(1); + assertThat(gate.tryAcquire()).isTrue(); + } + + @Test + public void release_unbounded_decrementsActiveCount() { + AsyncGate gate = AsyncGate.create(0); + assertThat(gate.tryAcquire()).isTrue(); + + gate.release(); + + assertThat(gate.activeCount()).isEqualTo(0); + } + + @Test + public void release_allowsSubsequentTryAcquire() { + AsyncGate gate = AsyncGate.create(1); + assertThat(gate.tryAcquire()).isTrue(); + + gate.release(); + + assertThat(gate.tryAcquire()).isTrue(); + assertThat(gate.activeCount()).isEqualTo(1); + } + + @Test + public void release_withoutPriorAcquire_doesNotExceedMaxConcurrency() { + AsyncGate gate = AsyncGate.create(2); + + gate.release(); + + assertThat(gate.activeCount()).isEqualTo(0); + assertThat(gate.tryAcquire()).isTrue(); + assertThat(gate.tryAcquire()).isTrue(); + assertThat(gate.tryAcquire()).isFalse(); + } + + @Test + public void release_withoutPriorAcquire_doesNotUnderflowActiveCount() { + AsyncGate gate = AsyncGate.create(0); + + gate.release(); + gate.release(); + + assertThat(gate.activeCount()).isEqualTo(0); + } + + @Test + public void release_calledMoreThanAcquires_onlyReleasesAcquiredPermits() { + AsyncGate gate = AsyncGate.create(1); + assertThat(gate.tryAcquire()).isTrue(); + + gate.release(); + gate.release(); + + assertThat(gate.activeCount()).isEqualTo(0); + assertThat(gate.tryAcquire()).isTrue(); + assertThat(gate.tryAcquire()).isFalse(); + } + + @Test + public void cancel_setsIsCancelledToTrue() { + AsyncGate gate = AsyncGate.create(1); + + gate.cancel(); + + assertThat(gate.isCancelled()).isTrue(); + } + + @Test + public void cancel_idempotent() { + AsyncGate gate = AsyncGate.create(1); + + gate.cancel(); + gate.cancel(); + + assertThat(gate.isCancelled()).isTrue(); + } + + @Test + public void create_factoryMethod_returnsConfiguredGate() { + AsyncGate gate = AsyncGate.create(5); + + assertThat(gate.activeCount()).isEqualTo(0); + assertThat(gate.isCancelled()).isFalse(); + } + + @Test + public void create_withMaxInteger_initializesCorrectly() { + AsyncGate gate = AsyncGate.create(Integer.MAX_VALUE); + + assertThat(gate.tryAcquire()).isTrue(); + assertThat(gate.activeCount()).isEqualTo(1); + } + + @Test + public void concurrentTryAcquireAndRelease_neverExceedsMaxConcurrency() + throws InterruptedException { + int maxConcurrency = 4; + int taskCount = 32; + AsyncGate gate = AsyncGate.create(maxConcurrency); + AtomicInteger peakConcurrency = new AtomicInteger(); + ExecutorService executor = Executors.newFixedThreadPool(8); + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch doneLatch = new CountDownLatch(taskCount); + + try { + for (int i = 0; i < taskCount; i++) { + executor.execute( + () -> { + try { + startLatch.await(); + while (!gate.tryAcquire()) { + Thread.sleep(1); + } + int current = gate.activeCount(); + peakConcurrency.accumulateAndGet(current, Math::max); + Thread.sleep(2); + gate.release(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + doneLatch.countDown(); + } + }); + } + + startLatch.countDown(); + boolean completed = doneLatch.await(5, SECONDS); + + assertThat(completed).isTrue(); + assertThat(peakConcurrency.get()).isAtMost(maxConcurrency); + assertThat(gate.activeCount()).isEqualTo(0); + } finally { + executor.shutdown(); + } + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel index 9116818dc..5ff4b4d81 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel @@ -49,6 +49,7 @@ java_library( "//runtime:runtime_helpers", "//runtime:standard_functions", "//runtime:unknown_attributes", + "//runtime/planner:async_gate", "//runtime/planner:program_planner", "//runtime/standard:type", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", From cf1292b5d7c04b6cfa11095de689f51479f267d6 Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Mon, 14 Sep 2026 16:57:22 -0700 Subject: [PATCH 200/204] Remove throwaway builders, boxed positions and eager maps from the parse path Four related allocation reductions. They land together because the last two are coupled: the parser's new one-shot position map only pays off once the source builder can adopt an immutable map instead of copying it. CelExpr. Every newBuilder() allocated a fresh CelNotSet plus an ExprKind wrapper purely to be overwritten a moment later; those become shared NOT_SET_KIND and NOT_SET_EXPR singletons. CelSelect paid that once per node and CelComprehension five times. addArgs, addElements and addEntries used Arrays.asList(...) followed by forEach(list::add), allocating a list view and a capturing lambda per call, and now use Collections.addAll and Iterables.addAll. The ofCall, ofList, ofStruct and ofMap factories hand an already-immutable collection straight to the value class rather than copying it through the builder's mutable ArrayList. PrattParser. Builds calls, lists, maps and structs through those factories, which also shortens the call sites. The expression position map becomes an int[] indexed by expression id rather than a Map; ids are dense and handed out sequentially by nextId, so this drops two boxed objects and a hash insert per node. copyPositionsTo then builds the map once, presized. The source content is cached in a field rather than re-fetched through the accessor, and nextSignificantToken loses a loop over WHITESPACE and COMMENT tokens that could never iterate, since Lexer.lex() consumes those internally and never emits them. CelSource.Builder. positions and macroCalls start as empty immutable maps and are copied into a HashMap only when something actually mutates them. Two builders exist per parse, so this removes up to four hash maps. addPositionsMap and addAllMacroCalls adopt an already-immutable argument outright while the builder is still pristine, which makes build()'s copyOf a no-op and removes the second copy of the parser's position map. extensions is created lazily, and addAllExtensions short-circuits on an empty argument, which matters because toBuilder() always calls it. CelValidationResult. Hoists its issue comparator into a constant and replaces a stream().anyMatch(...) with an indexed loop. Every successful parse paid both. Measured with CelParserBenchmark (parseOnly, built -c opt). "Pratt before" is this CL's parent, so the last column is this CL's own contribution. The ANTLR column is the series baseline and is unaffected by any of these changes. Objects allocated per parse: | Case | ANTLR | Pratt before | Pratt after | Pratt vs ANTLR | Delta this CL | | :--- | ---: | ---: | ---: | ---: | ---: | | SMOKE_TEST | 357 | 120 | 70 | 5.1x smaller | -41.7% | | CHAINED_ORS | 968 | 339 | 201 | 4.8x smaller | -40.7% | | LIST_COMPREHENSION | 512 | 160 | 100 | 5.1x smaller | -37.5% | | MESSAGE_CREATION | 1,253 | 406 | 272 | 4.6x smaller | -33.0% | | LONG_LIST | 81,794 | 18,263 | 13,005 | 6.3x smaller | -28.8% | Bytes allocated per parse: | Case | ANTLR | Pratt before | Pratt after | Pratt vs ANTLR | Delta this CL | | :--- | ---: | ---: | ---: | ---: | ---: | | SMOKE_TEST | 12,256 | 3,512 | 2,040 | 6.0x smaller | -41.9% | | CHAINED_ORS | 32,160 | 9,592 | 5,656 | 5.7x smaller | -41.0% | | LIST_COMPREHENSION | 17,320 | 4,736 | 2,928 | 5.9x smaller | -38.2% | | MESSAGE_CREATION | 43,128 | 12,416 | 8,696 | 5.0x smaller | -30.0% | | LONG_LIST | 2,907,488 | 531,888 | 390,992 | 7.4x smaller | -26.5% | Wall clock, mean of 3 caliper trial medians: | Case | ANTLR | Pratt before | Pratt after | Pratt vs ANTLR | Delta this CL | | :--- | ---: | ---: | ---: | ---: | ---: | | SMOKE_TEST | 4,940 ns | 692 ns | 388 ns | 12.7x faster | -43.9% | | CHAINED_ORS | 14,641 ns | 2,055 ns | 1,111 ns | 13.2x faster | -45.9% | | LIST_COMPREHENSION | 7,514 ns | 1,231 ns | 797 ns | 9.4x faster | -35.3% | | MESSAGE_CREATION | 20,979 ns | 3,583 ns | 2,524 ns | 8.3x faster | -29.6% | | LONG_LIST | 1,616,631 ns | 142,764 ns | 117,823 ns | 13.7x faster | -17.5% | PiperOrigin-RevId: 981440440 --- .../main/java/dev/cel/common/CelSource.java | 72 ++++-- .../dev/cel/common/CelValidationResult.java | 17 +- .../main/java/dev/cel/common/ast/CelExpr.java | 106 +++++---- .../java/dev/cel/common/CelSourceTest.java | 35 +++ .../src/main/java/dev/cel/parser/BUILD.bazel | 1 - .../main/java/dev/cel/parser/PrattParser.java | 218 +++++++++--------- .../dev/cel/parser/CelParserImplTest.java | 83 ++++++- 7 files changed, 355 insertions(+), 177 deletions(-) diff --git a/common/src/main/java/dev/cel/common/CelSource.java b/common/src/main/java/dev/cel/common/CelSource.java index d64049f61..4ea2b7a1e 100644 --- a/common/src/main/java/dev/cel/common/CelSource.java +++ b/common/src/main/java/dev/cel/common/CelSource.java @@ -33,6 +33,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import org.jspecify.annotations.Nullable; /** Represents the source content of an expression and related metadata. */ @Immutable @@ -162,9 +163,13 @@ public static final class Builder { private final CelCodePointArray codePoints; private final List lineOffsets; - private final Map positions; - private final Map macroCalls; - private final ImmutableSet.Builder extensions; + // Both maps start out immutable and empty, and are only copied into a mutable map once they are + // actually modified. This keeps the common case (a source that is either never populated, or + // populated in bulk from an already immutable map) allocation free. + private Map positions; + private Map macroCalls; + // Null until the first extension is added; extensions are rare. + private ImmutableSet.@Nullable Builder extensions; private final boolean lineOffsetsAlreadyComputed; private String description; @@ -176,9 +181,8 @@ private Builder() { private Builder(CelCodePointArray codePoints, List lineOffsets) { this.codePoints = checkNotNull(codePoints); this.lineOffsets = checkNotNull(lineOffsets); - this.positions = new HashMap<>(); - this.macroCalls = new HashMap<>(); - this.extensions = ImmutableSet.builder(); + this.positions = ImmutableMap.of(); + this.macroCalls = ImmutableMap.of(); this.description = ""; this.lineOffsetsAlreadyComputed = !lineOffsets.isEmpty(); } @@ -207,39 +211,72 @@ public Builder addAllLineOffsets(Iterable lineOffsets) { return this; } + /** + * Returns a map containing every entry of {@code map} plus every entry of {@code additions}. + * + *

If {@code map} is still the empty immutable placeholder a builder starts with, and {@code + * additions} is already immutable, then {@code additions} is adopted as-is and no copy is made. + * That is the common case: a source populated in bulk exactly once, which lets {@link #build()} + * reuse the argument directly. Otherwise the entries are merged into a mutable copy. + */ + private static Map augmentedMap(Map map, Map additions) { + if (map instanceof ImmutableMap && map.isEmpty() && additions instanceof ImmutableMap) { + return additions; + } + Map merged = map instanceof HashMap ? map : new HashMap<>(map); + merged.putAll(additions); + return merged; + } + + private Map mutablePositions() { + if (!(positions instanceof HashMap)) { + positions = new HashMap<>(positions); + } + return positions; + } + @CanIgnoreReturnValue public Builder addPositionsMap(Map positionsMap) { checkNotNull(positionsMap); - this.positions.putAll(positionsMap); + positions = augmentedMap(positions, positionsMap); return this; } @CanIgnoreReturnValue public Builder addPositions(long exprId, int position) { - this.positions.put(exprId, position); + mutablePositions().put(exprId, position); return this; } @CanIgnoreReturnValue public Builder removePositions(long exprId) { - this.positions.remove(exprId); + if (positions.containsKey(exprId)) { + mutablePositions().remove(exprId); + } return this; } + private Map mutableMacroCalls() { + if (!(macroCalls instanceof HashMap)) { + macroCalls = new HashMap<>(macroCalls); + } + return macroCalls; + } + @CanIgnoreReturnValue public Builder addMacroCalls(long exprId, CelExpr expr) { - this.macroCalls.put(exprId, expr); + mutableMacroCalls().put(exprId, expr); return this; } @CanIgnoreReturnValue public Builder addAllMacroCalls(Map macroCalls) { - this.macroCalls.putAll(macroCalls); + this.macroCalls = augmentedMap(this.macroCalls, macroCalls); return this; } public ImmutableSet getExtensions() { - return extensions.build(); + return extensions == null ? ImmutableSet.of() : extensions.build(); } /** @@ -249,6 +286,9 @@ public ImmutableSet getExtensions() { @CanIgnoreReturnValue public Builder addAllExtensions(Iterable extensions) { checkNotNull(extensions); + if (this.extensions == null) { + this.extensions = ImmutableSet.builder(); + } this.extensions.addAll(extensions); return this; } @@ -287,14 +327,16 @@ public Optional getOffsetLocation(int offset) { return CelSourceHelper.getOffsetLocation(codePoints, offset); } + /** Returns a live, mutable view of the positions recorded so far. */ @CheckReturnValue public Map getPositionsMap() { - return this.positions; + return mutablePositions(); } + /** Returns a live, mutable view of the macro calls recorded so far. */ @CheckReturnValue public Map getMacroCalls() { - return macroCalls; + return mutableMacroCalls(); } @CheckReturnValue @@ -310,7 +352,7 @@ public CelSource build() { ImmutableList.copyOf(lineOffsets), ImmutableMap.copyOf(positions), ImmutableMap.copyOf(macroCalls), - extensions.build()); + getExtensions()); } } diff --git a/common/src/main/java/dev/cel/common/CelValidationResult.java b/common/src/main/java/dev/cel/common/CelValidationResult.java index 61152c493..f1f218336 100644 --- a/common/src/main/java/dev/cel/common/CelValidationResult.java +++ b/common/src/main/java/dev/cel/common/CelValidationResult.java @@ -22,6 +22,7 @@ import com.google.errorprone.annotations.Immutable; import com.google.errorprone.annotations.InlineMe; import dev.cel.common.annotations.Internal; +import java.util.Comparator; import org.jspecify.annotations.Nullable; /** @@ -31,6 +32,9 @@ @Immutable public final class CelValidationResult { + private static final Comparator BY_SOURCE_LOCATION = + comparing(CelIssue::getSourceLocation); + @SuppressWarnings("Immutable") private final @Nullable Throwable failure; @@ -64,11 +68,20 @@ private CelValidationResult( @Nullable Throwable failure) { this.ast = ast; this.source = source; - this.issues = ImmutableList.sortedCopyOf(comparing(CelIssue::getSourceLocation), issues); - this.hasError = issues.stream().anyMatch(CelValidationResult::issueIsError) || failure != null; + this.issues = ImmutableList.sortedCopyOf(BY_SOURCE_LOCATION, issues); + this.hasError = failure != null || containsError(issues); this.failure = failure; } + private static boolean containsError(ImmutableList issues) { + for (int i = 0; i < issues.size(); i++) { + if (issueIsError(issues.get(i))) { + return true; + } + } + return false; + } + /** * Returns the validated {@code CelAbstractSyntaxTree} if one exists. * diff --git a/common/src/main/java/dev/cel/common/ast/CelExpr.java b/common/src/main/java/dev/cel/common/ast/CelExpr.java index cac968686..0f238b63d 100644 --- a/common/src/main/java/dev/cel/common/ast/CelExpr.java +++ b/common/src/main/java/dev/cel/common/ast/CelExpr.java @@ -20,11 +20,13 @@ import com.google.auto.value.AutoOneOf; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Optional; /** @@ -38,6 +40,16 @@ @SuppressWarnings("unchecked") // Class ensures only the super type is used public abstract class CelExpr implements Expression { + /** + * Shared instance of the {@link ExprKind.Kind#NOT_SET} kind. {@link CelNotSet} carries no state, + * so a single instance can back every unset expression. + */ + private static final ExprKind NOT_SET_KIND = + AutoOneOf_CelExpr_ExprKind.notSet(new AutoValue_CelExpr_CelNotSet()); + + /** Shared instance of an expression with an unset kind and a zero id. */ + private static final CelExpr NOT_SET_EXPR = ofNotSet(0L); + @Override public abstract long id(); @@ -340,9 +352,7 @@ public Builder setComprehension(CelComprehension comprehension) { public abstract Builder toBuilder(); public static Builder newBuilder() { - return new AutoValue_CelExpr.Builder() - .setId(0) - .setExprKind(AutoOneOf_CelExpr_ExprKind.notSet(new AutoValue_CelExpr_CelNotSet())); + return new AutoValue_CelExpr.Builder().setId(0).setExprKind(NOT_SET_KIND); } /** Denotes the kind of the expression. An expression can only be of one kind. */ @@ -457,7 +467,7 @@ public abstract static class Builder { public static Builder newBuilder() { return new AutoValue_CelExpr_CelSelect.Builder() .setField("") - .setOperand(CelExpr.newBuilder().build()) + .setOperand(NOT_SET_EXPR) .setTestOnly(false); } } @@ -525,13 +535,14 @@ public Builder clearTarget() { @CanIgnoreReturnValue public Builder addArgs(CelExpr... args) { checkNotNull(args); - return addArgs(Arrays.asList(args)); + Collections.addAll(mutableArgs, args); + return this; } @CanIgnoreReturnValue public Builder addArgs(Iterable args) { checkNotNull(args); - args.forEach(mutableArgs::add); + Iterables.addAll(mutableArgs, args); return this; } @@ -604,13 +615,14 @@ public Builder setElement(int index, CelExpr element) { @CanIgnoreReturnValue public Builder addElements(CelExpr... elements) { checkNotNull(elements); - return addElements(Arrays.asList(elements)); + Collections.addAll(mutableElements, elements); + return this; } @CanIgnoreReturnValue public Builder addElements(Iterable elements) { checkNotNull(elements); - elements.forEach(mutableElements::add); + Iterables.addAll(mutableElements, elements); return this; } @@ -696,13 +708,14 @@ public Builder setEntry(int index, CelStruct.Entry entry) { @CanIgnoreReturnValue public Builder addEntries(CelStruct.Entry... entries) { checkNotNull(entries); - return addEntries(Arrays.asList(entries)); + Collections.addAll(mutableEntries, entries); + return this; } @CanIgnoreReturnValue public Builder addEntries(Iterable entries) { checkNotNull(entries); - entries.forEach(mutableEntries::add); + Iterables.addAll(mutableEntries, entries); return this; } @@ -815,13 +828,14 @@ public Builder setEntry(int index, CelMap.Entry entry) { @CanIgnoreReturnValue public Builder addEntries(CelMap.Entry... entries) { checkNotNull(entries); - return addEntries(Arrays.asList(entries)); + Collections.addAll(mutableEntries, entries); + return this; } @CanIgnoreReturnValue public Builder addEntries(Iterable entries) { checkNotNull(entries); - entries.forEach(mutableEntries::add); + Iterables.addAll(mutableEntries, entries); return this; } @@ -963,20 +977,17 @@ public static Builder newBuilder() { return new AutoValue_CelExpr_CelComprehension.Builder() .setIterVar("") .setIterVar2("") - .setIterRange(CelExpr.newBuilder().build()) + .setIterRange(NOT_SET_EXPR) .setAccuVar("") - .setAccuInit(CelExpr.newBuilder().build()) - .setLoopCondition(CelExpr.newBuilder().build()) - .setLoopStep(CelExpr.newBuilder().build()) - .setResult(CelExpr.newBuilder().build()); + .setAccuInit(NOT_SET_EXPR) + .setLoopCondition(NOT_SET_EXPR) + .setLoopStep(NOT_SET_EXPR) + .setResult(NOT_SET_EXPR); } } public static CelExpr ofNotSet(long id) { - return newBuilder() - .setId(id) - .setExprKind(AutoOneOf_CelExpr_ExprKind.notSet(new AutoValue_CelExpr_CelNotSet())) - .build(); + return newBuilder().setId(id).setExprKind(NOT_SET_KIND).build(); } public static CelExpr ofConstant(long id, CelConstant celConstant) { @@ -1007,46 +1018,45 @@ public static CelExpr ofSelect(long id, CelExpr operandExpr, String field, boole .build(); } + /** Creates a global (non receiver-style) call expression. */ + public static CelExpr ofCall(long id, String function, ImmutableList arguments) { + return ofCall(id, Optional.empty(), function, arguments); + } + public static CelExpr ofCall( long id, Optional targetExpr, String function, ImmutableList arguments) { - - CelCall.Builder celCallBuilder = CelCall.newBuilder().setFunction(function).addArgs(arguments); - targetExpr.ifPresent(celCallBuilder::setTarget); - return newBuilder() - .setId(id) - .setExprKind(AutoOneOf_CelExpr_ExprKind.call(celCallBuilder.build())) - .build(); + // setArgs/autoBuild are used in place of addArgs/build so that the already-immutable argument + // list is handed straight to the value class, skipping a copy through the builder's mutable + // list. This is on the hot path of every parse. + CelCall celCall = + CelCall.newBuilder() + .setFunction(function) + .setTarget(targetExpr) + .setArgs(arguments) + .autoBuild(); + return newBuilder().setId(id).setExprKind(AutoOneOf_CelExpr_ExprKind.call(celCall)).build(); } public static CelExpr ofList( long id, ImmutableList elements, ImmutableList optionalIndices) { - return newBuilder() - .setId(id) - .setExprKind( - AutoOneOf_CelExpr_ExprKind.list( - CelList.newBuilder() - .addElements(elements) - .addOptionalIndices(optionalIndices) - .build())) - .build(); + CelList celList = + CelList.newBuilder() + .setElements(elements) + .addOptionalIndices(optionalIndices) + .autoBuild(); + return newBuilder().setId(id).setExprKind(AutoOneOf_CelExpr_ExprKind.list(celList)).build(); } public static CelExpr ofStruct( long id, String messageName, ImmutableList entries) { - return newBuilder() - .setId(id) - .setExprKind( - AutoOneOf_CelExpr_ExprKind.struct( - CelStruct.newBuilder().setMessageName(messageName).addEntries(entries).build())) - .build(); + CelStruct celStruct = + CelStruct.newBuilder().setMessageName(messageName).setEntries(entries).autoBuild(); + return newBuilder().setId(id).setExprKind(AutoOneOf_CelExpr_ExprKind.struct(celStruct)).build(); } public static CelExpr ofMap(long id, ImmutableList entries) { - return newBuilder() - .setId(id) - .setExprKind( - AutoOneOf_CelExpr_ExprKind.map(CelMap.newBuilder().addEntries(entries).build())) - .build(); + CelMap celMap = CelMap.newBuilder().setEntries(entries).autoBuild(); + return newBuilder().setId(id).setExprKind(AutoOneOf_CelExpr_ExprKind.map(celMap)).build(); } public static CelStruct.Entry ofStructEntry( diff --git a/common/src/test/java/dev/cel/common/CelSourceTest.java b/common/src/test/java/dev/cel/common/CelSourceTest.java index d8b3701e3..24eded8fd 100644 --- a/common/src/test/java/dev/cel/common/CelSourceTest.java +++ b/common/src/test/java/dev/cel/common/CelSourceTest.java @@ -18,10 +18,12 @@ import static org.antlr.v4.runtime.IntStream.UNKNOWN_SOURCE_NAME; import static org.junit.Assert.assertThrows; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import dev.cel.common.CelSource.Extension; import dev.cel.common.CelSource.Extension.Component; import dev.cel.common.CelSource.Extension.Version; +import dev.cel.common.ast.CelExpr; import dev.cel.common.internal.BasicCodePointArray; import dev.cel.common.internal.CodePointStream; import dev.cel.common.internal.Latin1CodePointArray; @@ -192,4 +194,37 @@ public void source_lineOffsetsAlreadyComputed_throws() { .hasMessageThat() .contains("Line offsets were already been computed through the provided code points."); } + + @Test + public void builder_getPositionsMap_isMutable() { + CelSource.Builder builder = CelSource.newBuilder(); + builder.getPositionsMap().put(1L, 10); + assertThat(builder.build().getPositionsMap()).containsExactly(1L, 10); + } + + @Test + public void builder_getMacroCalls_isMutable() { + CelSource.Builder builder = CelSource.newBuilder(); + CelExpr macroCall = CelExpr.ofIdent(1, "foo"); + builder.getMacroCalls().put(1L, macroCall); + assertThat(builder.build().getMacroCalls()).containsExactly(1L, macroCall); + } + + @Test + public void builder_addPositionsMap_mergesWithExisting() { + CelSource.Builder builder = CelSource.newBuilder(); + builder.addPositions(1L, 10); + builder.addPositionsMap(ImmutableMap.of(2L, 20)); + assertThat(builder.build().getPositionsMap()).containsExactly(1L, 10, 2L, 20); + } + + @Test + public void builder_addAllMacroCalls_mergesWithExisting() { + CelSource.Builder builder = CelSource.newBuilder(); + CelExpr macro1 = CelExpr.ofIdent(1, "foo"); + CelExpr macro2 = CelExpr.ofIdent(2, "bar"); + builder.addMacroCalls(1L, macro1); + builder.addAllMacroCalls(ImmutableMap.of(2L, macro2)); + assertThat(builder.build().getMacroCalls()).containsExactly(1L, macro1, 2L, macro2); + } } diff --git a/parser/src/main/java/dev/cel/parser/BUILD.bazel b/parser/src/main/java/dev/cel/parser/BUILD.bazel index 55183446f..848209380 100644 --- a/parser/src/main/java/dev/cel/parser/BUILD.bazel +++ b/parser/src/main/java/dev/cel/parser/BUILD.bazel @@ -118,7 +118,6 @@ java_library( "//common:source_location", "//common/ast", "//common/internal", - "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:org_jspecify_jspecify", ], diff --git a/parser/src/main/java/dev/cel/parser/PrattParser.java b/parser/src/main/java/dev/cel/parser/PrattParser.java index dceaadad6..1fe2a4bab 100644 --- a/parser/src/main/java/dev/cel/parser/PrattParser.java +++ b/parser/src/main/java/dev/cel/parser/PrattParser.java @@ -26,6 +26,7 @@ import dev.cel.common.Operator; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; +import dev.cel.common.internal.CelCodePointArray; import dev.cel.common.internal.Constants; import java.text.ParseException; import java.util.ArrayDeque; @@ -40,9 +41,14 @@ /** Pratt parser implementation for CEL. */ final class PrattParser { + /** Sentinel stored in {@link #positions} for expression ids that have no source position. */ + private static final int NO_POSITION = -1; + private static final String ACCUMULATOR_NAME = "@result"; private static final CelExpr ERROR = CelExpr.newBuilder().setConstant(Constants.ERROR).build(); - private static final Lexer.Token END_TOKEN = new Lexer.Token(Lexer.TokenType.END, 0, 0); + private static final Lexer.Token END_TOKEN = + new Lexer.Token(Lexer.TokenType.END, NO_POSITION, NO_POSITION); + /** Most logical chains are short; 8 avoids resizing for the overwhelming majority. */ private static final int INITIAL_CHAIN_CAPACITY = 8; @@ -115,10 +121,18 @@ private static final class UnaryOp { } private final CelSource source; + private final CelCodePointArray content; private final CelOptions options; private final ImmutableMap macros; private final Lexer lexer; - private final Map positions; + + /** + * Code point offset of each expression node, indexed by expression id, with {@link #NO_POSITION} + * for nodes that have none. Ids are dense and handed out sequentially by {@link #nextId}, so an + * array avoids the boxing and hashing a {@code Map} would cost on every node. + */ + private int[] positions; + private Map macroCalls = ImmutableMap.of(); private PrattMacroExprFactory macroExprFactory; private final List issues; @@ -150,7 +164,7 @@ static CelValidationResult parse( } CelSource.Builder sourceBuilder = source.toBuilder(); - sourceBuilder.addPositionsMap(prattParser.positions); + prattParser.copyPositionsTo(sourceBuilder); sourceBuilder.addAllMacroCalls(prattParser.macroCalls); return new CelValidationResult( @@ -160,10 +174,12 @@ static CelValidationResult parse( private PrattParser(CelSource source, CelOptions options, Map macros) { this.source = source; + this.content = source.getContent(); this.options = options; this.macros = ImmutableMap.copyOf(macros); - this.lexer = new Lexer(source.getContent()); - this.positions = new HashMap<>(); + this.lexer = new Lexer(content); + this.positions = new int[Math.max(16, Math.min(content.size() + 1, 1024))]; + Arrays.fill(this.positions, NO_POSITION); this.issues = new ArrayList<>(); this.nextId = 1; peekToken = nextSignificantToken(true); @@ -197,13 +213,14 @@ private String getTokenText(Lexer.Token tok) { if (tok.text != null) { return tok.text; } - if (tok.start >= 0 && tok.end >= tok.start && tok.end <= source.getContent().size()) { - return source.getContent().substring(tok.start, tok.end); + if (tok.start >= 0 && tok.end >= tok.start && tok.end <= content.size()) { + return content.substring(tok.start, tok.end); } return ""; } private Lexer.Token nextSignificantToken(boolean reportError) { + // The lexer skips whitespace and comments itself, so every token it returns is significant. Lexer.Token tok = lexer.lex(); if (tok.type == Lexer.TokenType.ERROR && reportError) { reportSyntaxError(tok, lexer.getError().message); @@ -250,6 +267,7 @@ private boolean expect(Lexer.TokenType type, String msg) { return false; } + // Find the next delimiter to prevent a cascade of spurious secondary errors. private void synchronizeOnDelimiter() { if (isRecoveryLimitExceeded()) { peekToken = END_TOKEN; @@ -276,7 +294,7 @@ private long nextId(int position) { nodeLimitExceeded = true; } if (!nodeLimitExceeded && position >= 0) { - positions.put(id, position); + setPosition(id, position); } return id; } @@ -286,32 +304,65 @@ private long nextId(Lexer.Token token) { } private long nextId() { - return nextId(-1); + return nextId(NO_POSITION); } private void setPosition(long id, Lexer.Token token) { if (token.start >= 0) { - positions.put(id, token.start); + setPosition(id, token.start); + } + } + + private void setPosition(long id, int position) { + int index = (int) id; + if (index >= positions.length) { + int oldLength = positions.length; + positions = Arrays.copyOf(positions, Math.max(index + 1, oldLength * 2)); + Arrays.fill(positions, oldLength, positions.length, NO_POSITION); } + positions[index] = position; + } + + /** Returns the recorded position of {@code id}, or {@link #NO_POSITION} if it has none. */ + private int getPosition(long id) { + int index = (int) id; + return index >= 0 && index < positions.length ? positions[index] : NO_POSITION; + } + + private void copyPositionsTo(CelSource.Builder sourceBuilder) { + ImmutableMap.Builder positionsMap = + ImmutableMap.builderWithExpectedSize((int) nextId); + for (long id = 1; id < nextId; id++) { + int position = getPosition(id); + if (position != NO_POSITION) { + positionsMap.put(id, position); + } + } + sourceBuilder.addPositionsMap(positionsMap.buildOrThrow()); } private long copyId(long id) { if (id == 0) { return 0; } - int pos = positions.getOrDefault(id, 0); - return nextId(pos); + return nextId(getPosition(id)); } private void eraseId(long id) { - positions.remove(id); + int index = (int) id; + if (index >= 0 && index < positions.length) { + positions[index] = NO_POSITION; + } if (nextId == id + 1) { --nextId; } } private void reportError(int position, String msg) { - CelSourceLocation loc = source.getOffsetLocation(position).orElse(CelSourceLocation.NONE); + CelSourceLocation loc = + position >= 0 + ? source.getOffsetLocation(position).orElse(CelSourceLocation.NONE) + : CelSourceLocation.NONE; reportError(loc, msg); } @@ -411,16 +462,8 @@ private CelExpr parseTernary(CelExpr lhs) { return lhs; } CelExpr falseExpr = parseExpr(); - return CelExpr.newBuilder() - .setId(opId) - .setCall( - CelExpr.CelCall.newBuilder() - .setFunction(Operator.CONDITIONAL.getFunction()) - .addArgs(lhs) - .addArgs(trueExpr) - .addArgs(falseExpr) - .build()) - .build(); + return CelExpr.ofCall( + opId, Operator.CONDITIONAL.getFunction(), ImmutableList.of(lhs, trueExpr, falseExpr)); } private CelExpr parseBalancedLogicalChain(CelExpr lhs, BinaryOpInfo opInfo) { @@ -462,11 +505,11 @@ private CelExpr balancedTree(String op, CelExpr[] terms, long[] ops, int lo, int } private static CelExpr buildBinaryCall(long id, String function, CelExpr lhs, CelExpr rhs) { - return CelExpr.newBuilder() - .setId(id) - .setCall( - CelExpr.CelCall.newBuilder().setFunction(function).addArgs(lhs).addArgs(rhs).build()) - .build(); + return CelExpr.ofCall(id, function, ImmutableList.of(lhs, rhs)); + } + + private static CelExpr buildUnaryCall(long id, String function, CelExpr operand) { + return CelExpr.ofCall(id, function, ImmutableList.of(operand)); } private CelExpr parseSelectorChain() { @@ -519,38 +562,18 @@ private CelExpr parseSelectorChainTail(CelExpr initialLhs) { String idText = normalizeIdent(idTok, /* allowQuoted= */ !isMemberCall); if (optional) { long opId = nextId(dotTok); - CelExpr arg1 = lhs; - CelExpr arg2 = + CelExpr field = CelExpr.ofConstant(nextId(getLeftmostPosition(lhs)), CelConstant.ofValue(idText)); - lhs = - CelExpr.newBuilder() - .setId(opId) - .setCall( - CelExpr.CelCall.newBuilder() - .setFunction(Operator.OPTIONAL_SELECT.getFunction()) - .addArgs(arg1) - .addArgs(arg2) - .build()) - .build(); + lhs = buildBinaryCall(opId, Operator.OPTIONAL_SELECT.getFunction(), lhs, field); } else if (peekToken.type == Lexer.TokenType.LEFT_PAREN) { Lexer.Token lparen = nextToken(); long callId = nextId(lparen); ImmutableList args = parseArguments(Lexer.TokenType.RIGHT_PAREN); Optional expanded = tryExpandMacro(callId, idText, lhs, args); - if (expanded.isPresent()) { - lhs = expanded.get(); - } else { - lhs = - CelExpr.newBuilder() - .setId(callId) - .setCall( - CelExpr.CelCall.newBuilder() - .setFunction(idText) - .setTarget(lhs) - .addArgs(args) - .build()) - .build(); - } + lhs = + expanded.isPresent() + ? expanded.get() + : CelExpr.ofCall(callId, Optional.of(lhs), idText, args); } else { lhs = CelExpr.ofSelect(nextId(dotTok), lhs, idText, /* isTestOnly= */ false); } @@ -573,16 +596,7 @@ private CelExpr parseSelectorChainTail(CelExpr initialLhs) { expect(Lexer.TokenType.RIGHT_BRACKET, "expected ']'"); String opName = optional ? Operator.OPTIONAL_INDEX.getFunction() : Operator.INDEX.getFunction(); - lhs = - CelExpr.newBuilder() - .setId(opId) - .setCall( - CelExpr.CelCall.newBuilder() - .setFunction(opName) - .addArgs(lhs) - .addArgs(index) - .build()) - .build(); + lhs = buildBinaryCall(opId, opName, lhs, index); } else if (tok == Lexer.TokenType.LEFT_BRACE) { String structName = extractStructName(lhs); if (structName == null) { @@ -629,10 +643,7 @@ private CelExpr parseUnaryOps() { (opType == Lexer.TokenType.EXCLAMATION) ? Operator.LOGICAL_NOT.getFunction() : Operator.NEGATE.getFunction(); - return CelExpr.newBuilder() - .setId(opId) - .setCall(CelExpr.CelCall.newBuilder().setFunction(opName).addArgs(operand).build()) - .build(); + return buildUnaryCall(opId, opName, operand); } private CelExpr parseUnaryOpsChain(Lexer.Token firstOp) { @@ -706,11 +717,7 @@ private CelExpr parseUnaryOpsChain(Lexer.Token firstOp) { (ops.get(i).token.type == Lexer.TokenType.EXCLAMATION) ? Operator.LOGICAL_NOT.getFunction() : Operator.NEGATE.getFunction(); - operand = - CelExpr.newBuilder() - .setId(ops.get(i).id) - .setCall(CelExpr.CelCall.newBuilder().setFunction(opName).addArgs(operand).build()) - .build(); + operand = buildUnaryCall(ops.get(i).id, opName, operand); } return operand; @@ -744,10 +751,7 @@ private CelExpr parseIdentOrCall() { if (expanded.isPresent()) { return expanded.get(); } - return CelExpr.newBuilder() - .setId(callId) - .setCall(CelExpr.CelCall.newBuilder().setFunction(name).addArgs(args).build()) - .build(); + return CelExpr.ofCall(callId, name, args); } long id = nextId(leadingDot ? firstTok : idTok); return CelExpr.ofIdent(id, name); @@ -815,7 +819,8 @@ private CelExpr parsePrimary() { private CelExpr parseList() { Lexer.Token openTok = nextToken(); long listId = nextId(openTok); - CelExpr.CelList.Builder listBuilder = CelExpr.CelList.newBuilder(); + ImmutableList.Builder elements = ImmutableList.builder(); + ImmutableList.Builder optionalIndices = ImmutableList.builder(); int elemIndex = 0; while (peekToken.type != Lexer.TokenType.RIGHT_BRACKET && peekToken.type != Lexer.TokenType.END) { @@ -827,9 +832,9 @@ private CelExpr parseList() { reportError(q.start, "unsupported syntax '?'"); } } - listBuilder.addElements(parseExpr()); + elements.add(parseExpr()); if (optional) { - listBuilder.addOptionalIndices(elemIndex); + optionalIndices.add(elemIndex); } elemIndex++; if (peekToken.type == Lexer.TokenType.COMMA) { @@ -839,13 +844,13 @@ private CelExpr parseList() { } } expect(Lexer.TokenType.RIGHT_BRACKET, "expected ']'"); - return CelExpr.newBuilder().setId(listId).setList(listBuilder.build()).build(); + return CelExpr.ofList(listId, elements.build(), optionalIndices.build()); } private CelExpr parseMap() { Lexer.Token openTok = nextToken(); long mapId = nextId(openTok); - CelExpr.CelMap.Builder mapBuilder = CelExpr.CelMap.newBuilder(); + ImmutableList.Builder entries = ImmutableList.builder(); while (peekToken.type != Lexer.TokenType.RIGHT_BRACE && peekToken.type != Lexer.TokenType.END) { boolean optional = false; Lexer.Token keyStart = peekToken; @@ -865,13 +870,7 @@ private CelExpr parseMap() { } setPosition(entryId, colon); CelExpr value = parseExpr(); - mapBuilder.addEntries( - CelExpr.CelMap.Entry.newBuilder() - .setId(entryId) - .setKey(key) - .setValue(value) - .setOptionalEntry(optional) - .build()); + entries.add(CelExpr.ofMapEntry(entryId, key, value, optional)); if (peekToken.type == Lexer.TokenType.COMMA) { nextToken(); } else { @@ -879,13 +878,12 @@ private CelExpr parseMap() { } } expect(Lexer.TokenType.RIGHT_BRACE, "expected '}'"); - return CelExpr.newBuilder().setId(mapId).setMap(mapBuilder.build()).build(); + return CelExpr.ofMap(mapId, entries.build()); } private CelExpr parseStruct(long objId, String structName) { nextToken(); - CelExpr.CelStruct.Builder structBuilder = - CelExpr.CelStruct.newBuilder().setMessageName(structName); + ImmutableList.Builder entries = ImmutableList.builder(); while (peekToken.type != Lexer.TokenType.RIGHT_BRACE && peekToken.type != Lexer.TokenType.END) { boolean optional = false; if (peekToken.type == Lexer.TokenType.QUESTION) { @@ -909,13 +907,7 @@ private CelExpr parseStruct(long objId, String structName) { } long fieldId = nextId(colon); CelExpr value = parseExpr(); - structBuilder.addEntries( - CelExpr.CelStruct.Entry.newBuilder() - .setId(fieldId) - .setFieldKey(fieldName) - .setValue(value) - .setOptionalEntry(optional) - .build()); + entries.add(CelExpr.ofStructEntry(fieldId, fieldName, value, optional)); if (peekToken.type == Lexer.TokenType.COMMA) { nextToken(); } else { @@ -923,7 +915,7 @@ private CelExpr parseStruct(long objId, String structName) { } } expect(Lexer.TokenType.RIGHT_BRACE, "expected '}'"); - return CelExpr.newBuilder().setId(objId).setStruct(structBuilder.build()).build(); + return CelExpr.ofStruct(objId, structName, entries.build()); } private ImmutableList parseArguments(Lexer.TokenType closeToken) { @@ -1068,7 +1060,7 @@ private int getLeftmostPosition(CelExpr expr) { while (expr.exprKind().getKind() == CelExpr.ExprKind.Kind.SELECT) { expr = expr.select().operand(); } - return positions.getOrDefault(expr.id(), 0); + return getPosition(expr.id()); } private @Nullable CelMacro lookupMacro(String id, int argCount, boolean receiverStyle) { @@ -1097,8 +1089,7 @@ private Optional tryExpandMacro( } if (nodeLimitExceeded) { reportError( - positions.getOrDefault(exprId, 0), - "could not expand macro: expression node limit exceeded"); + getPosition(exprId), "could not expand macro: expression node limit exceeded"); return Optional.empty(); } @@ -1107,8 +1098,8 @@ private Optional tryExpandMacro( return Optional.of(ERROR); } - int macroPosition = positions.getOrDefault(exprId, 0); - CelExpr targetExpr = (target != null ? target : CelExpr.newBuilder().build()); + int macroPosition = getPosition(exprId); + CelExpr targetExpr = (target != null ? target : CelExpr.ofNotSet(0)); Optional expandedExpr = expandMacro(macroPosition, macro, targetExpr, args); if (expandedExpr.isPresent()) { @@ -1184,8 +1175,9 @@ private int countGroupingParentheses() { // Fast path: if the next non-whitespace character is not '(', leading open parens is 1. int pos = peekToken.end; - while (pos < source.getContent().size()) { - int c = source.getContent().get(pos); + int size = content.size(); + while (pos < size) { + int c = content.get(pos); if (c != ' ' && c != '\t' && c != '\n' && c != '\r' && c != '\f' && c != 11) { if (c == '/') { // A comment might precede another '('. @@ -1280,7 +1272,10 @@ public String getAccumulatorVarName() { @Override protected CelSourceLocation getSourceLocation(long exprId) { - int pos = positions.getOrDefault(exprId, -1); + int pos = getPosition(exprId); + if (pos < 0) { + return CelSourceLocation.NONE; + } return source.getOffsetLocation(pos).orElse(CelSourceLocation.NONE); } @@ -1289,7 +1284,10 @@ protected CelSourceLocation currentSourceLocationForMacro() { int pos = !macroPositions.isEmpty() ? peekPosition() - : (currentToken != null ? currentToken.start : 0); + : (currentToken != null ? currentToken.start : NO_POSITION); + if (pos < 0) { + return CelSourceLocation.NONE; + } return source.getOffsetLocation(pos).orElse(CelSourceLocation.NONE); } diff --git a/parser/src/test/java/dev/cel/parser/CelParserImplTest.java b/parser/src/test/java/dev/cel/parser/CelParserImplTest.java index 5b7f9defb..09d578a36 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserImplTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserImplTest.java @@ -18,6 +18,7 @@ import static org.junit.Assert.assertThrows; import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; @@ -27,9 +28,11 @@ import dev.cel.common.CelSource; import dev.cel.common.CelValidationException; import dev.cel.common.CelValidationResult; +import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; import java.util.Collections; import java.util.Optional; +import org.junit.Assume; import org.junit.Test; import org.junit.runner.RunWith; @@ -51,6 +54,7 @@ public void build_withMacros_containsAllMacros() { CelParserImpl parser = (CelParserImpl) newParserBuilder().setStandardMacros(CelStandardMacro.STANDARD_MACROS).build(); + assertThat(parser.findMacro("has:1:false")).hasValue(CelStandardMacro.HAS.getDefinition()); assertThat(parser.findMacro("all:2:true")).hasValue(CelStandardMacro.ALL.getDefinition()); assertThat(parser.findMacro("exists:2:true")).hasValue(CelStandardMacro.EXISTS.getDefinition()); @@ -67,6 +71,7 @@ public void build_withStandardMacros_containsAllMacros() { CelParserImpl parser = (CelParserImpl) newParserBuilder().setStandardMacros(CelStandardMacro.STANDARD_MACROS).build(); + assertThat(parser.findMacro("has:1:false")).hasValue(CelStandardMacro.HAS.getDefinition()); assertThat(parser.findMacro("all:2:true")).hasValue(CelStandardMacro.ALL.getDefinition()); assertThat(parser.findMacro("exists:2:true")).hasValue(CelStandardMacro.EXISTS.getDefinition()); @@ -106,6 +111,7 @@ public void build_withStandardMacrosAndCustomMacros_containsAllMacros() { public void build_withMacro_containsMacro() { CelParserImpl parser = (CelParserImpl) newParserBuilder().setStandardMacros(CelStandardMacro.HAS).build(); + assertThat(parser.findMacro("has:1:false")).hasValue(CelStandardMacro.HAS.getDefinition()); } @@ -113,6 +119,7 @@ public void build_withMacro_containsMacro() { public void build_withStandardMacro_containsMacro() { CelParserImpl parser = (CelParserImpl) newParserBuilder().setStandardMacros(CelStandardMacro.HAS).build(); + assertThat(parser.findMacro("has:1:false")).hasValue(CelStandardMacro.HAS.getDefinition()); } @@ -146,6 +153,7 @@ public void build_standardMacroKeyConflictsWithCustomMacro_throws() { @Test public void build_containsNoMacros() { CelParserImpl parser = (CelParserImpl) newParserBuilder().build(); + assertThat(parser.findMacro("has:1:false")).isEmpty(); } @@ -180,7 +188,9 @@ public void parse_throwsWhenExpressionSizeCodePointLimitExceeded() { .maxExpressionCodePointSize(2) .build()) .build(); + CelValidationResult parseResult = parser.parse(CelSource.newBuilder("foo").build()); + CelValidationException exception = assertThrows(CelValidationException.class, parseResult::getAst); assertThat(exception.getErrors()).hasSize(1); @@ -261,7 +271,6 @@ public void parse_largeExprHitsMaxRecursionLimit_throws( public void parse_exprUnderMaxRecursionLimit_doesNotThrow( @TestParameter MaxParseRecursionDepthTestCase testCase) throws CelValidationException { int maxParseRecursionLimit = MaxParseRecursionDepthTestCase.MAX_RECURSION_LIMIT + 1; - CelParser parser = newParserBuilder() .setOptions( @@ -270,7 +279,9 @@ public void parse_exprUnderMaxRecursionLimit_doesNotThrow( .maxParseRecursionDepth(maxParseRecursionLimit) .build()) .build(); + CelValidationResult parseResult = parser.parse(CelSource.newBuilder(testCase.source).build()); + assertThat(parseResult.hasError()).isFalse(); assertThat(parseResult.getAst()).isNotNull(); } @@ -285,6 +296,7 @@ public void parse_nodeLimitExceeded_throws() { .maxParseExpressionNodeCount(2) .build()) .build(); + CelValidationResult parseResult = parser.parse("a + b + c"); CelValidationException exception = @@ -304,6 +316,7 @@ public void parse_macroExpansionNodeLimitExceeded_throws() { .maxParseExpressionNodeCount(5) .build()) .build(); + CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)"); CelValidationException exception = @@ -330,7 +343,9 @@ public void parse_macroExpansionNodeLimitNotExceeded_success() throws CelValidat .maxParseExpressionNodeCount(100) .build()) .build(); + CelValidationResult parseResult = parser.parse("[1, 2, 3, 4, 5].map(x, x * 2)"); + assertThat(parseResult.hasError()).isFalse(); assertThat(parseResult.getAst()).isNotNull(); } @@ -402,10 +417,76 @@ public void toParserBuilder_collectionProperties_copied() { @Test public void parse_logicalChainLongerThanInitialCapacity_succeeds() { CelParser parser = newParserBuilder().build(); + for (int operands = 2; operands <= 64; operands++) { String expr = Joiner.on(" || ").join(Collections.nCopies(operands, "true")); + CelValidationResult result = parser.parse(expr); + assertThat(result.hasError()).isFalse(); } } + + @Test + public void parse_lexerErrorExceedsRecoveryLimit_stopsParsing() { + Assume.assumeTrue(enablePrattParser); + CelParser parser = + newParserBuilder() + .setOptions( + CelOptions.newBuilder() + .enablePrattParser(enablePrattParser) + .maxParseErrorRecoveryLimit(2) + .build()) + .build(); + + CelValidationResult result = parser.parse("[ @, @, @ ]"); + + assertThat(result.hasError()).isTrue(); + assertThat(result.getErrors()).hasSize(3); + } + + @Test + public void parse_largeExpression_expandsPositionsArray() throws Exception { + CelParser parser = newParserBuilder().build(); + String expr = "[" + Joiner.on(", ").join(Collections.nCopies(1025, "1")) + "]"; + ImmutableMap.Builder expectedPositions = + ImmutableMap.builderWithExpectedSize(1026); + expectedPositions.put(1L, 0); + for (int i = 0; i < 1025; i++) { + expectedPositions.put((long) (i + 2), 1 + 3 * i); + } + + CelValidationResult result = parser.parse(expr); + + assertThat(result.hasError()).isFalse(); + assertThat(result.getAst().getSource().getPositionsMap()) + .containsExactlyEntriesIn(expectedPositions.buildOrThrow()); + } + + @Test + public void parse_macroCopiesNodeWithoutPosition_noSourcePositionRecorded() throws Exception { + CelMacro macro = + CelMacro.newGlobalMacro( + "copy_macro", + 0, + (exprFactory, target, args) -> { + CelExpr nodeWithoutPosition = + CelExpr.newBuilder() + .setId(5L) + .setConstant(CelConstant.ofValue(10L)) + .build(); + return Optional.of(exprFactory.copy(nodeWithoutPosition)); + }); + CelParser parser = newParserBuilder().addMacros(macro).build(); + + CelValidationResult result = parser.parse("copy_macro()"); + + assertThat(result.hasError()).isFalse(); + // The contract for nodes without a source position is -1 (NO_POSITION). Unpositioned + // nodes must not be assigned position 0 (which is a valid source offset) and thus should + // not have an entry recorded in the positions map. + assertThat(result.getAst().getSource().getPositionsMap()).isEmpty(); + } } + + From ca299fa9e1b6e43afd5b01dc3c9eb640a5aac8df Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Tue, 15 Sep 2026 11:50:41 -0700 Subject: [PATCH 201/204] Add test cases around map type parameter propagation PiperOrigin-RevId: 981946267 --- .../test/java/dev/cel/checker/TypesTest.java | 60 +++++++++++++++++++ .../extensions/CelOptionalLibraryTest.java | 30 ++++++++++ 2 files changed, 90 insertions(+) diff --git a/checker/src/test/java/dev/cel/checker/TypesTest.java b/checker/src/test/java/dev/cel/checker/TypesTest.java index a8ca2167e..786e50668 100644 --- a/checker/src/test/java/dev/cel/checker/TypesTest.java +++ b/checker/src/test/java/dev/cel/checker/TypesTest.java @@ -256,6 +256,66 @@ public void isAssignable_typeType_occursCheck_failsOnTransitiveCycle() { assertThat(result).isNull(); } + @Test + public void isAssignable_typeType_occursCheck_mapTypeParam_to_typeParam() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(MapType.create(SimpleType.STRING, typeParamT)); + CelType toType = TypeType.create(typeParamT); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).isNull(); + } + + @Test + public void isAssignable_typeType_occursCheck_typeParam_to_mapTypeParam() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(typeParamT); + CelType toType = TypeType.create(MapType.create(SimpleType.STRING, typeParamT)); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).isNull(); + } + + @Test + public void isAssignable_typeType_occursCheck_mapTypeParamInKey_to_typeParam() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(MapType.create(typeParamT, SimpleType.STRING)); + CelType toType = TypeType.create(typeParamT); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).isNull(); + } + + @Test + public void isAssignable_typeType_occursCheck_listTypeParam_to_typeParam() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(ListType.create(typeParamT)); + CelType toType = TypeType.create(typeParamT); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).isNull(); + } + + @Test + public void isAssignable_typeType_occursCheck_optionalTypeParam_to_typeParam() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(OptionalType.create(typeParamT)); + CelType toType = TypeType.create(typeParamT); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).isNull(); + } + @Test public void compiler_typeParamInTypeType_resolvesReturnTypeInt() throws Exception { TypeParamType typeParamT = TypeParamType.create("T"); diff --git a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java index 650c01526..f594c6dc2 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java @@ -14,8 +14,10 @@ package dev.cel.extensions; +import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import static org.junit.Assume.assumeFalse; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -35,6 +37,8 @@ import dev.cel.common.CelValidationException; import dev.cel.common.CelVarDecl; import dev.cel.common.ast.CelExpr; +import dev.cel.common.ast.CelExpr.CelList; +import dev.cel.common.types.CelKind; import dev.cel.common.types.CelType; import dev.cel.common.types.ListType; import dev.cel.common.types.MapType; @@ -1799,6 +1803,32 @@ public void optionalMessageCreation_fieldKeySetOnNonOptional_throws() { "Cannot initialize optional entry 'single_double_wrapper' from non-optional value foo"); } + @Test + @TestParameters("{expression: '[type([]), int, type(optional.none())]'}") + @TestParameters("{expression: '[type([]), type(optional.none()), int]'}") + @TestParameters("{expression: '[int, type([]), type(optional.none())]'}") + @TestParameters("{expression: '[int, type(optional.none()), type([])]'}") + @TestParameters("{expression: '[type(optional.none()), type([]), int]'}") + @TestParameters("{expression: '[type(optional.none()), int, type([])]'}") + public void listType_heterogeneousTypePermutations_resolvesToListDyn(String expression) + throws Exception { + assumeFalse(testMode.equals(TestMode.PLANNER_PARSE_ONLY)); + + Cel cel = newCelBuilder().build(); + + CelAbstractSyntaxTree ast = compile(cel, expression); + CelList list = ast.getExpr().listOrDefault(); + + assertThat(ast.getResultType()).isEqualTo(ListType.create(SimpleType.DYN)); + assertThat( + list.elements().stream() + .map(elem -> ast.getType(elem.id()).map(CelType::kind)) + .collect(toImmutableList())) + .containsExactly( + Optional.of(CelKind.TYPE), Optional.of(CelKind.TYPE), Optional.of(CelKind.TYPE)); + assertThat((List) cel.createProgram(ast).eval()).hasSize(3); + } + private CelAbstractSyntaxTree compile(CelCompiler compiler, String expression) throws CelValidationException { CelAbstractSyntaxTree ast = compiler.parse(expression).getAst(); From 1c1604c60664c39a7e25dcd905b93f449df2c6ca Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Tue, 15 Sep 2026 13:04:01 -0700 Subject: [PATCH 202/204] Change cel.@attribute to return a parameterized type to avoid dyn typed results PiperOrigin-RevId: 981993302 --- .../optimizer/optimizers/SelectOptimizer.java | 81 +++++-- .../dev/cel/optimizer/optimizers/BUILD.bazel | 1 + .../optimizers/SelectOptimizerTest.java | 222 +++++++++++++----- 3 files changed, 233 insertions(+), 71 deletions(-) diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java index 3c6097180..c50c07c29 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java @@ -59,6 +59,8 @@ import dev.cel.common.types.CelTypes; import dev.cel.common.types.ListType; import dev.cel.common.types.SimpleType; +import dev.cel.common.types.TypeParamType; +import dev.cel.common.types.TypeType; import dev.cel.common.values.CelByteString; import dev.cel.optimizer.AstMutator; import dev.cel.optimizer.CelAstOptimizer; @@ -103,9 +105,9 @@ *

Expressions are rewritten into the following forms: * *

- *   // Selection chains (user message is 3-tuple, leaf scalar is 4-tuple)
+ *   // Selection chains (user message is 3-tuple, leaf scalar is 4-tuple, leaf type is 3rd argument)
  *   request.user.age -> cel.@attribute(request,
- *       [[user_num, "user", type_code], [age_num, "age", type_code, default_val]])
+ *       [[user_num, "user", type_code], [age_num, "age", type_code, default_val]], int)
  *
  *   // Presence tests (2-tuples)
  *   has(request.user.age) -> cel.@hasField(request,
@@ -122,20 +124,23 @@ public final class SelectOptimizer implements CelAstOptimizer {
    * 

Protobuf wire format encodes maps as repeated message entries ({@code MapEntry}). To avoid * wire-decoding ambiguities with singular submessages, maps use this dedicated type code. */ - private static final long CEL_MAP_TYPE_CODE = 20L; + private static final long CEL_MAP_TYPE_CODE = -1L; private static final String CEL_ATTRIBUTE_FUNCTION_NAME = "cel.@attribute"; private static final String CEL_HAS_FIELD_FUNCTION_NAME = "cel.@hasField"; + private static final TypeParamType TYPE_PARAM_T = TypeParamType.create("T"); + @VisibleForTesting static final CelFunctionDecl CEL_ATTRIBUTE_FUNCTION_DECL = CelFunctionDecl.newFunctionDeclaration( CEL_ATTRIBUTE_FUNCTION_NAME, CelOverloadDecl.newGlobalOverload( "cel_attribute_list", + TYPE_PARAM_T, SimpleType.DYN, - SimpleType.DYN, - ListType.create(SimpleType.DYN))); + ListType.create(SimpleType.DYN), + TypeType.create(TYPE_PARAM_T))); @VisibleForTesting static final CelFunctionDecl CEL_HAS_FIELD_FUNCTION_DECL = @@ -295,8 +300,19 @@ private void rewriteSelectChain( CelMutableExpr qualifiersExpr = CelMutableExpr.ofList(idGenerator.nextExprId(), CelMutableList.create(qualifierLists)); - String functionName = isHasField ? CEL_HAS_FIELD_FUNCTION_NAME : CEL_ATTRIBUTE_FUNCTION_NAME; - topNode.expr().setCall(CelMutableCall.create(functionName, currentExpr, qualifiersExpr)); + if (isHasField) { + topNode + .expr() + .setCall(CelMutableCall.create(CEL_HAS_FIELD_FUNCTION_NAME, currentExpr, qualifiersExpr)); + } else { + CelMutableExpr typeExpr = + CelMutableExpr.ofIdent(idGenerator.nextExprId(), resolveTypeIdent(topField)); + topNode + .expr() + .setCall( + CelMutableCall.create( + CEL_ATTRIBUTE_FUNCTION_NAME, currentExpr, qualifiersExpr, typeExpr)); + } } private static long resolveTypeCode(FieldDescriptor field) { @@ -306,6 +322,43 @@ private static long resolveTypeCode(FieldDescriptor field) { return field.getType().toProto().getNumber(); } + private static String resolveTypeIdent(FieldDescriptor field) { + if (field.isMapField()) { + return "map"; + } + if (field.isRepeated()) { + return "list"; + } + switch (field.getType()) { + case DOUBLE: + case FLOAT: + return "double"; + case INT64: + case SINT64: + case SFIXED64: + case INT32: + case SINT32: + case SFIXED32: + case ENUM: + return "int"; + case UINT64: + case FIXED64: + case UINT32: + case FIXED32: + return "uint"; + case BOOL: + return "bool"; + case STRING: + return "string"; + case BYTES: + return "bytes"; + case MESSAGE: + return field.getMessageType().getFullName(); + default: + throw new IllegalArgumentException("Unsupported protobuf field type: " + field.getType()); + } + } + private boolean isTopOfSelectChain(CelNavigableMutableAst navAst, CelNavigableMutableExpr node) { return getOptimizableField(navAst, node).isPresent() && !node.parent().flatMap(parent -> getOptimizableField(navAst, parent)).isPresent(); @@ -414,13 +467,6 @@ private static CelAbstractSyntaxTree tagAstExtension(CelAbstractSyntaxTree ast) return CelAbstractSyntaxTree.newParsedAst(ast.getExpr(), celSourceBuilder.build()); } - private SelectOptimizer( - SelectOptimizerOptions options, Iterable fileDescriptors) { - this.options = checkNotNull(options); - this.astMutator = AstMutator.newInstance(options.iterationLimit()); - this.descriptorPool = newDescriptorPool(options, checkNotNull(fileDescriptors)); - } - private static CelDescriptorPool newDescriptorPool( SelectOptimizerOptions options, Iterable fileDescriptors) { CelDescriptors celDescriptors = @@ -432,6 +478,13 @@ private static CelDescriptorPool newDescriptorPool( return CombinedDescriptorPool.create(descriptorPools.build()); } + private SelectOptimizer( + SelectOptimizerOptions options, Iterable fileDescriptors) { + this.options = checkNotNull(options); + this.astMutator = AstMutator.newInstance(options.iterationLimit()); + this.descriptorPool = newDescriptorPool(options, checkNotNull(fileDescriptors)); + } + /** Options configuring the behavior of {@link SelectOptimizer}. */ @AutoValue public abstract static class SelectOptimizerOptions { diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel index 787012466..1fd34709a 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -26,6 +26,7 @@ java_library( "//extensions:optional_library", # "//java/com/google/testing/testsize:annotations", "//optimizer", + "//optimizer:ast_optimizer", "//optimizer:optimization_exception", "//optimizer:optimizer_builder", "//optimizer/optimizers:common_subexpression_elimination", diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java index 7740319fe..83057e44d 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java @@ -34,8 +34,10 @@ import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelMutableAst; import dev.cel.common.CelOptions; +import dev.cel.common.CelOverloadDecl; import dev.cel.common.CelProtoAbstractSyntaxTree; import dev.cel.common.CelValidationException; +import dev.cel.common.ast.CelReference; import dev.cel.common.navigation.CelNavigableMutableAst; import dev.cel.common.types.MapType; import dev.cel.common.types.SimpleType; @@ -43,6 +45,7 @@ import dev.cel.expr.conformance.proto2.NestedTestAllTypes; import dev.cel.expr.conformance.proto2.TestAllTypesProto; import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.optimizer.CelAstOptimizer; import dev.cel.optimizer.CelOptimizer; import dev.cel.optimizer.CelOptimizerFactory; import dev.cel.optimizer.optimizers.SelectOptimizer.SelectOptimizerOptions; @@ -119,27 +122,32 @@ private static Cel setupEnv(CelBuilder celBuilder) { private enum RewriteTestCase { // === Selection & Traversal === PROTO3_SINGLE_FIELD_SELECT( - "msg.single_int64", "cel.@attribute(msg, [[2, \"single_int64\", 3, 0]])"), + "msg.single_int64", "cel.@attribute(msg, [[2, \"single_int64\", 3, 0]], int)"), PROTO3_SINGLE_MESSAGE_FIELD_SELECT( - "msg.single_nested_message", "cel.@attribute(msg, [[21, \"single_nested_message\", 11]])"), + "msg.single_nested_message", + "cel.@attribute(msg, [[21, \"single_nested_message\", 11]]," + + " cel.expr.conformance.proto3.TestAllTypes.NestedMessage)"), PROTO3_CHAINED_FIELD_SELECT( "msg.single_nested_message.bb", - "cel.@attribute(msg, [[21, \"single_nested_message\", 11], [1, \"bb\", 5, 0]])"), + "cel.@attribute(msg, [[21, \"single_nested_message\", 11], [1, \"bb\", 5, 0]], int)"), PROTO2_SINGLE_MESSAGE_FIELD_SELECT( "proto2_msg.single_nested_message", - "cel.@attribute(proto2_msg, [[21, \"single_nested_message\", 11]])"), + "cel.@attribute(proto2_msg, [[21, \"single_nested_message\", 11]]," + + " cel.expr.conformance.proto2.TestAllTypes.NestedMessage)"), PROTO2_CHAINED_FIELD_SELECT( "proto2_msg.single_nested_message.bb", - "cel.@attribute(proto2_msg, [[21, \"single_nested_message\", 11], [1, \"bb\", 5, 0]])"), + "cel.@attribute(proto2_msg, [[21, \"single_nested_message\", 11], [1, \"bb\", 5, 0]]," + + " int)"), PROTO2_TRIPLE_CHAINED_FIELD_SELECT( "nested_msg.child.payload.single_int64", "cel.@attribute(nested_msg, " + "[[1, \"child\", 11], " + "[2, \"payload\", 11], " - + "[2, \"single_int64\", 3, -64]])"), + + "[2, \"single_int64\", 3, -64]], int)"), PROTO2_CHAINED_MESSAGE_FIELD_SELECT( "nested_msg.child.payload", - "cel.@attribute(nested_msg, [[1, \"child\", 11], [2, \"payload\", 11]])"), + "cel.@attribute(nested_msg, [[1, \"child\", 11], [2, \"payload\", 11]]," + + " cel.expr.conformance.proto2.TestAllTypes)"), // === Presence Tests: Proto2 (Explicit Presence) vs Proto3 (Implicit/Explicit Presence) === // In proto2, scalar fields have explicit presence (has-bit). @@ -178,102 +186,122 @@ private enum RewriteTestCase { // === Default Value Divergence: Proto2 Custom Defaults vs Proto3 Zero Defaults === // Int32: proto2 has custom default -32, proto3 has 0 PROTO2_CUSTOM_INT32( - "proto2_msg.single_int32", "cel.@attribute(proto2_msg, [[1, \"single_int32\", 5, -32]])"), - PROTO3_ZERO_INT32("msg.single_int32", "cel.@attribute(msg, [[1, \"single_int32\", 5, 0]])"), + "proto2_msg.single_int32", + "cel.@attribute(proto2_msg, [[1, \"single_int32\", 5, -32]], int)"), + PROTO3_ZERO_INT32( + "msg.single_int32", "cel.@attribute(msg, [[1, \"single_int32\", 5, 0]], int)"), // Int64: proto2 has custom default -64, proto3 has 0 PROTO2_CUSTOM_INT64( - "proto2_msg.single_int64", "cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]])"), - PROTO3_ZERO_INT64("msg.single_int64", "cel.@attribute(msg, [[2, \"single_int64\", 3, 0]])"), + "proto2_msg.single_int64", + "cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]], int)"), + PROTO3_ZERO_INT64( + "msg.single_int64", "cel.@attribute(msg, [[2, \"single_int64\", 3, 0]], int)"), // Uint32: proto2 has custom default 32, proto3 has 0 PROTO2_CUSTOM_UINT32( "proto2_msg.single_uint32", - "cel.@attribute(proto2_msg, [[3, \"single_uint32\", 13, 32u]])"), + "cel.@attribute(proto2_msg, [[3, \"single_uint32\", 13, 32u]], uint)"), PROTO3_ZERO_UINT32( - "msg.single_uint32", "cel.@attribute(msg, [[3, \"single_uint32\", 13, 0u]])"), + "msg.single_uint32", "cel.@attribute(msg, [[3, \"single_uint32\", 13, 0u]], uint)"), // Uint64: proto2 has custom default 64, proto3 has 0 PROTO2_CUSTOM_UINT64( - "proto2_msg.single_uint64", "cel.@attribute(proto2_msg, [[4, \"single_uint64\", 4, 64u]])"), - PROTO3_ZERO_UINT64("msg.single_uint64", "cel.@attribute(msg, [[4, \"single_uint64\", 4, 0u]])"), + "proto2_msg.single_uint64", + "cel.@attribute(proto2_msg, [[4, \"single_uint64\", 4, 64u]], uint)"), + PROTO3_ZERO_UINT64( + "msg.single_uint64", "cel.@attribute(msg, [[4, \"single_uint64\", 4, 0u]], uint)"), // String: proto2 has custom default "empty", proto3 has "" PROTO2_CUSTOM_STRING( "proto2_msg.single_string", - "cel.@attribute(proto2_msg, [[14, \"single_string\", 9, \"empty\"]])"), + "cel.@attribute(proto2_msg, [[14, \"single_string\", 9, \"empty\"]], string)"), PROTO3_ZERO_STRING( - "msg.single_string", "cel.@attribute(msg, [[14, \"single_string\", 9, \"\"]])"), + "msg.single_string", "cel.@attribute(msg, [[14, \"single_string\", 9, \"\"]], string)"), // Bool: proto2 has custom default true, proto3 has false PROTO2_CUSTOM_BOOL( - "proto2_msg.single_bool", "cel.@attribute(proto2_msg, [[13, \"single_bool\", 8, true]])"), - PROTO3_ZERO_BOOL("msg.single_bool", "cel.@attribute(msg, [[13, \"single_bool\", 8, false]])"), + "proto2_msg.single_bool", + "cel.@attribute(proto2_msg, [[13, \"single_bool\", 8, true]], bool)"), + PROTO3_ZERO_BOOL( + "msg.single_bool", "cel.@attribute(msg, [[13, \"single_bool\", 8, false]], bool)"), // Float: proto2 has custom default 3.0, proto3 has 0.0 PROTO2_CUSTOM_FLOAT( - "proto2_msg.single_float", "cel.@attribute(proto2_msg, [[11, \"single_float\", 2, 3.0]])"), - PROTO3_ZERO_FLOAT("msg.single_float", "cel.@attribute(msg, [[11, \"single_float\", 2, 0.0]])"), + "proto2_msg.single_float", + "cel.@attribute(proto2_msg, [[11, \"single_float\", 2, 3.0]], double)"), + PROTO3_ZERO_FLOAT( + "msg.single_float", "cel.@attribute(msg, [[11, \"single_float\", 2, 0.0]], double)"), // Double: proto2 has custom default 6.4, proto3 has 0.0 PROTO2_CUSTOM_DOUBLE( "proto2_msg.single_double", - "cel.@attribute(proto2_msg, [[12, \"single_double\", 1, 6.4]])"), + "cel.@attribute(proto2_msg, [[12, \"single_double\", 1, 6.4]], double)"), PROTO3_ZERO_DOUBLE( - "msg.single_double", "cel.@attribute(msg, [[12, \"single_double\", 1, 0.0]])"), + "msg.single_double", "cel.@attribute(msg, [[12, \"single_double\", 1, 0.0]], double)"), // Bytes: proto2 has custom default "none", proto3 has "" PROTO2_CUSTOM_BYTES( "proto2_msg.single_bytes", - "cel.@attribute(proto2_msg, [[15, \"single_bytes\", 12, b\"\\156\\157\\156\\145\"]])"), + "cel.@attribute(proto2_msg, [[15, \"single_bytes\", 12, b\"\\156\\157\\156\\145\"]]," + + " bytes)"), PROTO3_ZERO_BYTES( - "msg.single_bytes", "cel.@attribute(msg, [[15, \"single_bytes\", 12, b\"\"]])"), + "msg.single_bytes", "cel.@attribute(msg, [[15, \"single_bytes\", 12, b\"\"]], bytes)"), // Enum: proto2 has custom default 1 (BAR), proto3 has 0 (FOO) PROTO2_CUSTOM_ENUM( "proto2_msg.single_nested_enum", - "cel.@attribute(proto2_msg, [[22, \"single_nested_enum\", 14, 1]])"), + "cel.@attribute(proto2_msg, [[22, \"single_nested_enum\", 14, 1]], int)"), PROTO3_ZERO_ENUM( - "msg.single_nested_enum", "cel.@attribute(msg, [[22, \"single_nested_enum\", 14, 0]])"), - - // Fixed / sfixed fields + "msg.single_nested_enum", + "cel.@attribute(msg, [[22, \"single_nested_enum\", 14, 0]], int)"), + + // Fixed / sfixed / sint fields + PROTO3_FIXED32( + "msg.single_fixed32", "cel.@attribute(msg, [[7, \"single_fixed32\", 7, 0u]], uint)"), + PROTO3_FIXED64( + "msg.single_fixed64", "cel.@attribute(msg, [[8, \"single_fixed64\", 6, 0u]], uint)"), PROTO3_SFIXED32( - "msg.single_sfixed32", "cel.@attribute(msg, [[9, \"single_sfixed32\", 15, 0]])"), + "msg.single_sfixed32", "cel.@attribute(msg, [[9, \"single_sfixed32\", 15, 0]], int)"), PROTO3_SFIXED64( - "msg.single_sfixed64", "cel.@attribute(msg, [[10, \"single_sfixed64\", 16, 0]])"), + "msg.single_sfixed64", "cel.@attribute(msg, [[10, \"single_sfixed64\", 16, 0]], int)"), + PROTO3_SINT32("msg.single_sint32", "cel.@attribute(msg, [[5, \"single_sint32\", 17, 0]], int)"), + PROTO3_SINT64("msg.single_sint64", "cel.@attribute(msg, [[6, \"single_sint64\", 18, 0]], int)"), // Repeated fields: empty list default PROTO2_REPEATED_PRIMITIVE( "proto2_msg.repeated_int64", - "cel.@attribute(proto2_msg, [[32, \"repeated_int64\", 3, []]])"), + "cel.@attribute(proto2_msg, [[32, \"repeated_int64\", 3, []]], list)"), PROTO3_REPEATED_PRIMITIVE( - "msg.repeated_int64", "cel.@attribute(msg, [[32, \"repeated_int64\", 3, []]])"), + "msg.repeated_int64", "cel.@attribute(msg, [[32, \"repeated_int64\", 3, []]], list)"), PROTO3_REPEATED_MESSAGE( "msg.repeated_nested_message", - "cel.@attribute(msg, [[51, \"repeated_nested_message\", 11, []]])"), + "cel.@attribute(msg, [[51, \"repeated_nested_message\", 11, []]], list)"), // Well-known types PROTO3_TIMESTAMP( "msg.single_timestamp", - "cel.@attribute(msg, [[102, \"single_timestamp\", 11, timestamp(0)]])"), + "cel.@attribute(msg, [[102, \"single_timestamp\", 11, timestamp(0)]]," + + " google.protobuf.Timestamp)"), PROTO3_DURATION( "msg.single_duration", - "cel.@attribute(msg, [[101, \"single_duration\", 11, duration(\"0s\")]])"), + "cel.@attribute(msg, [[101, \"single_duration\", 11, duration(\"0s\")]]," + + " google.protobuf.Duration)"), // Map selects MAP_FIELD_INDEXING( "msg.map_int64_message[1].bb", "cel.@attribute(" - + "cel.@attribute(msg, [[95, \"map_int64_message\", 20, {}]])[1], " - + "[[1, \"bb\", 5, 0]])"), + + "cel.@attribute(msg, [[95, \"map_int64_message\", -1, {}]], map)[1], " + + "[[1, \"bb\", 5, 0]], int)"), MAP_FIELD_SELECT_CHAIN_STOPS_AT_MAP_BOUNDARY( "map_var_msg.key.single_nested_message.bb", "cel.@attribute(map_var_msg.key, " + "[[21, \"single_nested_message\", 11], " - + "[1, \"bb\", 5, 0]])"), + + "[1, \"bb\", 5, 0]], int)"), MAP_FIELD_SELECT_STOPS_AT_MAP_BOUNDARY( "map_var_msg.key.single_int64", - "cel.@attribute(map_var_msg.key, [[2, \"single_int64\", 3, 0]])"), + "cel.@attribute(map_var_msg.key, [[2, \"single_int64\", 3, 0]], int)"), MAP_FIELD_HAS_STOPS_AT_MAP_BOUNDARY( "has(map_var_msg.key.single_nested_message)", "cel.@hasField(map_var_msg.key, [[21, \"single_nested_message\"]])"), @@ -283,17 +311,17 @@ private enum RewriteTestCase { PROTO_MAP_FIELD_SELECT_STOPS_AT_MAP_BOUNDARY( "msg.map_string_message.key.bb", "cel.@attribute(" - + "cel.@attribute(msg, [[227, \"map_string_message\", 20, {}]]).key, " - + "[[1, \"bb\", 5, 0]])"), + + "cel.@attribute(msg, [[227, \"map_string_message\", -1, {}]], map).key, " + + "[[1, \"bb\", 5, 0]], int)"), PROTO_MAP_FIELD_HAS_STOPS_AT_MAP_BOUNDARY( "has(msg.map_string_message.key.bb)", "cel.@hasField(" - + "cel.@attribute(msg, [[227, \"map_string_message\", 20, {}]]).key, " + + "cel.@attribute(msg, [[227, \"map_string_message\", -1, {}]], map).key, " + "[[1, \"bb\"]])"), MIXED_BOOLEAN_EXPRESSION( "msg.single_int64 > 0 && has(msg.single_nested_message)", - "cel.@attribute(msg, [[2, \"single_int64\", 3, 0]]) > 0 " + "cel.@attribute(msg, [[2, \"single_int64\", 3, 0]], int) > 0 " + "&& cel.@hasField(msg, [[21, \"single_nested_message\"]])"); private final String expression; @@ -376,7 +404,7 @@ public void optimize_withFileDescriptors_success() throws Exception { CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); assertThat(CEL_UNPARSER.unparse(optimizedAst)) - .isEqualTo("cel.@attribute(msg, [[2, \"single_int64\", 3, 0]])"); + .isEqualTo("cel.@attribute(msg, [[2, \"single_int64\", 3, 0]], int)"); } @Test @@ -394,7 +422,7 @@ public void optimize_withFileDescriptorsIterable_success() throws Exception { CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); assertThat(CEL_UNPARSER.unparse(optimizedAst)) - .isEqualTo("cel.@attribute(msg, [[2, \"single_int64\", 3, 0]])"); + .isEqualTo("cel.@attribute(msg, [[2, \"single_int64\", 3, 0]], int)"); } @Test @@ -409,7 +437,7 @@ public void newInstance_withOptionsAndFileDescriptors_preservesAddedDescriptors( CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast, cel).optimizedAst(); assertThat(CEL_UNPARSER.unparse(optimizedAst)) - .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]])"); + .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]], int)"); } @Test @@ -459,7 +487,9 @@ public void optimizeAndEvaluate_withAttributeFunctionBinding_evaluatesSuccessful .addFunctionDeclarations(SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL) .addFunctionBindings( CelFunctionBinding.from( - "cel_attribute_list", Object.class, List.class, (target, path) -> 42L)) + "cel_attribute_list", + ImmutableList.of(Object.class, List.class, Object.class), + args -> 42L)) .build(); CelOptimizer optimizer = CelOptimizerFactory.standardCelOptimizerBuilder(celWithBinding) @@ -487,7 +517,9 @@ public void optimizeAndEvaluate_withChainedMessageSelect_unpacksTuplesSuccessful .addFunctionDeclarations(SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL) .addFunctionBindings( CelFunctionBinding.from( - "cel_attribute_list", Object.class, List.class, (target, path) -> path)) + "cel_attribute_list", + ImmutableList.of(Object.class, List.class, Object.class), + args -> args[1])) .build(); CelOptimizer optimizer = CelOptimizerFactory.standardCelOptimizerBuilder(celWithBinding) @@ -545,7 +577,9 @@ public void optimizeAndEvaluate_withSelectOnMapValue_evaluatesSuccessfully() thr .addFunctionDeclarations(SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL) .addFunctionBindings( CelFunctionBinding.from( - "cel_attribute_list", Object.class, List.class, (target, path) -> 42L)) + "cel_attribute_list", + ImmutableList.of(Object.class, List.class, Object.class), + args -> 42L)) .build(); CelOptimizer optimizer = CelOptimizerFactory.standardCelOptimizerBuilder(celWithBinding) @@ -608,7 +642,9 @@ public void optimizeAndEvaluate_withMissingMapKey_throwsEvaluationException() th .addFunctionDeclarations(SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL) .addFunctionBindings( CelFunctionBinding.from( - "cel_attribute_list", Object.class, List.class, (target, path) -> 42L)) + "cel_attribute_list", + ImmutableList.of(Object.class, List.class, Object.class), + args -> 42L)) .build(); CelOptimizer optimizer = CelOptimizerFactory.standardCelOptimizerBuilder(celWithBinding) @@ -744,7 +780,7 @@ public void newInstance_fileDescriptorsVarargs_defaultOptions_success() throws E CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast, cel).optimizedAst(); assertThat(CEL_UNPARSER.unparse(optimizedAst)) - .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]])"); + .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]], int)"); } @Test @@ -756,7 +792,7 @@ public void newInstance_fileDescriptorsIterable_defaultOptions_success() throws CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast, cel).optimizedAst(); assertThat(CEL_UNPARSER.unparse(optimizedAst)) - .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]])"); + .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]], int)"); } @Test @@ -774,7 +810,7 @@ public void newInstance_fileDescriptorsIterable_defaultOptions_success() throws CelAbstractSyntaxTree proto3Optimized = optimizer.optimize(proto3Ast, cel).optimizedAst(); assertThat(CEL_UNPARSER.unparse(proto2Optimized)) - .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]])"); + .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]], int)"); assertThat(CEL_UNPARSER.unparse(proto3Optimized)).isEqualTo("msg.single_int64"); } @@ -794,18 +830,18 @@ public void newInstance_fileDescriptorsIterable_defaultOptions_success() throws CelAbstractSyntaxTree proto3Optimized = optimizer.optimize(proto3Ast, cel).optimizedAst(); assertThat(CEL_UNPARSER.unparse(proto2Optimized)) - .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]])"); + .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]], int)"); assertThat(CEL_UNPARSER.unparse(proto3Optimized)).isEqualTo("msg.single_int64"); } private enum CompilerRejectionTestCase { ATTRIBUTE_AT_SIGN( SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL, - "cel.@attribute(msg, [])", + "cel.@attribute(msg, [], int)", "token recognition error at: '@'"), ATTRIBUTE_OVERLOAD( SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL, - "cel_attribute_list(msg, [])", + "cel_attribute_list(msg, [], int)", "undeclared reference to 'cel_attribute_list'"), HAS_FIELD_AT_SIGN( SelectOptimizer.CEL_HAS_FIELD_FUNCTION_DECL, @@ -912,6 +948,12 @@ public void optimize_toParsedExpr_matchesExpectedSerializedProto() throws Except + " }\n" + " }\n" + " }\n" + + " args {\n" + + " id: 13\n" + + " ident_expr {\n" + + " name: \"int\"\n" + + " }\n" + + " }\n" + " }\n" + "}\n" + "source_info {\n" @@ -931,4 +973,70 @@ public void optimize_toParsedExpr_matchesExpectedSerializedProto() throws Except assertThat(parsedExpr).isEqualTo(expectedParsedExpr); } + + @Test + public void + optimize_binaryOperationOnOptimizedSelect_resolvesOverloadAndPreservesConcreteResultType() + throws Exception { + CelAbstractSyntaxTree ast = cel.compile("msg.single_int64 + 1").getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(optimizedAst.getResultType()).isEqualTo(SimpleType.INT); + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo("cel.@attribute(msg, [[2, \"single_int64\", 3, 0]], int) + 1"); + } + + @Test + public void + optimize_stringOperationOnOptimizedSelect_resolvesOverloadAndPreservesConcreteResultType() + throws Exception { + CelAbstractSyntaxTree ast = cel.compile("msg.single_string + 'suffix'").getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(optimizedAst.getResultType()).isEqualTo(SimpleType.STRING); + assertThat(CEL_UNPARSER.unparse(optimizedAst)) + .isEqualTo("cel.@attribute(msg, [[14, \"single_string\", 9, \"\"]], string) + \"suffix\""); + } + + @Test + public void optimize_resultFunctionDeclarations_containsOnlySingularAttributeAndHasField() + throws Exception { + SelectOptimizer optimizer = SelectOptimizer.newInstance(TestAllTypes.getDescriptor().getFile()); + CelAbstractSyntaxTree ast = cel.compile("msg.single_int64").getAst(); + + CelAstOptimizer.OptimizationResult result = optimizer.optimize(ast, cel); + + assertThat(result.newFunctionDecls()) + .containsExactly( + SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL, + SelectOptimizer.CEL_HAS_FIELD_FUNCTION_DECL); + assertThat( + SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL.overloads().stream() + .map(CelOverloadDecl::overloadId)) + .containsExactly("cel_attribute_list"); + } + + @Test + public void optimize_referenceMap_containsSingleOverloadIdForAttributeCall() throws Exception { + CelAbstractSyntaxTree ast = cel.compile("msg.single_int64").getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + CelReference reference = optimizedAst.getReferenceOrThrow(optimizedAst.getExpr().id()); + assertThat(reference.overloadIds()).containsExactly("cel_attribute_list"); + } + + @Test + public void optimize_binaryOperationBetweenOptimizedSelects_resolvesSingleOverloadInReferenceMap() + throws Exception { + CelAbstractSyntaxTree ast = cel.compile("msg.single_int64 + msg.single_sint64").getAst(); + + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + assertThat(optimizedAst.getResultType()).isEqualTo(SimpleType.INT); + CelReference addReference = optimizedAst.getReferenceOrThrow(optimizedAst.getExpr().id()); + assertThat(addReference.overloadIds()).containsExactly("add_int64"); + } } From e1a373b922088fdde844ec5c6c3ddb98f4b050f9 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Tue, 15 Sep 2026 13:37:47 -0700 Subject: [PATCH 203/204] Add top-level APIs for instantiating pratt parser in Android PiperOrigin-RevId: 982012899 --- common/BUILD.bazel | 65 ++++- common/ast/BUILD.bazel | 5 + .../src/main/java/dev/cel/common/BUILD.bazel | 152 ++++++++++-- .../cel/common/CelValidationException.java | 9 +- .../main/java/dev/cel/common/ast/BUILD.bazel | 13 + parser/BUILD.bazel | 33 +++ .../src/main/java/dev/cel/parser/BUILD.bazel | 159 +++++++++++- .../dev/cel/parser/CelLiteParserFactory.java | 38 +++ .../main/java/dev/cel/parser/CelMacro.java | 4 +- .../java/dev/cel/parser/CelParserBase.java | 227 +++++++++++++++++ .../java/dev/cel/parser/CelParserImpl.java | 183 ++------------ .../java/dev/cel/parser/LiteParserImpl.java | 89 +++++++ .../main/java/dev/cel/parser/PrattParser.java | 18 +- .../src/test/java/dev/cel/parser/BUILD.bazel | 24 +- .../cel/parser/CelLiteParserAndroidTest.java | 40 +++ .../cel/parser/CelLiteParserFactoryTest.java | 229 ++++++++++++++++++ 16 files changed, 1080 insertions(+), 208 deletions(-) create mode 100644 parser/src/main/java/dev/cel/parser/CelLiteParserFactory.java create mode 100644 parser/src/main/java/dev/cel/parser/CelParserBase.java create mode 100644 parser/src/main/java/dev/cel/parser/LiteParserImpl.java create mode 100644 parser/src/test/java/dev/cel/parser/CelLiteParserAndroidTest.java create mode 100644 parser/src/test/java/dev/cel/parser/CelLiteParserFactoryTest.java diff --git a/common/BUILD.bazel b/common/BUILD.bazel index b67069de7..9cb0c2f7b 100644 --- a/common/BUILD.bazel +++ b/common/BUILD.bazel @@ -8,7 +8,65 @@ package( java_library( name = "compiler_common", - exports = ["//common/src/main/java/dev/cel/common:compiler_common"], + deprecation = "Please use the granular targets (e.g. :cel_issue, :cel_validation_result, etc.) instead.", + exports = [ + ":cel_function_decl", + ":cel_issue", + ":cel_overload_decl", + ":cel_validation_exception", + ":cel_validation_result", + ":cel_var_decl", + ], +) + +java_library( + name = "cel_function_decl", + exports = ["//common/src/main/java/dev/cel/common:cel_function_decl"], +) + +java_library( + name = "cel_overload_decl", + exports = ["//common/src/main/java/dev/cel/common:cel_overload_decl"], +) + +java_library( + name = "cel_var_decl", + exports = ["//common/src/main/java/dev/cel/common:cel_var_decl"], +) + +cel_android_library( + name = "cel_var_decl_android", + exports = ["//common/src/main/java/dev/cel/common:cel_var_decl_android"], +) + +java_library( + name = "cel_issue", + exports = ["//common/src/main/java/dev/cel/common:cel_issue"], +) + +cel_android_library( + name = "cel_issue_android", + exports = ["//common/src/main/java/dev/cel/common:cel_issue_android"], +) + +java_library( + name = "cel_validation_exception", + exports = ["//common/src/main/java/dev/cel/common:cel_validation_exception"], +) + +cel_android_library( + name = "cel_validation_exception_android", + exports = ["//common/src/main/java/dev/cel/common:cel_validation_exception_android"], +) + +java_library( + name = "cel_validation_result", + exports = ["//common/src/main/java/dev/cel/common:cel_validation_result"], +) + +cel_android_library( + name = "cel_validation_result_android", + exports = ["//common/src/main/java/dev/cel/common:cel_validation_result_android"], ) java_library( @@ -74,6 +132,11 @@ java_library( exports = ["//common/src/main/java/dev/cel/common:source_location"], ) +cel_android_library( + name = "source_location_android", + exports = ["//common/src/main/java/dev/cel/common:source_location_android"], +) + java_library( name = "cel_source", exports = ["//common/src/main/java/dev/cel/common:cel_source"], diff --git a/common/ast/BUILD.bazel b/common/ast/BUILD.bazel index 1361ad76b..9b7573a7c 100644 --- a/common/ast/BUILD.bazel +++ b/common/ast/BUILD.bazel @@ -53,6 +53,11 @@ java_library( exports = ["//common/src/main/java/dev/cel/common/ast:expr_factory"], ) +cel_android_library( + name = "expr_factory_android", + exports = ["//common/src/main/java/dev/cel/common/ast:expr_factory_android"], +) + java_library( name = "mutable_expr", exports = ["//common/src/main/java/dev/cel/common/ast:mutable_expr"], diff --git a/common/src/main/java/dev/cel/common/BUILD.bazel b/common/src/main/java/dev/cel/common/BUILD.bazel index 11b762220..73475e623 100644 --- a/common/src/main/java/dev/cel/common/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/BUILD.bazel @@ -9,16 +9,6 @@ package( ], ) -# keep sorted -COMPILER_COMMON_SOURCES = [ - "CelFunctionDecl.java", - "CelIssue.java", - "CelOverloadDecl.java", - "CelValidationException.java", - "CelValidationResult.java", - "CelVarDecl.java", -] - # keep sorted SOURCE_SOURCES = [ "Source.java", @@ -67,28 +57,151 @@ java_library( ) java_library( - name = "compiler_common", - srcs = COMPILER_COMMON_SOURCES, + name = "cel_function_decl", + srcs = ["CelFunctionDecl.java"], tags = [ ], deps = [ - ":cel_ast", - ":cel_exception", - ":cel_source", - ":source", - ":source_location", + ":cel_overload_decl", "//:auto_value", "//common/annotations", - "//common/internal:safe_string_formatter", + "@cel_spec//proto/cel/expr:checked_java_proto", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "cel_overload_decl", + srcs = ["CelOverloadDecl.java"], + tags = [ + ], + deps = [ + "//:auto_value", "//common/types:cel_proto_types", "//common/types:type_providers", "@cel_spec//proto/cel/expr:checked_java_proto", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "cel_var_decl", + srcs = ["CelVarDecl.java"], + tags = [ + ], + deps = [ + "//:auto_value", + "//common/types:type_providers", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "cel_var_decl_android", + srcs = ["CelVarDecl.java"], + tags = [ + ], + deps = [ + "//:auto_value", + "//common/types:type_providers_android", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "cel_issue", + srcs = ["CelIssue.java"], + tags = [ + ], + deps = [ + ":source", + ":source_location", + "//:auto_value", + "//common/internal:safe_string_formatter", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "cel_issue_android", + srcs = ["CelIssue.java"], + tags = [ + ], + deps = [ + ":source_android", + ":source_location_android", + "//:auto_value", + "//common/internal:safe_string_formatter", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "cel_validation_exception", + srcs = ["CelValidationException.java"], + tags = [ + ], + deps = [ + ":cel_exception", + ":cel_issue", + ":cel_source", + "//common/annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "cel_validation_exception_android", + srcs = ["CelValidationException.java"], + tags = [ + ], + deps = [ + ":cel_exception", + ":cel_issue_android", + ":cel_source_android", + "//common/annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "cel_validation_result", + srcs = ["CelValidationResult.java"], + tags = [ + ], + deps = [ + ":cel_ast", + ":cel_issue", + ":cel_source", + ":cel_validation_exception", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", "@maven//:org_jspecify_jspecify", ], ) +cel_android_library( + name = "cel_validation_result_android", + srcs = ["CelValidationResult.java"], + tags = [ + ], + deps = [ + ":cel_ast_android", + ":cel_issue_android", + ":cel_source_android", + ":cel_validation_exception_android", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "cel_exception", srcs = ["CelException.java"], @@ -345,7 +458,8 @@ cel_android_library( cel_android_library( name = "source_location_android", srcs = ["CelSourceLocation.java"], - visibility = ["//visibility:private"], + tags = [ + ], deps = [ "//:auto_value", "@maven//:com_google_errorprone_error_prone_annotations", diff --git a/common/src/main/java/dev/cel/common/CelValidationException.java b/common/src/main/java/dev/cel/common/CelValidationException.java index 18bec2fe6..edbd9a0c0 100644 --- a/common/src/main/java/dev/cel/common/CelValidationException.java +++ b/common/src/main/java/dev/cel/common/CelValidationException.java @@ -14,8 +14,8 @@ package dev.cel.common; -import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import dev.cel.common.annotations.Internal; import java.util.List; /** Base class for all checked exceptions explicitly thrown by the library during parsing. */ @@ -27,7 +27,7 @@ public final class CelValidationException extends CelException { private final CelSource source; private final ImmutableList errors; - @VisibleForTesting + @Internal public CelValidationException(CelSource source, List errors) { super(safeJoinErrorMessage(source, errors)); this.source = source; @@ -49,8 +49,9 @@ private static String safeJoinErrorMessage(CelSource source, List erro List truncatedErrors = errors.subList(0, MAX_ERRORS_TO_REPORT); return CelIssue.toDisplayString(truncatedErrors, source) - + String.format( - "%n...and %d more errors (truncated)", errors.size() - MAX_ERRORS_TO_REPORT); + + "\n...and " + + (errors.size() - MAX_ERRORS_TO_REPORT) + + " more errors (truncated)"; } /** Returns the {@link CelSource} that was being validated. */ diff --git a/common/src/main/java/dev/cel/common/ast/BUILD.bazel b/common/src/main/java/dev/cel/common/ast/BUILD.bazel index c72857080..14cb75dd9 100644 --- a/common/src/main/java/dev/cel/common/ast/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/ast/BUILD.bazel @@ -156,6 +156,19 @@ java_library( ], ) +cel_android_library( + name = "expr_factory_android", + srcs = EXPR_FACTORY_SOURCES, + tags = [ + ], + deps = [ + ":ast_android", + "//common/annotations", + "//common/values:cel_byte_string", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "mutable_expr", srcs = MUTABLE_EXPR_SOURCES, diff --git a/parser/BUILD.bazel b/parser/BUILD.bazel index 8bd568183..38c7bbea3 100644 --- a/parser/BUILD.bazel +++ b/parser/BUILD.bazel @@ -1,4 +1,5 @@ load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") package( default_applicable_licenses = ["//:license"], @@ -22,11 +23,38 @@ java_library( exports = ["//parser/src/main/java/dev/cel/parser:parser_factory"], ) +java_library( + name = "lite_parser_factory", + exports = ["//parser/src/main/java/dev/cel/parser:lite_parser_factory"], +) + +cel_android_library( + name = "lite_parser_factory_android", + exports = ["//parser/src/main/java/dev/cel/parser:lite_parser_factory_android"], +) + +java_library( + name = "parser_base", + visibility = ["//:internal"], + exports = ["//parser/src/main/java/dev/cel/parser:parser_base"], +) + +cel_android_library( + name = "parser_base_android", + visibility = ["//:internal"], + exports = ["//parser/src/main/java/dev/cel/parser:parser_base_android"], +) + java_library( name = "parser_builder", exports = ["//parser/src/main/java/dev/cel/parser:parser_builder"], ) +cel_android_library( + name = "parser_builder_android", + exports = ["//parser/src/main/java/dev/cel/parser:parser_builder_android"], +) + java_library( name = "unparser_visitor", exports = ["//parser/src/main/java/dev/cel/parser:unparser_visitor"], @@ -37,6 +65,11 @@ java_library( exports = ["//parser/src/main/java/dev/cel/parser:macro"], ) +cel_android_library( + name = "macro_android", + exports = ["//parser/src/main/java/dev/cel/parser:macro_android"], +) + java_library( name = "cel_g4_visitors", visibility = ["//:internal"], diff --git a/parser/src/main/java/dev/cel/parser/BUILD.bazel b/parser/src/main/java/dev/cel/parser/BUILD.bazel index 848209380..e0bd48e12 100644 --- a/parser/src/main/java/dev/cel/parser/BUILD.bazel +++ b/parser/src/main/java/dev/cel/parser/BUILD.bazel @@ -1,4 +1,5 @@ load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_library") package( default_applicable_licenses = ["//:license"], @@ -8,18 +9,29 @@ package( ], ) -# keep sorted -PARSER_SOURCES = [ - "CelParserImpl.java", - "Parser.java", -] - # keep sorted ANTLR_PARSER_SOURCES = [ "AntlrParser.java", "ExpressionBalancer.java", ] +# keep sorted +LITE_PARSER_FACTORY_SOURCES = [ + "CelLiteParserFactory.java", + "LiteParserImpl.java", +] + +# keep sorted +PARSER_BASE_SOURCES = [ + "CelParserBase.java", +] + +# keep sorted +PARSER_SOURCES = [ + "CelParserImpl.java", + "Parser.java", +] + # keep sorted PRATT_PARSER_SOURCES = [ "Lexer.java", @@ -60,6 +72,78 @@ java_library( ], ) +java_library( + name = "parser_base", + srcs = PARSER_BASE_SOURCES, + tags = [ + ], + deps = [ + ":macro", + ":parser_builder", + "//common:cel_source", + "//common:cel_validation_result", + "//common:options", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "parser_base_android", + srcs = PARSER_BASE_SOURCES, + tags = [ + ], + deps = [ + ":macro_android", + ":parser_builder_android", + "//common:cel_source_android", + "//common:cel_validation_result_android", + "//common:options", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "lite_parser_factory", + srcs = LITE_PARSER_FACTORY_SOURCES, + tags = [ + ], + deps = [ + ":macro", + ":parser_base", + ":parser_builder", + ":pratt_parser", + "//common:cel_source", + "//common:cel_validation_result", + "//common:options", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "lite_parser_factory_android", + srcs = LITE_PARSER_FACTORY_SOURCES, + tags = [ + ], + deps = [ + ":macro_android", + ":parser_base_android", + ":parser_builder_android", + ":pratt_parser_android", + "//common:cel_source_android", + "//common:cel_validation_result_android", + "//common:options", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "parser", srcs = PARSER_SOURCES, @@ -68,10 +152,11 @@ java_library( deps = [ ":antlr_parser", ":macro", + ":parser_base", ":parser_builder", ":pratt_parser", "//common:cel_source", - "//common:compiler_common", + "//common:cel_validation_result", "//common:options", "//common/annotations", "//common/internal:env_visitor", @@ -88,8 +173,9 @@ java_library( deps = [ ":macro", "//common:cel_ast", + "//common:cel_issue", "//common:cel_source", - "//common:compiler_common", + "//common:cel_validation_result", "//common:operator", "//common:options", "//common:source_location", @@ -111,8 +197,9 @@ java_library( deps = [ ":macro", "//common:cel_ast", + "//common:cel_issue", "//common:cel_source", - "//common:compiler_common", + "//common:cel_validation_result", "//common:operator", "//common:options", "//common:source_location", @@ -123,6 +210,25 @@ java_library( ], ) +cel_android_library( + name = "pratt_parser_android", + srcs = PRATT_PARSER_SOURCES, + deps = [ + ":macro_android", + "//common:cel_ast_android", + "//common:cel_issue_android", + "//common:cel_source_android", + "//common:cel_validation_result_android", + "//common:operator_android", + "//common:options", + "//common:source_location_android", + "//common/ast:ast_android", + "//common/internal:internal_android", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "parser_builder", srcs = PARSER_BUILDER_SOURCES, @@ -131,7 +237,21 @@ java_library( deps = [ ":macro", "//common:cel_source", - "//common:compiler_common", + "//common:cel_validation_result", + "//common:options", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "parser_builder_android", + srcs = PARSER_BUILDER_SOURCES, + tags = [ + ], + deps = [ + ":macro_android", + "//common:cel_source_android", + "//common:cel_validation_result_android", "//common:options", "@maven//:com_google_errorprone_error_prone_annotations", ], @@ -144,7 +264,7 @@ java_library( ], deps = [ "//:auto_value", - "//common:compiler_common", + "//common:cel_issue", "//common:operator", "//common:source_location", "//common/ast", @@ -154,6 +274,23 @@ java_library( ], ) +cel_android_library( + name = "macro_android", + srcs = MACRO_SOURCES, + tags = [ + ], + deps = [ + "//:auto_value", + "//common:cel_issue_android", + "//common:operator_android", + "//common:source_location_android", + "//common/ast:ast_android", + "//common/ast:expr_factory_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "unparser", srcs = UNPARSER_SOURCES, diff --git a/parser/src/main/java/dev/cel/parser/CelLiteParserFactory.java b/parser/src/main/java/dev/cel/parser/CelLiteParserFactory.java new file mode 100644 index 000000000..3bfce1c69 --- /dev/null +++ b/parser/src/main/java/dev/cel/parser/CelLiteParserFactory.java @@ -0,0 +1,38 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.parser; + +import dev.cel.common.CelOptions; +import dev.cel.common.annotations.Beta; + +/** + * Factory class for producing light-weight {@link CelParser} instances and builders using the Pratt + * parser. + */ +@Beta +public final class CelLiteParserFactory { + + /** + * Configure a builder to construct a light-weight {@code CelParser} instance. + * + *

Note, the {@link CelOptions#current} are enabled by default with {@link + * CelOptions#enablePrattParser()} explicitly set to {@code true}. + */ + public static CelParserBuilder newLiteParserBuilder() { + return LiteParserImpl.newBuilder(); + } + + private CelLiteParserFactory() {} +} diff --git a/parser/src/main/java/dev/cel/parser/CelMacro.java b/parser/src/main/java/dev/cel/parser/CelMacro.java index cf75714a0..243baf477 100644 --- a/parser/src/main/java/dev/cel/parser/CelMacro.java +++ b/parser/src/main/java/dev/cel/parser/CelMacro.java @@ -158,12 +158,12 @@ public static CelMacro newReceiverVarArgMacro(String function, CelMacroExpander static String formatKey(String function, int argCount, boolean receiverStyle) { checkArgument(!isNullOrEmpty(function)); checkArgument(argCount >= 0); - return String.format("%s:%d:%s", function, argCount, receiverStyle); + return function + ":" + argCount + ":" + receiverStyle; } static String formatVarArgKey(String function, boolean receiverStyle) { checkArgument(!isNullOrEmpty(function)); - return String.format("%s:*:%s", function, receiverStyle); + return function + ":*:" + receiverStyle; } @AutoValue.Builder diff --git a/parser/src/main/java/dev/cel/parser/CelParserBase.java b/parser/src/main/java/dev/cel/parser/CelParserBase.java new file mode 100644 index 000000000..8f528e9f3 --- /dev/null +++ b/parser/src/main/java/dev/cel/parser/CelParserBase.java @@ -0,0 +1,227 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.parser; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.CelOptions; +import dev.cel.common.CelSource; +import dev.cel.common.CelValidationResult; +import dev.cel.common.annotations.Internal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Abstract base class containing shared logic for CEL parser implementations. + * + *

CEL Library Internals. Do Not Use. + */ +@Immutable +@Internal +abstract class CelParserBase implements CelParser { + + private final ImmutableMap macros; + private final ImmutableMap customMacros; + private final CelOptions options; + private final ImmutableList standardMacros; + + @SuppressWarnings("Immutable") // Interface not marked as immutable, however it should be. + private final ImmutableSet parserLibraries; + + @Override + public final CelValidationResult parse(String expression, String description) { + return parse(CelSource.newBuilder(expression).setDescription(description).build()); + } + + /** Return the options the {@link CelParser} was originally created with. */ + final CelOptions getOptions() { + return options; + } + + final ImmutableMap getMacros() { + return macros; + } + + final ImmutableList getStandardMacros() { + return standardMacros; + } + + final ImmutableSet getParserLibraries() { + return parserLibraries; + } + + final Optional findMacro(String key) { + return Optional.ofNullable(macros.get(key)); + } + + @CanIgnoreReturnValue + protected final > B populateBuilder(B builder) { + checkNotNull(builder); + + return builder + .setOptions(options) + .setStandardMacros(standardMacros) + .addMacros(customMacros.values()) + .addLibraries(parserLibraries); + } + + abstract static class Builder> implements CelParserBuilder { + + private final Set standardMacros; + private final Map macros; + private final Set celParserLibraries; + private CelOptions options; + + @SuppressWarnings("unchecked") // Safe cast for fluent builder chaining in subclasses. + protected B self() { + return (B) this; + } + + @CanIgnoreReturnValue + @Override + public B setStandardMacros(CelStandardMacro... macros) { + checkNotNull(macros); + return setStandardMacros(Arrays.asList(macros)); + } + + @CanIgnoreReturnValue + @Override + public B setStandardMacros(Iterable macros) { + checkNotNull(macros); + this.standardMacros.clear(); + for (CelStandardMacro macro : macros) { + this.standardMacros.add(checkNotNull(macro)); + } + return self(); + } + + @CanIgnoreReturnValue + @Override + public B addMacros(CelMacro... macros) { + checkNotNull(macros); + return addMacros(Arrays.asList(macros)); + } + + @CanIgnoreReturnValue + @Override + public B addMacros(Iterable macros) { + checkNotNull(macros); + for (CelMacro m : macros) { + CelMacro macro = checkNotNull(m); + this.macros.put(macro.getKey(), macro); + } + return self(); + } + + @CanIgnoreReturnValue + @Override + public B addLibraries(CelParserLibrary... libraries) { + checkNotNull(libraries); + return this.addLibraries(Arrays.asList(libraries)); + } + + @CanIgnoreReturnValue + @Override + public B addLibraries(Iterable libraries) { + checkNotNull(libraries); + for (CelParserLibrary library : libraries) { + this.celParserLibraries.add(checkNotNull(library)); + } + return self(); + } + + @CanIgnoreReturnValue + @Override + public B setOptions(CelOptions options) { + this.options = checkNotNull(options); + return self(); + } + + @Override + public CelOptions getOptions() { + return this.options; + } + + // Exists for test assertions in CelParserImplTest. + List getStandardMacros() { + return new ArrayList<>(this.standardMacros); + } + + // Exists for test assertions in CelParserImplTest. + Map getMacros() { + return this.macros; + } + + // Exists for test assertions in CelParserImplTest. + ImmutableSet.Builder getParserLibraries() { + return ImmutableSet.builder().addAll(this.celParserLibraries); + } + + protected ImmutableList buildStandardMacros() { + return ImmutableList.copyOf(standardMacros); + } + + protected ImmutableMap buildCustomMacros() { + return ImmutableMap.copyOf(macros); + } + + protected ImmutableMap buildMacroMap() { + ImmutableMap.Builder macroMapBuilder = ImmutableMap.builder(); + macroMapBuilder.putAll(macros); + for (CelStandardMacro standardMacro : standardMacros) { + CelMacro celMacro = standardMacro.getDefinition(); + macroMapBuilder.put(celMacro.getKey(), celMacro); + } + return macroMapBuilder.buildOrThrow(); + } + + protected ImmutableSet buildLibraries() { + ImmutableSet parserLibrarySet = ImmutableSet.copyOf(celParserLibraries); + parserLibrarySet.forEach(celLibrary -> celLibrary.setParserOptions(this)); + return parserLibrarySet; + } + + protected Builder() { + this.macros = new HashMap<>(); + this.celParserLibraries = new LinkedHashSet<>(); + this.standardMacros = new LinkedHashSet<>(); + this.options = CelOptions.DEFAULT; + } + } + + protected CelParserBase( + ImmutableMap allMacros, + ImmutableMap customMacros, + CelOptions options, + ImmutableList standardMacros, + ImmutableSet parserLibraries) { + this.macros = checkNotNull(allMacros); + this.customMacros = checkNotNull(customMacros); + this.options = checkNotNull(options); + this.standardMacros = checkNotNull(standardMacros); + this.parserLibraries = checkNotNull(parserLibraries); + } +} diff --git a/parser/src/main/java/dev/cel/parser/CelParserImpl.java b/parser/src/main/java/dev/cel/parser/CelParserImpl.java index 8ee9a3457..ecd018653 100644 --- a/parser/src/main/java/dev/cel/parser/CelParserImpl.java +++ b/parser/src/main/java/dev/cel/parser/CelParserImpl.java @@ -15,14 +15,10 @@ package dev.cel.parser; import static com.google.common.base.Preconditions.checkNotNull; -import static java.util.stream.Collectors.toCollection; -import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; -import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; import dev.cel.common.CelOptions; @@ -31,14 +27,6 @@ import dev.cel.common.annotations.Internal; import dev.cel.common.internal.EnvVisitable; import dev.cel.common.internal.EnvVisitor; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; /** * Modernized parser implementation for CEL. @@ -48,188 +36,53 @@ */ @Immutable @Internal -public final class CelParserImpl implements CelParser, EnvVisitable { - - // Common feature flags to be used with all calls. - - // Set of macros configured for parsing. - private final ImmutableMap macros; - - // Specific options for limits on parsing power. - private final CelOptions options; - - private final ImmutableList standardMacros; - - @SuppressWarnings("Immutable") // Interface not marked as immutable, however it should be. - private final ImmutableSet parserLibraries; +public final class CelParserImpl extends CelParserBase implements EnvVisitable { /** Creates a new {@link Builder}. */ - public static CelParserBuilder newBuilder() { + public static Builder newBuilder() { return new Builder().setOptions(CelOptions.DEFAULT); } - @Override - public CelValidationResult parse(String expression, String description) { - return parse(CelSource.newBuilder(expression).setDescription(description).build()); - } - @Override public CelValidationResult parse(CelSource source) { - return Parser.parse(this, source, getOptions()); + return Parser.parse(this, checkNotNull(source), getOptions()); } @Override - public CelParserBuilder toParserBuilder() { - HashSet standardMacroKeys = - standardMacros.stream() - .map(s -> s.getDefinition().getKey()) - .collect(Collectors.toCollection(HashSet::new)); - - return new Builder() - .setOptions(options) - .setStandardMacros(standardMacros) - .addMacros( - // Separate standard macros from the custom macros before constructing the builder - macros.values().stream() - .filter(m -> !standardMacroKeys.contains(m.getKey())) - .collect(toCollection(ArrayList::new))) - .addLibraries(parserLibraries); - } - - Optional findMacro(String key) { - return Optional.ofNullable(macros.get(key)); + public Builder toParserBuilder() { + return populateBuilder(new Builder()); } - ImmutableMap getMacros() { - return macros; - } - - /** Return the options the {@link CelParser} was originally created with. */ - public CelOptions getOptions() { - return options; + @Override + public void accept(EnvVisitor visitor) { + getMacros().forEach((name, macro) -> visitor.visitMacro(macro)); } /** Builder for {@link CelParserImpl}. */ - public static final class Builder implements CelParserBuilder { - - private final List standardMacros; - private final Map macros; - private final ImmutableSet.Builder celParserLibraries; - private CelOptions options; - - @Override - public CelParserBuilder setStandardMacros(CelStandardMacro... macros) { - checkNotNull(macros); - return setStandardMacros(Arrays.asList(macros)); - } - - @Override - public CelParserBuilder setStandardMacros(Iterable macros) { - checkNotNull(macros); - this.standardMacros.clear(); - Iterables.addAll(this.standardMacros, macros); - return this; - } - - @Override - public CelParserBuilder addMacros(CelMacro... macros) { - checkNotNull(macros); - return addMacros(Arrays.asList(macros)); - } - - @Override - public CelParserBuilder addMacros(Iterable macros) { - checkNotNull(macros); - for (CelMacro m : macros) { - CelMacro macro = checkNotNull(m); - this.macros.put(macro.getKey(), macro); - } - return this; - } - - @Override - public CelParserBuilder addLibraries(CelParserLibrary... libraries) { - checkNotNull(libraries); - return this.addLibraries(Arrays.asList(libraries)); - } - - @Override - public CelParserBuilder addLibraries(Iterable libraries) { - checkNotNull(libraries); - this.celParserLibraries.addAll(libraries); - return this; - } - - @CanIgnoreReturnValue - @Override - public Builder setOptions(CelOptions options) { - this.options = checkNotNull(options); - return this; - } - - @Override - public CelOptions getOptions() { - return this.options; - } - - // The following getters exist for asserting immutability for collections held by this builder, - // and shouldn't be exposed to the public. - @VisibleForTesting - List getStandardMacros() { - return this.standardMacros; - } - - @VisibleForTesting - Map getMacros() { - return this.macros; - } - - @VisibleForTesting - ImmutableSet.Builder getParserLibraries() { - return this.celParserLibraries; - } + public static final class Builder extends CelParserBase.Builder { @Override @CheckReturnValue public CelParserImpl build() { - ImmutableSet parserLibrarySet = celParserLibraries.build(); - - // Add libraries, such as extensions - parserLibrarySet.forEach(celLibrary -> celLibrary.setParserOptions(this)); - - ImmutableMap.Builder macroMapBuilder = ImmutableMap.builder(); - macroMapBuilder.putAll(macros); - standardMacros.stream() - .map(CelStandardMacro::getDefinition) - .forEach(celMacro -> macroMapBuilder.put(celMacro.getKey(), celMacro)); + ImmutableSet parserLibrarySet = buildLibraries(); return new CelParserImpl( - macroMapBuilder.buildOrThrow(), - options, - ImmutableList.copyOf(standardMacros), - celParserLibraries.build()); + buildMacroMap(), + buildCustomMacros(), + getOptions(), + buildStandardMacros(), + parserLibrarySet); } - private Builder() { - this.macros = new HashMap<>(); - this.celParserLibraries = ImmutableSet.builder(); - this.standardMacros = new ArrayList<>(); - } + private Builder() {} } private CelParserImpl( ImmutableMap macros, + ImmutableMap customMacros, CelOptions options, ImmutableList standardMacros, ImmutableSet parserLibraries) { - this.macros = macros; - this.options = checkNotNull(options); - this.standardMacros = standardMacros; - this.parserLibraries = parserLibraries; - } - - @Override - public void accept(EnvVisitor visitor) { - macros.forEach((name, macro) -> visitor.visitMacro(macro)); + super(macros, customMacros, options, standardMacros, parserLibraries); } } diff --git a/parser/src/main/java/dev/cel/parser/LiteParserImpl.java b/parser/src/main/java/dev/cel/parser/LiteParserImpl.java new file mode 100644 index 000000000..b88712608 --- /dev/null +++ b/parser/src/main/java/dev/cel/parser/LiteParserImpl.java @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.parser; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.CheckReturnValue; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.CelOptions; +import dev.cel.common.CelSource; +import dev.cel.common.CelValidationResult; +import dev.cel.common.annotations.Internal; + +/** + * Modernized lite parser implementation for CEL using the Pratt parser. + * + *

CEL Library Internals. Do Not Use. Consumers should use {@link CelLiteParserFactory} instead. + */ +@Immutable +@Internal +final class LiteParserImpl extends CelParserBase { + + static Builder newBuilder() { + return new Builder(); + } + + @Override + public CelValidationResult parse(CelSource source) { + return PrattParser.parse(checkNotNull(source), getOptions(), getMacros()); + } + + @Override + public CelParserBuilder toParserBuilder() { + return populateBuilder(new Builder()); + } + + static final class Builder extends CelParserBase.Builder { + + /** Throws if an unsupported flag in CelOptions is toggled. */ + private static void assertAllowedCelOptions(CelOptions celOptions) { + String prefix = "Misconfigured CelOptions: "; + if (!celOptions.enablePrattParser()) { + throw new IllegalArgumentException(prefix + "enablePrattParser cannot be disabled."); + } + } + + @Override + @CheckReturnValue + public CelParser build() { + ImmutableSet parserLibrarySet = buildLibraries(); + assertAllowedCelOptions(getOptions()); + + return new LiteParserImpl( + buildMacroMap(), + buildCustomMacros(), + getOptions(), + buildStandardMacros(), + parserLibrarySet); + } + + private Builder() { + setOptions(CelOptions.current().enablePrattParser(true).build()); + } + } + + private LiteParserImpl( + ImmutableMap macros, + ImmutableMap customMacros, + CelOptions options, + ImmutableList standardMacros, + ImmutableSet parserLibraries) { + super(macros, customMacros, options, standardMacros, parserLibraries); + } +} diff --git a/parser/src/main/java/dev/cel/parser/PrattParser.java b/parser/src/main/java/dev/cel/parser/PrattParser.java index 1fe2a4bab..829ac1dcc 100644 --- a/parser/src/main/java/dev/cel/parser/PrattParser.java +++ b/parser/src/main/java/dev/cel/parser/PrattParser.java @@ -34,6 +34,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import org.jspecify.annotations.Nullable; @@ -41,6 +42,8 @@ /** Pratt parser implementation for CEL. */ final class PrattParser { + private static final Locale LOCALE = Locale.US; + /** Sentinel stored in {@link #positions} for expression ids that have no source position. */ private static final int NO_POSITION = -1; @@ -154,8 +157,10 @@ static CelValidationResult parse( CelIssue.formatError( CelSourceLocation.NONE, String.format( + LOCALE, "expression code point size exceeds limit: size: %d, limit %d", - source.getContent().size(), options.maxExpressionCodePointSize())))); + source.getContent().size(), + options.maxExpressionCodePointSize())))); } PrattParser prattParser = new PrattParser(source, options, macros); CelExpr expr = prattParser.run(); @@ -290,7 +295,9 @@ private long nextId(int position) { reportError( position, String.format( - "expression node limit (%d) exceeded", options.maxParseExpressionNodeCount())); + LOCALE, + "expression node limit (%d) exceeded", + options.maxParseExpressionNodeCount())); nodeLimitExceeded = true; } if (!nodeLimitExceeded && position >= 0) { @@ -375,7 +382,8 @@ private void reportError(CelSourceLocation loc, String msg) { issues.add( CelIssue.formatError( CelSourceLocation.NONE, - String.format("More than %d parse errors.", options.maxParseErrorRecoveryLimit()))); + String.format( + LOCALE, "More than %d parse errors.", options.maxParseErrorRecoveryLimit()))); peekToken = END_TOKEN; } if (errorCount <= options.maxParseErrorRecoveryLimit()) { @@ -401,7 +409,9 @@ private void reportRecursionLimit(int position) { reportError( position, String.format( - "Expression recursion limit exceeded. limit: %d", options.maxParseRecursionDepth())); + LOCALE, + "Expression recursion limit exceeded. limit: %d", + options.maxParseRecursionDepth())); } } diff --git a/parser/src/test/java/dev/cel/parser/BUILD.bazel b/parser/src/test/java/dev/cel/parser/BUILD.bazel index eea155e92..db5a647b7 100644 --- a/parser/src/test/java/dev/cel/parser/BUILD.bazel +++ b/parser/src/test/java/dev/cel/parser/BUILD.bazel @@ -1,4 +1,5 @@ load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_local_test") load("//:testing.bzl", "junit4_test_suites") package( @@ -7,19 +8,25 @@ package( ], ) +ANDROID_TESTS = [ + "CelLiteParserAndroidTest.java", +] + java_library( name = "tests", testonly = True, srcs = glob( ["*Test.java"], - exclude = ["TmpPrattParserTest.java"], + exclude = ["TmpPrattParserTest.java"] + ANDROID_TESTS, ), resources = ["//parser/src/test/resources:baselines"], deps = [ "//:java_truth", "//common:cel_ast", + "//common:cel_issue", "//common:cel_source", - "//common:compiler_common", + "//common:cel_validation_exception", + "//common:cel_validation_result", "//common:operator", "//common:options", "//common:proto_ast", @@ -29,6 +36,7 @@ java_library( "//common/values:cel_byte_string", "//extensions:optional_library", "//parser", + "//parser:lite_parser_factory", "//parser:macro", "//parser:parser_builder", "//parser:parser_factory", @@ -46,6 +54,18 @@ java_library( ], ) +cel_android_local_test( + name = "android_tests", + srcs = ANDROID_TESTS, + test_class = "dev.cel.parser.CelLiteParserAndroidTest", + deps = [ + "//:java_truth", + "//common:cel_validation_result_android", + "//parser:lite_parser_factory_android", + "//parser:parser_builder_android", + ], +) + junit4_test_suites( name = "test_suites", sizes = [ diff --git a/parser/src/test/java/dev/cel/parser/CelLiteParserAndroidTest.java b/parser/src/test/java/dev/cel/parser/CelLiteParserAndroidTest.java new file mode 100644 index 000000000..63e4346d1 --- /dev/null +++ b/parser/src/test/java/dev/cel/parser/CelLiteParserAndroidTest.java @@ -0,0 +1,40 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.parser; + +import static com.google.common.truth.Truth.assertThat; + +import dev.cel.common.CelValidationResult; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class CelLiteParserAndroidTest { + + @Test + public void newLiteParserBuilder_build_isNotNull() { + assertThat(CelLiteParserFactory.newLiteParserBuilder().build()).isNotNull(); + } + + @Test + public void newLiteParserBuilder_parseSmokeTest() { + CelParser parser = CelLiteParserFactory.newLiteParserBuilder().build(); + + CelValidationResult result = parser.parse("1 + 1"); + + assertThat(result.hasError()).isFalse(); + } +} diff --git a/parser/src/test/java/dev/cel/parser/CelLiteParserFactoryTest.java b/parser/src/test/java/dev/cel/parser/CelLiteParserFactoryTest.java new file mode 100644 index 000000000..5b78521c5 --- /dev/null +++ b/parser/src/test/java/dev/cel/parser/CelLiteParserFactoryTest.java @@ -0,0 +1,229 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.parser; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import dev.cel.common.CelOptions; +import dev.cel.common.CelSource; +import dev.cel.common.CelValidationResult; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class CelLiteParserFactoryTest { + + @Test + public void newLiteParserBuilder_build_isNotNull() { + CelParserBuilder builder = CelLiteParserFactory.newLiteParserBuilder(); + + CelParser parser = builder.build(); + + assertThat(parser).isNotNull(); + } + + @Test + public void newLiteParserBuilder_defaultOptions_matchesCurrentWithPrattParserEnabled() { + CelParserBuilder builder = CelLiteParserFactory.newLiteParserBuilder(); + + assertThat(builder.getOptions()) + .isEqualTo(CelOptions.current().enablePrattParser(true).build()); + } + + @Test + public void newLiteParserBuilder_parseSmokeTest() { + CelParser parser = CelLiteParserFactory.newLiteParserBuilder().build(); + + CelValidationResult result = parser.parse("1 + 1"); + + assertThat(result.hasError()).isFalse(); + } + + @Test + public void parse_nullSource_throwsNullPointerException() { + CelParser parser = CelLiteParserFactory.newLiteParserBuilder().build(); + + assertThrows(NullPointerException.class, () -> parser.parse((CelSource) null)); + } + + @Test + public void setStandardMacros_secondCallReplacesPreviousStandardMacros() throws Exception { + CelParser parser = + CelLiteParserFactory.newLiteParserBuilder() + .setStandardMacros(CelStandardMacro.HAS, CelStandardMacro.ALL) + .setStandardMacros(CelStandardMacro.HAS) + .build(); + + CelValidationResult hasResult = parser.parse("has(a.b)"); + CelValidationResult allResult = parser.parse("[1].all(x, x > 0)"); + + assertThat(hasResult.getAst().getExpr().getKind()).isEqualTo(Kind.SELECT); + assertThat(allResult.getAst().getExpr().getKind()).isEqualTo(Kind.CALL); + } + + @Test + public void setStandardMacros_allStandardMacrosSupported() throws Exception { + CelParser parser = + CelLiteParserFactory.newLiteParserBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .build(); + + assertThat(parser.parse("has(a.b)").getAst().getExpr().getKind()).isEqualTo(Kind.SELECT); + assertThat(parser.parse("[1].all(x, x > 0)").getAst().getExpr().getKind()) + .isEqualTo(Kind.COMPREHENSION); + assertThat(parser.parse("[1].exists(x, x > 0)").getAst().getExpr().getKind()) + .isEqualTo(Kind.COMPREHENSION); + assertThat(parser.parse("[1].exists_one(x, x > 0)").getAst().getExpr().getKind()) + .isEqualTo(Kind.COMPREHENSION); + assertThat(parser.parse("[1].map(x, x > 0)").getAst().getExpr().getKind()) + .isEqualTo(Kind.COMPREHENSION); + assertThat(parser.parse("[1].map(x, x > 0, x)").getAst().getExpr().getKind()) + .isEqualTo(Kind.COMPREHENSION); + assertThat(parser.parse("[1].filter(x, x > 0)").getAst().getExpr().getKind()) + .isEqualTo(Kind.COMPREHENSION); + } + + @Test + public void addMacros_registersCustomMacro() throws Exception { + CelMacro customMacro = + CelMacro.newGlobalMacro( + "customMacro", + 0, + (exprFactory, target, args) -> Optional.of(exprFactory.newBoolLiteral(true))); + CelParser parser = CelLiteParserFactory.newLiteParserBuilder().addMacros(customMacro).build(); + + CelValidationResult result = parser.parse("customMacro()"); + + assertThat(result.getAst().getExpr().getKind()).isEqualTo(Kind.CONSTANT); + } + + @Test + public void addLibraries_configuresParserBuilder() throws Exception { + CelParserLibrary library = + new CelParserLibrary() { + @Override + public void setParserOptions(CelParserBuilder parserBuilder) { + parserBuilder.addMacros( + CelMacro.newGlobalMacro( + "libMacro", + 0, + (exprFactory, target, args) -> Optional.of(exprFactory.newBoolLiteral(true)))); + } + }; + CelParser parser = CelLiteParserFactory.newLiteParserBuilder().addLibraries(library).build(); + + CelValidationResult result = parser.parse("libMacro()"); + + assertThat(result.getAst().getExpr().getKind()).isEqualTo(Kind.CONSTANT); + } + + @Test + public void toParserBuilder_roundtripPreservesCustomAndStandardMacros() throws Exception { + CelMacro customMacro = + CelMacro.newGlobalMacro( + "customMacro", + 0, + (exprFactory, target, args) -> Optional.of(exprFactory.newBoolLiteral(true))); + CelParser parser = + CelLiteParserFactory.newLiteParserBuilder() + .setStandardMacros(CelStandardMacro.HAS) + .addMacros(customMacro) + .build(); + + CelParser roundtripParser = parser.toParserBuilder().build(); + CelValidationResult hasResult = roundtripParser.parse("has(a.b)"); + CelValidationResult customMacroResult = roundtripParser.parse("customMacro()"); + + assertThat(hasResult.getAst().getExpr().getKind()).isEqualTo(Kind.SELECT); + assertThat(customMacroResult.getAst().getExpr().getKind()).isEqualTo(Kind.CONSTANT); + } + + @Test + public void toParserBuilder_roundtripPreservesOptionsAndLibraries() throws Exception { + CelParserLibrary library = + new CelParserLibrary() { + @Override + public void setParserOptions(CelParserBuilder parserBuilder) { + parserBuilder.addMacros( + CelMacro.newGlobalMacro( + "libMacro", + 0, + (exprFactory, target, args) -> Optional.of(exprFactory.newBoolLiteral(true)))); + } + }; + CelOptions customOptions = + CelOptions.current().enablePrattParser(true).maxExpressionCodePointSize(100).build(); + CelParser parser = + CelLiteParserFactory.newLiteParserBuilder() + .setOptions(customOptions) + .addLibraries(library) + .build(); + + CelParser roundtripParser = parser.toParserBuilder().build(); + + assertThat(roundtripParser.parse("libMacro()").getAst().getExpr().getKind()) + .isEqualTo(Kind.CONSTANT); + } + + @Test + public void toParserBuilder_createsNewBuilder() { + CelParserBuilder originalBuilder = CelLiteParserFactory.newLiteParserBuilder(); + CelParser parser = originalBuilder.build(); + + CelParserBuilder roundtripBuilder = parser.toParserBuilder(); + + assertThat(roundtripBuilder).isNotSameInstanceAs(originalBuilder); + assertThat(roundtripBuilder.build()).isNotNull(); + } + + @Test + public void build_withPrattParserDisabled_throwsIllegalArgumentException() { + CelParserBuilder builder = + CelLiteParserFactory.newLiteParserBuilder() + .setOptions(CelOptions.current().enablePrattParser(false).build()); + + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, builder::build); + + assertThat(exception) + .hasMessageThat() + .isEqualTo("Misconfigured CelOptions: enablePrattParser cannot be disabled."); + } + + @Test + public void build_withLibraryDisablingPrattParser_throwsIllegalArgumentException() { + CelParserLibrary disablingLibrary = + new CelParserLibrary() { + @Override + public void setParserOptions(CelParserBuilder parserBuilder) { + parserBuilder.setOptions( + parserBuilder.getOptions().toBuilder().enablePrattParser(false).build()); + } + }; + CelParserBuilder builder = + CelLiteParserFactory.newLiteParserBuilder().addLibraries(disablingLibrary); + + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, builder::build); + + assertThat(exception) + .hasMessageThat() + .isEqualTo("Misconfigured CelOptions: enablePrattParser cannot be disabled."); + } +} From 248b623440b421df56b02ca5efbd7265f2cd25c2 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Tue, 15 Sep 2026 15:16:56 -0700 Subject: [PATCH 204/204] Add async completion coordinator PiperOrigin-RevId: 982070232 --- runtime/planner/BUILD.bazel | 7 + .../planner/AsyncCompletionCoordinator.java | 548 +++++ .../java/dev/cel/runtime/planner/BUILD.bazel | 17 + .../AsyncCompletionCoordinatorTest.java | 1801 +++++++++++++++++ .../java/dev/cel/runtime/planner/BUILD.bazel | 4 + 5 files changed, 2377 insertions(+) create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/AsyncCompletionCoordinator.java create mode 100644 runtime/src/test/java/dev/cel/runtime/planner/AsyncCompletionCoordinatorTest.java diff --git a/runtime/planner/BUILD.bazel b/runtime/planner/BUILD.bazel index 2781a2e22..0a4ef8a84 100644 --- a/runtime/planner/BUILD.bazel +++ b/runtime/planner/BUILD.bazel @@ -28,3 +28,10 @@ java_library( visibility = ["//:internal"], exports = ["//runtime/src/main/java/dev/cel/runtime/planner:async_gate"], ) + +java_library( + name = "async_completion_coordinator", + testonly = 1, + visibility = ["//:internal"], + exports = ["//runtime/src/main/java/dev/cel/runtime/planner:async_completion_coordinator"], +) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/AsyncCompletionCoordinator.java b/runtime/src/main/java/dev/cel/runtime/planner/AsyncCompletionCoordinator.java new file mode 100644 index 000000000..149212934 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/AsyncCompletionCoordinator.java @@ -0,0 +1,548 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CheckReturnValue; +import javax.annotation.concurrent.ThreadSafe; +import com.google.errorprone.annotations.concurrent.GuardedBy; +import dev.cel.runtime.CelAsyncCall; +import dev.cel.runtime.CelAsyncDrainAction; +import dev.cel.runtime.CelAsyncDrainStrategy; +import dev.cel.runtime.CelAsyncEvaluationOptions; +import java.time.Duration; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledFuture; +import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + +/** + * Coordinates asynchronous call completion notifications, debouncing, and re-evaluation dispatch. + */ +@ThreadSafe +final class AsyncCompletionCoordinator { + + /** Represents the result of attempting to wait for asynchronous completions. */ + enum WaitResult { + /** Continuation registered; execution will resume asynchronously when work arrives. */ + REGISTERED, + /** Drain strategy satisfied immediately; caller should reevaluate now via loop trampoline. */ + REEVALUATE_NOW, + /** + * No calls in flight and no completions pending; evaluation cannot make further progress. + * + *

Liveness fail-safe for a caller that waits with nothing left to wake it. Callers must + * treat this as an error rather than as a completed evaluation. + */ + NO_OUTSTANDING_WORK, + /** The coordinator has been cancelled. */ + CANCELLED + } + + // CEL-Internal-4 + private final Object lock; + + private final CelAsyncEvaluationOptions options; + private final AsyncGate gate; + private final Executor continuationExecutor; + + // CEL-Internal-4 + private final Consumer failureCallback; + + @GuardedBy("lock") + private final List completedBatch; + + @GuardedBy("lock") + private boolean isWaiting; + + @GuardedBy("lock") + private boolean isCancelled; + + @GuardedBy("lock") + private @Nullable Runnable continuation; + + @GuardedBy("lock") + private @Nullable ScheduledFuture debounceTimer; + + private final ThreadLocal> continuationTrampoline; + + @GuardedBy("lock") + private long cycleId; + + @GuardedBy("lock") + private long debounceGeneration; + + @GuardedBy("lock") + private boolean failureReported; + + static AsyncCompletionCoordinator create( + CelAsyncEvaluationOptions options, + AsyncGate gate, + Executor continuationExecutor, + Consumer failureCallback) { + return new AsyncCompletionCoordinator(options, gate, continuationExecutor, failureCallback); + } + + /** + * Notifies the coordinator that an asynchronous call has finished. + * + *

Releases the concurrency permit in {@link AsyncGate}, appends the call to the current batch, + * and evaluates the configured {@link CelAsyncDrainStrategy} if currently waiting. + */ + void callCompleted(CelAsyncCall call) { + checkNotNull(call, "call must not be null"); + // The unbalanced flag is acted on after the lock is released, because failAndCancel() runs the + // user-supplied failure callback, which must never execute while holding the coordinator lock. + boolean unbalanced = false; + CompletionSnapshot snapshot = null; + + synchronized (lock) { + // Check activeCount() <= 0 BEFORE release() to detect unbalanced completion misuse. + if (gate.activeCount() <= 0) { + unbalanced = true; + } else { + gate.release(); + if (isCancelled) { + return; + } + completedBatch.add(call); + if (!isWaiting) { + return; + } + // Take inFlight AFTER release() to capture remaining active calls for the drain strategy. + snapshot = + new CompletionSnapshot( + ImmutableList.copyOf(completedBatch), + gate.activeCount(), + cycleId, + ++debounceGeneration); + } + } + + if (unbalanced) { + failAndCancel(new IllegalStateException("callCompleted called with no active calls")); + return; + } + + CelAsyncDrainAction action; + try { + action = + checkNotNull( + options.drainStrategy().nextAction(snapshot.batch, snapshot.inFlight), + "drainStrategy must not return null"); + } catch (Throwable t) { + failAndCancel(t); + return; + } + applyDrainAction(action, snapshot.cycleId, snapshot.debounceGeneration); + } + + /** + * Waits for pending asynchronous completions or triggers immediate re-evaluation. + * + * @param continuationCallback callback invoked when the drain strategy allows re-evaluation. + * @return {@link WaitResult} indicating how the caller should proceed. + */ + @CheckReturnValue + WaitResult waitForCompletions(Runnable continuationCallback) { + checkNotNull(continuationCallback, "continuationCallback must not be null"); + CompletionSnapshot snapshot; + + synchronized (lock) { + if (isCancelled) { + return WaitResult.CANCELLED; + } + checkState(!isWaiting, "Coordinator is already waiting for completions"); + + // Liveness fail-safe: no completion can ever arrive to resume the continuation. + if (gate.activeCount() == 0 && completedBatch.isEmpty()) { + return WaitResult.NO_OUTSTANDING_WORK; + } + + this.isWaiting = true; + this.continuation = continuationCallback; + + if (completedBatch.isEmpty()) { + return WaitResult.REGISTERED; + } + + snapshot = + new CompletionSnapshot( + ImmutableList.copyOf(completedBatch), + gate.activeCount(), + this.cycleId, + ++this.debounceGeneration); + } + + CelAsyncDrainAction action; + try { + action = + checkNotNull( + options.drainStrategy().nextAction(snapshot.batch, snapshot.inFlight), + "drainStrategy must not return null"); + } catch (Throwable t) { + failAndCancel(t); + return WaitResult.CANCELLED; + } + + boolean reevaluateNow = false; + synchronized (lock) { + if (isCancelled) { + return WaitResult.CANCELLED; + } + + if (this.debounceGeneration == snapshot.debounceGeneration) { + // If strategy says reevaluate, OR if activeCount is 0 (escape hatch preventing indefinite + // stall with custom drain strategies when all in-flight calls finish). + if (action.shouldReevaluate() || gate.activeCount() == 0) { + // The continuation in DrainResult is intentionally not dispatched here because + // WaitResult.REEVALUATE_NOW instructs the calling thread to re-evaluate synchronously. + DrainResult unused = drainAndResetUnderLock(); + reevaluateNow = true; + } + } else { + return WaitResult.REGISTERED; + } + } + if (reevaluateNow) { + return WaitResult.REEVALUATE_NOW; + } + + Duration waitDuration = action.waitDuration(); + if (waitDuration.isZero()) { + return WaitResult.REGISTERED; + } + long delayNanos; + try { + delayNanos = waitDuration.toNanos(); + } catch (ArithmeticException e) { + failAndCancel(e); + return WaitResult.CANCELLED; + } + return scheduleDebounce(delayNanos, snapshot.cycleId, snapshot.debounceGeneration) + ? WaitResult.REGISTERED + : WaitResult.CANCELLED; + } + + private void applyDrainAction( + CelAsyncDrainAction action, long expectedCycleId, long expectedGen) { + boolean shouldReevaluate; + DrainResult drainResult = null; + synchronized (lock) { + // Live read: check gate.activeCount() == 0 under lock so we do not schedule an unnecessary + // timer if all remaining calls completed while evaluating nextAction(). + shouldReevaluate = action.shouldReevaluate() || gate.activeCount() == 0; + if (shouldReevaluate && isCurrentUnderLock(expectedCycleId, expectedGen)) { + drainResult = drainAndResetUnderLock(); + } + } + if (drainResult != null) { + if (drainResult.timer != null) { + drainResult.timer.cancel(false); + } + if (drainResult.continuation != null) { + dispatchContinuation(drainResult.continuation); + } + return; + } + if (shouldReevaluate) { + return; + } + + Duration waitDuration = action.waitDuration(); + if (!waitDuration.isZero()) { + long delayNanos; + try { + delayNanos = waitDuration.toNanos(); + } catch (ArithmeticException e) { + failAndCancel(e); + return; + } + boolean unusedScheduled = scheduleDebounce(delayNanos, expectedCycleId, expectedGen); + return; + } + + ScheduledFuture timerToCancel = null; + synchronized (lock) { + if (isCurrentUnderLock(expectedCycleId, expectedGen)) { + timerToCancel = cancelDebounceTimerUnderLock(); + } + } + if (timerToCancel != null) { + timerToCancel.cancel(false); + } + } + + /** + * Schedules a debounce timer that fires {@link #onDebounceFired} after {@code nanos}. + * + * @return false if the timer could not be scheduled, in which case the coordinator has already + * been failed and cancelled. + */ + private boolean scheduleDebounce(long nanos, long scheduledCycleId, long scheduledGen) { + ScheduledFuture future; + try { + future = + options + .resolveScheduledExecutorService() + .schedule(() -> onDebounceFired(scheduledCycleId, scheduledGen), nanos, NANOSECONDS); + } catch (Throwable t) { + failAndCancel(t); + return false; + } + + ScheduledFuture redundantFuture = null; + synchronized (lock) { + if (isCurrentUnderLock(scheduledCycleId, scheduledGen)) { + if (debounceTimer != null) { + redundantFuture = debounceTimer; + } + debounceTimer = future; + } else { + redundantFuture = future; + } + } + if (redundantFuture != null) { + redundantFuture.cancel(false); + } + return true; + } + + /** + * Returns true if the coordinator is still actively waiting on the cycle and debounce generation + * that produced the in-flight action, meaning the action is not stale. + */ + @GuardedBy("lock") + private boolean isCurrentUnderLock(long expectedCycleId, long expectedGen) { + return !isCancelled + && isWaiting + && this.cycleId == expectedCycleId + && this.debounceGeneration == expectedGen; + } + + @VisibleForTesting + void onDebounceFired(long firedCycleId, long firedGen) { + DrainResult drainResult = null; + synchronized (lock) { + if (isCurrentUnderLock(firedCycleId, firedGen)) { + drainResult = drainAndResetUnderLock(); + } + } + if (drainResult != null) { + if (drainResult.timer != null) { + drainResult.timer.cancel(false); + } + if (drainResult.continuation != null) { + dispatchContinuation(drainResult.continuation); + } + } + } + + private void dispatchContinuation(Runnable run) { + Deque queue = continuationTrampoline.get(); + queue.add(run); + if (queue.size() > 1) { + return; + } + try { + while (!queue.isEmpty()) { + Runnable next = queue.peek(); + try { + continuationExecutor.execute(next); + } catch (Throwable t) { + failAndCancel(t); + break; + } finally { + queue.poll(); + } + } + } finally { + queue.clear(); + continuationTrampoline.remove(); + } + } + + /** + * Cancels the coordinator and associated concurrency gate. + * + *

Any registered continuation callback is discarded without being executed. The caller or + * owner of this coordinator is responsible for completing or failing the outer evaluation future + * itself; calling {@code cancel()} does not notify the continuation callback. + */ + void cancel() { + ScheduledFuture timerToCancel; + synchronized (lock) { + if (isCancelled) { + return; + } + isCancelled = true; + isWaiting = false; + continuation = null; + completedBatch.clear(); + timerToCancel = cancelDebounceTimerUnderLock(); + } + if (timerToCancel != null) { + timerToCancel.cancel(false); + } + gate.cancel(); + } + + @SuppressWarnings("ReferenceEquality") // Identity comparison avoids Throwable self-suppression. + private void failAndCancel(Throwable t) { + synchronized (lock) { + if (failureReported) { + return; + } + failureReported = true; + } + cancel(); + try { + failureCallback.accept(t); + } catch (Throwable callbackFailure) { + if (t != callbackFailure) { + t.addSuppressed(callbackFailure); + } + } + } + + @GuardedBy("lock") + @CheckReturnValue + private DrainResult drainAndResetUnderLock() { + cycleId++; + debounceGeneration++; + isWaiting = false; + completedBatch.clear(); + Runnable run = continuation; + continuation = null; + ScheduledFuture timer = cancelDebounceTimerUnderLock(); + return new DrainResult(run, timer); + } + + @GuardedBy("lock") + private @Nullable ScheduledFuture cancelDebounceTimerUnderLock() { + ScheduledFuture timer = debounceTimer; + debounceTimer = null; + return timer; + } + + @VisibleForTesting + boolean hasPendingBatch() { + synchronized (lock) { + return !completedBatch.isEmpty(); + } + } + + @VisibleForTesting + boolean isWaiting() { + synchronized (lock) { + return isWaiting; + } + } + + @VisibleForTesting + boolean hasContinuation() { + synchronized (lock) { + return continuation != null; + } + } + + @VisibleForTesting + boolean hasScheduledDebounceTimer() { + synchronized (lock) { + return debounceTimer != null; + } + } + + @VisibleForTesting + long cycleId() { + synchronized (lock) { + return cycleId; + } + } + + @VisibleForTesting + long debounceGeneration() { + synchronized (lock) { + return debounceGeneration; + } + } + + @VisibleForTesting + boolean isCancelled() { + synchronized (lock) { + return isCancelled; + } + } + + private static final class CompletionSnapshot { + final ImmutableList batch; + final int inFlight; + final long cycleId; + final long debounceGeneration; + + private CompletionSnapshot( + ImmutableList batch, int inFlight, long cycleId, long debounceGeneration) { + this.batch = checkNotNull(batch, "batch must not be null"); + this.inFlight = inFlight; + this.cycleId = cycleId; + this.debounceGeneration = debounceGeneration; + } + } + + private static final class DrainResult { + private final @Nullable Runnable continuation; + private final @Nullable ScheduledFuture timer; + + private DrainResult(@Nullable Runnable continuation, @Nullable ScheduledFuture timer) { + this.continuation = continuation; + this.timer = timer; + } + } + + private AsyncCompletionCoordinator( + CelAsyncEvaluationOptions options, + AsyncGate gate, + Executor continuationExecutor, + Consumer failureCallback) { + this.options = checkNotNull(options, "options must not be null"); + this.gate = checkNotNull(gate, "gate must not be null"); + this.continuationExecutor = + checkNotNull(continuationExecutor, "continuationExecutor must not be null"); + this.failureCallback = checkNotNull(failureCallback, "failureCallback must not be null"); + this.lock = new Object(); + this.completedBatch = new ArrayList<>(); + this.continuationTrampoline = + new ThreadLocal>() { + @Override + protected Deque initialValue() { + return new ArrayDeque<>(); + } + }; + this.isWaiting = false; + this.isCancelled = false; + this.failureReported = false; + this.cycleId = 0; + this.debounceGeneration = 0; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index 74d7d8d41..d838e8d53 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -200,6 +200,23 @@ java_library( ], ) +java_library( + name = "async_completion_coordinator", + srcs = ["AsyncCompletionCoordinator.java"], + tags = [ + ], + deps = [ + ":async_gate", + "//runtime:async_call", + "//runtime:async_drain_strategy", + "//runtime:async_options", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + java_library( name = "activation_wrapper", srcs = ["ActivationWrapper.java"], diff --git a/runtime/src/test/java/dev/cel/runtime/planner/AsyncCompletionCoordinatorTest.java b/runtime/src/test/java/dev/cel/runtime/planner/AsyncCompletionCoordinatorTest.java new file mode 100644 index 000000000..8c2073f43 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/planner/AsyncCompletionCoordinatorTest.java @@ -0,0 +1,1801 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.runtime.planner; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.truth.Truth.assertThat; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.Assert.assertThrows; + +import dev.cel.runtime.CelAsyncCall; +import dev.cel.runtime.CelAsyncDrainAction; +import dev.cel.runtime.CelAsyncDrainStrategy; +import dev.cel.runtime.CelAsyncEvaluationOptions; +import dev.cel.runtime.planner.AsyncCompletionCoordinator.WaitResult; +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Delayed; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class AsyncCompletionCoordinatorTest { + + private static final CelAsyncCall DUMMY_CALL = + new CelAsyncCall() { + @Override + public long callId() { + return 1L; + } + + @Override + public long exprId() { + return 10L; + } + + @Override + public String functionName() { + return "testFn"; + } + + @Override + public String overloadId() { + return "testFn_overload"; + } + }; + + @Test + public void waitForCompletions_whenNoCallsInFlightAndEmptyBatch_returnsNoOutstandingWork() { + AsyncGate gate = AsyncGate.create(1); + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + AtomicBoolean continuationRan = new AtomicBoolean(false); + + WaitResult result = coordinator.waitForCompletions(() -> continuationRan.set(true)); + + assertThat(result).isEqualTo(WaitResult.NO_OUTSTANDING_WORK); + assertThat(continuationRan.get()).isFalse(); + assertThat(coordinator.isWaiting()).isFalse(); + } + + @Test + public void + waitForCompletions_afterDrainConsumedBatchWithNoNewDispatch_returnsNoOutstandingWork() { + AsyncGate gate = AsyncGate.create(1); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainAll()) + .build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + coordinator.callCompleted(DUMMY_CALL); + + WaitResult firstWait = coordinator.waitForCompletions(() -> {}); + WaitResult secondWait = coordinator.waitForCompletions(() -> {}); + + assertThat(firstWait).isEqualTo(WaitResult.REEVALUATE_NOW); + assertThat(secondWait).isEqualTo(WaitResult.NO_OUTSTANDING_WORK); + assertThat(coordinator.isWaiting()).isFalse(); + assertThat(coordinator.hasPendingBatch()).isFalse(); + } + + @Test + public void waitForCompletions_whenCallsInFlightAndEmptyBatch_returnsRegistered() { + AsyncGate gate = AsyncGate.create(1); + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + AtomicBoolean continuationRan = new AtomicBoolean(false); + + WaitResult result = coordinator.waitForCompletions(() -> continuationRan.set(true)); + + assertThat(result).isEqualTo(WaitResult.REGISTERED); + assertThat(coordinator.isWaiting()).isTrue(); + assertThat(continuationRan.get()).isFalse(); + } + + @Test + public void + waitForCompletions_whenDrainStrategySatisfiedImmediately_returnsReevaluateNowWithoutDispatch() { + AsyncGate gate = AsyncGate.create(1); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainAll()) + .build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + coordinator.callCompleted(DUMMY_CALL); + AtomicBoolean continuationRan = new AtomicBoolean(false); + + WaitResult result = coordinator.waitForCompletions(() -> continuationRan.set(true)); + + assertThat(result).isEqualTo(WaitResult.REEVALUATE_NOW); + assertThat(continuationRan.get()).isFalse(); + assertThat(coordinator.isWaiting()).isFalse(); + assertThat(coordinator.hasPendingBatch()).isFalse(); + assertThat(coordinator.hasScheduledDebounceTimer()).isFalse(); + } + + @Test + public void + waitForCompletions_whenDrainStrategyReturnsReevaluateWithInFlightCalls_returnsReevaluateNow() { + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainNone()) + .build(); + AsyncGate gate = AsyncGate.create(2); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + coordinator.callCompleted(DUMMY_CALL); + AtomicBoolean continuationRan = new AtomicBoolean(false); + + WaitResult result = coordinator.waitForCompletions(() -> continuationRan.set(true)); + + assertThat(result).isEqualTo(WaitResult.REEVALUATE_NOW); + assertThat(continuationRan.get()).isFalse(); + assertThat(coordinator.isWaiting()).isFalse(); + assertThat(gate.activeCount()).isEqualTo(1); + } + + @Test + public void waitForCompletions_whenDebounceRequested_schedulesTimerAndReturnsRegistered() { + ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1); + try { + AsyncGate gate = AsyncGate.create(2); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainReady(Duration.ofMinutes(10))) + .setScheduledExecutorService(scheduler) + .build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + coordinator.callCompleted(DUMMY_CALL); + AtomicBoolean continuationRan = new AtomicBoolean(false); + + WaitResult result = coordinator.waitForCompletions(() -> continuationRan.set(true)); + + assertThat(result).isEqualTo(WaitResult.REGISTERED); + assertThat(coordinator.isWaiting()).isTrue(); + assertThat(coordinator.hasScheduledDebounceTimer()).isTrue(); + assertThat(continuationRan.get()).isFalse(); + } finally { + scheduler.shutdownNow(); + } + } + + @Test + public void waitForCompletions_whenAlreadyWaiting_throwsIllegalStateException() { + AsyncGate gate = AsyncGate.create(1); + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + registerWait(coordinator, () -> {}); + + IllegalStateException thrown = + assertThrows(IllegalStateException.class, () -> coordinator.waitForCompletions(() -> {})); + + assertThat(thrown).hasMessageThat().contains("Coordinator is already waiting for completions"); + } + + @Test + public void waitForCompletions_whenCoordinatorCancelled_returnsCancelled() { + AsyncGate gate = AsyncGate.create(1); + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + coordinator.cancel(); + AtomicBoolean continuationRan = new AtomicBoolean(false); + + WaitResult result = coordinator.waitForCompletions(() -> continuationRan.set(true)); + + assertThat(result).isEqualTo(WaitResult.CANCELLED); + assertThat(continuationRan.get()).isFalse(); + } + + @Test + public void callCompleted_releasesGatePermitAndAddsToBatch() { + AsyncGate gate = AsyncGate.create(2); + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(gate.activeCount()).isEqualTo(1); + assertThat(coordinator.hasPendingBatch()).isTrue(); + } + + @Test + public void callCompleted_whenCancelled_ignoresCall() { + AsyncGate gate = AsyncGate.create(1); + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + coordinator.cancel(); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(gate.activeCount()).isEqualTo(0); + assertThat(coordinator.hasPendingBatch()).isFalse(); + } + + @Test + public void callCompleted_whenWaitingWithPendingCalls_schedulesDebounceTimer() { + ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1); + try { + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainReady(Duration.ofMinutes(10))) + .setScheduledExecutorService(scheduler) + .build(); + AsyncGate gate = AsyncGate.create(2); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + AtomicBoolean continuationRan = new AtomicBoolean(false); + registerWait(coordinator, () -> continuationRan.set(true)); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(continuationRan.get()).isFalse(); + assertThat(coordinator.isWaiting()).isTrue(); + assertThat(coordinator.hasScheduledDebounceTimer()).isTrue(); + assertThat(scheduler.getQueue()).isNotEmpty(); + } finally { + scheduler.shutdownNow(); + } + } + + @Test + public void callCompleted_whenWaitingWithDrainAllStrategy_waitsWhileCallsRemainInFlight() { + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainAll()) + .build(); + AsyncGate gate = AsyncGate.create(2); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + AtomicBoolean continuationRan = new AtomicBoolean(false); + registerWait(coordinator, () -> continuationRan.set(true)); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(continuationRan.get()).isFalse(); + assertThat(coordinator.isWaiting()).isTrue(); + } + + @Test + public void + callCompleted_whenWaitingWithDrainAllStrategy_triggersContinuationWhenFinalCallCompletes() { + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainAll()) + .build(); + AsyncGate gate = AsyncGate.create(2); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + AtomicBoolean continuationRan = new AtomicBoolean(false); + registerWait(coordinator, () -> continuationRan.set(true)); + coordinator.callCompleted(DUMMY_CALL); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(continuationRan.get()).isTrue(); + assertThat(coordinator.isWaiting()).isFalse(); + } + + @Test + public void + callCompleted_whenWaitingWithDrainNoneStrategy_triggersContinuationWhileCallsRemainInFlight() { + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainNone()) + .build(); + AsyncGate gate = AsyncGate.create(2); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + AtomicBoolean continuationRan = new AtomicBoolean(false); + registerWait(coordinator, () -> continuationRan.set(true)); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(continuationRan.get()).isTrue(); + assertThat(coordinator.isWaiting()).isFalse(); + assertThat(gate.activeCount()).isEqualTo(1); + } + + @Test + public void callCompleted_whenDebounceTimerPending_resetsDebounceTimerForSlidingWindow() { + TrackingScheduler scheduler = new TrackingScheduler(); + try { + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainReady(Duration.ofMinutes(10))) + .setScheduledExecutorService(scheduler) + .build(); + AsyncGate gate = AsyncGate.create(3); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + acquirePermit(gate); + registerWait(coordinator, () -> {}); + coordinator.callCompleted(DUMMY_CALL); + ScheduledFuture firstTimer = (ScheduledFuture) scheduler.getQueue().peek(); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(firstTimer).isNotNull(); + assertThat(firstTimer.isCancelled()).isTrue(); + assertThat(scheduler.lastMayInterrupt()).hasValue(false); + assertThat(coordinator.hasScheduledDebounceTimer()).isTrue(); + } finally { + scheduler.shutdownNow(); + } + } + + @Test + public void + callCompleted_whenDebounceTimerPendingAndNextActionIsWaitForMore_cancelsPendingDebounceTimer() { + TrackingScheduler scheduler = new TrackingScheduler(); + try { + AtomicInteger callCount = new AtomicInteger(); + CelAsyncDrainStrategy strategy = new TwoPhaseDrainStrategy(callCount); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(strategy) + .setScheduledExecutorService(scheduler) + .build(); + AsyncGate gate = AsyncGate.create(3); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + acquirePermit(gate); + registerWait(coordinator, () -> {}); + coordinator.callCompleted(DUMMY_CALL); + ScheduledFuture scheduledTask = (ScheduledFuture) scheduler.getQueue().peek(); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(scheduledTask).isNotNull(); + assertThat(scheduledTask.isCancelled()).isTrue(); + assertThat(scheduler.lastMayInterrupt()).hasValue(false); + assertThat(coordinator.hasScheduledDebounceTimer()).isFalse(); + assertThat(coordinator.isWaiting()).isTrue(); + } finally { + scheduler.shutdownNow(); + } + } + + @Test + public void onDebounceFired_whenWaiting_triggersContinuation() { + TrackingScheduler scheduler = new TrackingScheduler(); + try { + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainReady(Duration.ofMinutes(10))) + .setScheduledExecutorService(scheduler) + .build(); + AsyncGate gate = AsyncGate.create(2); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + AtomicBoolean continuationRan = new AtomicBoolean(false); + registerWait(coordinator, () -> continuationRan.set(true)); + coordinator.callCompleted(DUMMY_CALL); + ScheduledFuture scheduledTask = (ScheduledFuture) scheduler.getQueue().peek(); + + ((Runnable) requireNonNull(scheduledTask)).run(); + + assertThat(continuationRan.get()).isTrue(); + assertThat(coordinator.isWaiting()).isFalse(); + assertThat(coordinator.hasPendingBatch()).isFalse(); + assertThat(coordinator.hasScheduledDebounceTimer()).isFalse(); + assertThat(coordinator.hasContinuation()).isFalse(); + assertThat(scheduledTask.isCancelled()).isTrue(); + assertThat(scheduler.lastMayInterrupt()).hasValue(false); + } finally { + scheduler.shutdownNow(); + } + } + + @Test + public void onDebounceFired_whenCycleMismatch_doesNotExecuteContinuation() { + AtomicInteger executedCount = new AtomicInteger(); + Executor rejectingNullExecutor = + task -> { + requireNonNull(task, "task must not be null"); + executedCount.incrementAndGet(); + task.run(); + }; + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncGate gate = AsyncGate.create(1); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, rejectingNullExecutor, t -> {}); + acquirePermit(gate); + registerWait(coordinator, () -> {}); + + coordinator.onDebounceFired(coordinator.cycleId() - 1, coordinator.debounceGeneration()); + + assertThat(executedCount.get()).isEqualTo(0); + assertThat(coordinator.isWaiting()).isTrue(); + assertThat(coordinator.hasContinuation()).isTrue(); + } + + @Test + public void onDebounceFired_whenDebounceGenerationMismatch_doesNotExecuteContinuation() { + ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1); + try { + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainReady(Duration.ofMinutes(10))) + .setScheduledExecutorService(scheduler) + .build(); + AsyncGate gate = AsyncGate.create(3); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + acquirePermit(gate); + AtomicInteger continuationRan = new AtomicInteger(); + registerWait(coordinator, continuationRan::incrementAndGet); + coordinator.callCompleted(DUMMY_CALL); + long staleGen = coordinator.debounceGeneration(); + coordinator.callCompleted(DUMMY_CALL); + + coordinator.onDebounceFired(coordinator.cycleId(), staleGen); + + assertThat(continuationRan.get()).isEqualTo(0); + assertThat(coordinator.isWaiting()).isTrue(); + } finally { + scheduler.shutdownNow(); + } + } + + @Test + public void cancel_cancelsDebounceTimerAndPreventsContinuation() { + TrackingScheduler scheduler = new TrackingScheduler(); + try { + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainReady(Duration.ofMinutes(10))) + .setScheduledExecutorService(scheduler) + .build(); + AsyncGate gate = AsyncGate.create(2); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + AtomicBoolean continuationRan = new AtomicBoolean(false); + registerWait(coordinator, () -> continuationRan.set(true)); + coordinator.callCompleted(DUMMY_CALL); + ScheduledFuture scheduledTask = (ScheduledFuture) scheduler.getQueue().peek(); + + coordinator.cancel(); + + assertThat(coordinator.hasPendingBatch()).isFalse(); + assertThat(coordinator.isWaiting()).isFalse(); + assertThat(coordinator.hasContinuation()).isFalse(); + assertThat(coordinator.hasScheduledDebounceTimer()).isFalse(); + assertThat(scheduledTask).isNotNull(); + assertThat(scheduledTask.isCancelled()).isTrue(); + assertThat(scheduler.lastMayInterrupt()).hasValue(false); + assertThat(continuationRan.get()).isFalse(); + } finally { + scheduler.shutdownNow(); + } + } + + @Test + public void onDebounceFired_whenCancelled_doesNotTriggerContinuation() { + ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1); + try { + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainReady(Duration.ofMinutes(10))) + .setScheduledExecutorService(scheduler) + .build(); + AsyncGate gate = AsyncGate.create(2); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + AtomicBoolean continuationRan = new AtomicBoolean(false); + registerWait(coordinator, () -> continuationRan.set(true)); + coordinator.callCompleted(DUMMY_CALL); + ScheduledFuture scheduledTask = (ScheduledFuture) scheduler.getQueue().peek(); + coordinator.cancel(); + + assertThat(scheduledTask).isNotNull(); + ((Runnable) scheduledTask).run(); + + assertThat(continuationRan.get()).isFalse(); + } finally { + scheduler.shutdownNow(); + } + } + + @Test + public void cancel_cancelsAssociatedGate() { + AsyncGate gate = AsyncGate.create(1); + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + + coordinator.cancel(); + + assertThat(gate.isCancelled()).isTrue(); + } + + @Test + public void applyDrainAction_whenInFlightZeroAndStrategyWaits_forcesReevaluation() { + CelAsyncDrainStrategy alwaysWaitStrategy = (batch, active) -> CelAsyncDrainAction.waitForMore(); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder().setDrainStrategy(alwaysWaitStrategy).build(); + AsyncGate gate = AsyncGate.create(1); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + AtomicBoolean continuationRan = new AtomicBoolean(false); + registerWait(coordinator, () -> continuationRan.set(true)); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(continuationRan.get()).isTrue(); + assertThat(coordinator.isWaiting()).isFalse(); + } + + @Test + public void dispatchContinuation_whenExecutorThrows_invokesFailureCallback() { + Executor rejectingExecutor = + r -> { + throw new RejectedExecutionException("rejected"); + }; + AtomicReference failure = new AtomicReference<>(); + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncGate gate = AsyncGate.create(1); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, rejectingExecutor, failure::set); + acquirePermit(gate); + registerWait(coordinator, () -> {}); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(failure.get()).isInstanceOf(RejectedExecutionException.class); + } + + @Test + public void scheduleDebounce_whenSchedulerThrows_invokesFailureCallback() { + ScheduledThreadPoolExecutor rejectingScheduler = + new ScheduledThreadPoolExecutor(1) { + @Override + public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { + throw new RejectedExecutionException("scheduler rejected"); + } + }; + try { + AtomicReference failure = new AtomicReference<>(); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainReady(Duration.ofMinutes(10))) + .setScheduledExecutorService(rejectingScheduler) + .build(); + AsyncGate gate = AsyncGate.create(2); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, failure::set); + acquirePermit(gate); + acquirePermit(gate); + registerWait(coordinator, () -> {}); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(failure.get()).isInstanceOf(RejectedExecutionException.class); + } finally { + rejectingScheduler.shutdownNow(); + } + } + + @Test + public void + scheduleDebounce_whenCoordinatorCancelledConcurrently_cancelsScheduledFutureWithoutInterrupt() { + AtomicBoolean cancelledInsideScheduler = new AtomicBoolean(false); + AsyncCompletionCoordinator[] coordinatorHolder = new AsyncCompletionCoordinator[1]; + TrackingScheduler scheduler = + new TrackingScheduler() { + @Override + public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { + ScheduledFuture task = super.schedule(command, delay, unit); + if (coordinatorHolder[0] != null && !cancelledInsideScheduler.get()) { + cancelledInsideScheduler.set(true); + coordinatorHolder[0].cancel(); + } + return task; + } + }; + + try { + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainReady(Duration.ofMinutes(10))) + .setScheduledExecutorService(scheduler) + .build(); + AsyncGate gate = AsyncGate.create(2); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + coordinatorHolder[0] = coordinator; + acquirePermit(gate); + acquirePermit(gate); + coordinator.callCompleted(DUMMY_CALL); + registerWait(coordinator, () -> {}); + + ScheduledFuture scheduledTask = (ScheduledFuture) scheduler.getQueue().peek(); + assertThat(scheduledTask).isNotNull(); + assertThat(scheduledTask.isCancelled()).isTrue(); + assertThat(scheduler.lastMayInterrupt()).hasValue(false); + } finally { + scheduler.shutdownNow(); + } + } + + @Test + public void multiThreadedConcurrentCompletions_retainsSingleContinuationDispatch() + throws Exception { + int workerCount = 10; + ExecutorService workers = Executors.newFixedThreadPool(workerCount); + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + try { + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainAll()) + .setScheduledExecutorService(scheduler) + .build(); + AsyncGate gate = AsyncGate.create(workerCount); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, workers, t -> {}); + for (int i = 0; i < workerCount; i++) { + acquirePermit(gate); + } + AtomicInteger continuationDispatches = new AtomicInteger(); + CountDownLatch continuationLatch = new CountDownLatch(1); + CountDownLatch readyLatch = new CountDownLatch(workerCount); + CountDownLatch startLatch = new CountDownLatch(1); + + registerWait( + coordinator, + () -> { + continuationDispatches.incrementAndGet(); + continuationLatch.countDown(); + }); + + for (int i = 0; i < workerCount; i++) { + workers.execute( + () -> { + readyLatch.countDown(); + try { + startLatch.await(); + coordinator.callCompleted(DUMMY_CALL); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + } + + readyLatch.await(5, SECONDS); + startLatch.countDown(); + boolean continuationReached = continuationLatch.await(5, SECONDS); + workers.shutdown(); + boolean workersTerminated = workers.awaitTermination(5, SECONDS); + + assertThat(continuationReached).isTrue(); + assertThat(workersTerminated).isTrue(); + assertThat(continuationDispatches.get()).isEqualTo(1); + assertThat(coordinator.isWaiting()).isFalse(); + } finally { + workers.shutdownNow(); + scheduler.shutdownNow(); + } + } + + @Test + public void create_nullOptions_throwsNullPointerException() { + AsyncGate gate = AsyncGate.create(1); + + NullPointerException thrown = + assertThrows( + NullPointerException.class, + () -> AsyncCompletionCoordinator.create(null, gate, Runnable::run, t -> {})); + + assertThat(thrown).hasMessageThat().contains("options must not be null"); + } + + @Test + public void create_nullGate_throwsNullPointerException() { + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + + NullPointerException thrown = + assertThrows( + NullPointerException.class, + () -> AsyncCompletionCoordinator.create(options, null, Runnable::run, t -> {})); + + assertThat(thrown).hasMessageThat().contains("gate must not be null"); + } + + @Test + public void create_nullExecutor_throwsNullPointerException() { + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncGate gate = AsyncGate.create(1); + + NullPointerException thrown = + assertThrows( + NullPointerException.class, + () -> AsyncCompletionCoordinator.create(options, gate, null, t -> {})); + + assertThat(thrown).hasMessageThat().contains("continuationExecutor must not be null"); + } + + @Test + public void create_nullFailureCallback_throwsNullPointerException() { + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncGate gate = AsyncGate.create(1); + + NullPointerException thrown = + assertThrows( + NullPointerException.class, + () -> AsyncCompletionCoordinator.create(options, gate, Runnable::run, null)); + + assertThat(thrown).hasMessageThat().contains("failureCallback must not be null"); + } + + @Test + public void callCompleted_nullCall_throwsNullPointerException() { + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncGate gate = AsyncGate.create(1); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + + NullPointerException thrown = + assertThrows(NullPointerException.class, () -> coordinator.callCompleted(null)); + + assertThat(thrown).hasMessageThat().contains("call must not be null"); + } + + @Test + public void waitForCompletions_nullContinuation_throwsNullPointerException() { + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncGate gate = AsyncGate.create(1); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + + NullPointerException thrown = + assertThrows(NullPointerException.class, () -> coordinator.waitForCompletions(null)); + + assertThat(thrown).hasMessageThat().contains("continuationCallback must not be null"); + } + + @Test + public void staleTimerFromPreviousPass_doesNotTriggerContinuationOnSubsequentPass() { + ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1); + try { + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainReady(Duration.ofMinutes(10))) + .setScheduledExecutorService(scheduler) + .build(); + AsyncGate gate = AsyncGate.create(2); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + AtomicInteger pass1Count = new AtomicInteger(); + registerWait(coordinator, pass1Count::incrementAndGet); + coordinator.callCompleted(DUMMY_CALL); + ScheduledFuture pass1Timer = (ScheduledFuture) scheduler.getQueue().peek(); + coordinator.callCompleted(DUMMY_CALL); + acquirePermit(gate); + AtomicInteger pass2Count = new AtomicInteger(); + registerWait(coordinator, pass2Count::incrementAndGet); + + assertThat(pass1Timer).isNotNull(); + ((Runnable) pass1Timer).run(); + + assertThat(pass2Count.get()).isEqualTo(0); + assertThat(coordinator.isWaiting()).isTrue(); + } finally { + scheduler.shutdownNow(); + } + } + + @Test + public void drainAndReset_incrementsCycleIdAndClearsContinuation() { + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncGate gate = AsyncGate.create(1); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + registerWait(coordinator, () -> {}); + long initialCycleId = coordinator.cycleId(); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(coordinator.cycleId()).isGreaterThan(initialCycleId); + assertThat(coordinator.hasContinuation()).isFalse(); + } + + @Test + public void + waitForCompletions_lastCallCompletesDuringDrainStrategyEval_runsContinuationAndReturnsRegistered() { + AtomicReference coordinatorRef = new AtomicReference<>(); + CelAsyncDrainStrategy racingStrategy = new RacingDrainStrategy(coordinatorRef); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder().setDrainStrategy(racingStrategy).build(); + AsyncGate gate = AsyncGate.create(2); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + coordinatorRef.set(coordinator); + acquirePermit(gate); + acquirePermit(gate); + coordinator.callCompleted(DUMMY_CALL); + AtomicBoolean continuationRan = new AtomicBoolean(false); + + WaitResult result = coordinator.waitForCompletions(() -> continuationRan.set(true)); + + assertThat(result).isEqualTo(WaitResult.REGISTERED); + assertThat(continuationRan.get()).isTrue(); + assertThat(coordinator.isWaiting()).isFalse(); + } + + @Test + public void dispatchContinuation_directExecutorReentrantCompletions_doesNotCauseStackOverflow() { + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncGate gate = AsyncGate.create(1); + AtomicInteger step = new AtomicInteger(); + int targetSteps = 1000; + AtomicReference coordinatorRef = new AtomicReference<>(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + coordinatorRef.set(coordinator); + acquirePermit(gate); + registerWait( + coordinator, + new Runnable() { + @Override + public void run() { + if (step.incrementAndGet() < targetSteps) { + acquirePermit(gate); + registerWait(coordinatorRef.get(), this); + coordinatorRef.get().callCompleted(DUMMY_CALL); + } + } + }); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(step.get()).isEqualTo(targetSteps); + } + + @Test + public void dispatchContinuation_nestedCoordinatorsOnSameThread_doesNotHijackExecutor() { + AtomicBoolean coordinator2ExecutorUsed = new AtomicBoolean(false); + AsyncGate gate1 = AsyncGate.create(1); + AsyncGate gate2 = AsyncGate.create(1); + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncCompletionCoordinator coordinator1 = + AsyncCompletionCoordinator.create(options, gate1, Runnable::run, t -> {}); + AsyncCompletionCoordinator coordinator2 = + AsyncCompletionCoordinator.create( + options, + gate2, + task -> { + coordinator2ExecutorUsed.set(true); + task.run(); + }, + t -> {}); + acquirePermit(gate1); + acquirePermit(gate2); + registerWait( + coordinator1, + () -> { + registerWait(coordinator2, () -> {}); + coordinator2.callCompleted(DUMMY_CALL); + }); + + coordinator1.callCompleted(DUMMY_CALL); + + assertThat(coordinator2ExecutorUsed.get()).isTrue(); + } + + @Test + public void cancel_cancelsGateAndPreventsFutureAcquire() { + AsyncGate gate = AsyncGate.create(1); + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + + coordinator.cancel(); + + assertThat(gate.isCancelled()).isTrue(); + assertThat(gate.tryAcquire()).isFalse(); + assertThat(gate.activeCount()).isEqualTo(0); + } + + @Test + public void callCompleted_whenNoCallsInFlight_invokesFailureCallbackAndCancelsCoordinator() { + AsyncGate gate = AsyncGate.create(1); + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AtomicReference capturedFailure = new AtomicReference<>(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, capturedFailure::set); + + coordinator.callCompleted(DUMMY_CALL); + + Throwable failure = capturedFailure.get(); + assertThat(failure).isInstanceOf(IllegalStateException.class); + assertThat(failure).hasMessageThat().contains("callCompleted called with no active calls"); + assertThat(coordinator.isCancelled()).isTrue(); + assertThat(gate.isCancelled()).isTrue(); + } + + @Test + public void callCompleted_whenDrainStrategyThrows_invokesFailureCallbackAndCancels() { + RuntimeException failure = new RuntimeException("strategy failed"); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(new FailingDrainStrategy(failure)) + .build(); + AsyncGate gate = AsyncGate.create(1); + AtomicReference capturedFailure = new AtomicReference<>(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, capturedFailure::set); + acquirePermit(gate); + registerWait(coordinator, () -> {}); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(capturedFailure.get()).isSameInstanceAs(failure); + assertThat(coordinator.isCancelled()).isTrue(); + assertThat(gate.isCancelled()).isTrue(); + } + + @Test + public void waitForCompletions_whenDrainStrategyThrows_invokesFailureCallbackAndCancels() { + RuntimeException failure = new RuntimeException("strategy failed"); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(new FailingDrainStrategy(failure)) + .build(); + AsyncGate gate = AsyncGate.create(1); + AtomicReference capturedFailure = new AtomicReference<>(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, capturedFailure::set); + acquirePermit(gate); + coordinator.callCompleted(DUMMY_CALL); + acquirePermit(gate); + + WaitResult result = coordinator.waitForCompletions(() -> {}); + + assertThat(result).isEqualTo(WaitResult.CANCELLED); + assertThat(capturedFailure.get()).isSameInstanceAs(failure); + assertThat(coordinator.isCancelled()).isTrue(); + } + + @Test + public void waitForCompletions_whenCancelledDuringDrainStrategyEvaluation_returnsCancelled() { + AtomicReference coordinatorRef = new AtomicReference<>(); + CelAsyncDrainStrategy cancellingStrategy = new CancellingDrainStrategy(coordinatorRef); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder().setDrainStrategy(cancellingStrategy).build(); + AsyncGate gate = AsyncGate.create(2); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + coordinatorRef.set(coordinator); + acquirePermit(gate); + acquirePermit(gate); + coordinator.callCompleted(DUMMY_CALL); + AtomicBoolean continuationRan = new AtomicBoolean(false); + + WaitResult result = coordinator.waitForCompletions(() -> continuationRan.set(true)); + + assertThat(result).isEqualTo(WaitResult.CANCELLED); + assertThat(coordinator.isCancelled()).isTrue(); + assertThat(continuationRan.get()).isFalse(); + } + + @Test + public void + waitForCompletions_whenDrainStrategyReturnsWaitForMore_registersWithoutSchedulingTimer() { + CelAsyncDrainStrategy waitForMoreStrategy = + (batch, active) -> CelAsyncDrainAction.waitForMore(); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder().setDrainStrategy(waitForMoreStrategy).build(); + AsyncGate gate = AsyncGate.create(2); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + coordinator.callCompleted(DUMMY_CALL); + AtomicBoolean continuationRan = new AtomicBoolean(false); + + WaitResult result = coordinator.waitForCompletions(() -> continuationRan.set(true)); + + assertThat(result).isEqualTo(WaitResult.REGISTERED); + assertThat(coordinator.isWaiting()).isTrue(); + assertThat(coordinator.hasScheduledDebounceTimer()).isFalse(); + assertThat(continuationRan.get()).isFalse(); + } + + @Test + public void scheduleDebounce_whenSchedulerThrows_cancelsCoordinator() { + ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1); + scheduler.shutdown(); + try { + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainReady(Duration.ofMinutes(10))) + .setScheduledExecutorService(scheduler) + .build(); + AsyncGate gate = AsyncGate.create(2); + AtomicReference capturedFailure = new AtomicReference<>(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, capturedFailure::set); + acquirePermit(gate); + acquirePermit(gate); + registerWait(coordinator, () -> {}); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(capturedFailure.get()).isInstanceOf(RejectedExecutionException.class); + assertThat(coordinator.isCancelled()).isTrue(); + assertThat(gate.isCancelled()).isTrue(); + } finally { + scheduler.shutdownNow(); + } + } + + @Test + public void waitForCompletions_whenSchedulerRejectsDebounce_invokesFailureCallbackAndCancels() { + ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1); + scheduler.shutdown(); + try { + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainReady(Duration.ofMinutes(10))) + .setScheduledExecutorService(scheduler) + .build(); + AsyncGate gate = AsyncGate.create(2); + AtomicReference capturedFailure = new AtomicReference<>(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, capturedFailure::set); + acquirePermit(gate); + acquirePermit(gate); + coordinator.callCompleted(DUMMY_CALL); + + WaitResult result = coordinator.waitForCompletions(() -> {}); + + assertThat(result).isEqualTo(WaitResult.CANCELLED); + assertThat(capturedFailure.get()).isInstanceOf(RejectedExecutionException.class); + assertThat(coordinator.isCancelled()).isTrue(); + assertThat(gate.isCancelled()).isTrue(); + assertThat(coordinator.isWaiting()).isFalse(); + assertThat(coordinator.hasContinuation()).isFalse(); + assertThat(coordinator.hasScheduledDebounceTimer()).isFalse(); + } finally { + scheduler.shutdownNow(); + } + } + + @Test + public void waitForCompletions_whenIntermediateCallArrivesDuringStrategyEval_preservesDebounce() { + ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1); + try { + AtomicReference coordinatorRef = new AtomicReference<>(); + CelAsyncDrainStrategy racingStrategy = + new SingleShotRacingDrainStrategy( + coordinatorRef, CelAsyncDrainAction.waitDuration(Duration.ofMinutes(5))); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(racingStrategy) + .setScheduledExecutorService(scheduler) + .build(); + AsyncGate gate = AsyncGate.create(3); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + coordinatorRef.set(coordinator); + acquirePermit(gate); + acquirePermit(gate); + acquirePermit(gate); + coordinator.callCompleted(DUMMY_CALL); + + WaitResult result = coordinator.waitForCompletions(() -> {}); + + assertThat(result).isEqualTo(WaitResult.REGISTERED); + assertThat(coordinator.isWaiting()).isTrue(); + assertThat(coordinator.hasScheduledDebounceTimer()).isTrue(); + } finally { + scheduler.shutdownNow(); + } + } + + @Test + public void dispatchContinuation_whenExecutorThrows_cancelsCoordinatorAndNotifiesCallback() { + RejectedExecutionException failure = new RejectedExecutionException("rejected"); + Executor rejectingExecutor = + task -> { + throw failure; + }; + AsyncGate gate = AsyncGate.create(1); + AtomicReference capturedFailure = new AtomicReference<>(); + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, rejectingExecutor, capturedFailure::set); + acquirePermit(gate); + registerWait(coordinator, () -> {}); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(capturedFailure.get()).isSameInstanceAs(failure); + assertThat(coordinator.isCancelled()).isTrue(); + assertThat(gate.isCancelled()).isTrue(); + } + + @Test + public void failAndCancel_concurrentFailures_notifiesCallbackAtMostOnce() { + RuntimeException failure1 = new RuntimeException("error 1"); + AtomicInteger callbackCount = new AtomicInteger(); + AsyncGate gate = AsyncGate.create(2); + FailingDrainStrategy failingStrategy = new FailingDrainStrategy(failure1); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder().setDrainStrategy(failingStrategy).build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create( + options, gate, Runnable::run, t -> callbackCount.incrementAndGet()); + acquirePermit(gate); + registerWait(coordinator, () -> {}); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(callbackCount.get()).isEqualTo(1); + assertThat(coordinator.isCancelled()).isTrue(); + } + + @Test + public void applyDrainAction_whenGenerationStale_discardsStaleAction() { + ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1); + try { + AtomicReference coordinatorRef = new AtomicReference<>(); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy( + new StaleRacingDrainStrategy(coordinatorRef, CelAsyncDrainAction.reevaluate())) + .setScheduledExecutorService(scheduler) + .build(); + AsyncGate gate = AsyncGate.create(3); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + coordinatorRef.set(coordinator); + acquirePermit(gate); + acquirePermit(gate); + acquirePermit(gate); + registerWait(coordinator, () -> {}); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(coordinator.isWaiting()).isTrue(); + assertThat(coordinator.hasScheduledDebounceTimer()).isTrue(); + } finally { + scheduler.shutdownNow(); + } + } + + @Test + public void applyDrainAction_whenGenerationStaleAndActionIsWaitForMore_keepsNewerDebounceTimer() { + TrackingScheduler scheduler = new TrackingScheduler(); + try { + AtomicReference coordinatorRef = new AtomicReference<>(); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy( + new StaleRacingDrainStrategy(coordinatorRef, CelAsyncDrainAction.waitForMore())) + .setScheduledExecutorService(scheduler) + .build(); + AsyncGate gate = AsyncGate.create(3); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + coordinatorRef.set(coordinator); + acquirePermit(gate); + acquirePermit(gate); + acquirePermit(gate); + registerWait(coordinator, () -> {}); + + coordinator.callCompleted(DUMMY_CALL); + + // The re-entrant completion bumped the debounce generation and scheduled a newer timer, so + // the outer (now stale) waitForMore action must not cancel it. + ScheduledFuture newerTimer = (ScheduledFuture) scheduler.getQueue().peek(); + assertThat(newerTimer).isNotNull(); + assertThat(newerTimer.isCancelled()).isFalse(); + assertThat(coordinator.hasScheduledDebounceTimer()).isTrue(); + assertThat(coordinator.isWaiting()).isTrue(); + } finally { + scheduler.shutdownNow(); + } + } + + @Test + public void + failAndCancel_whenFailureCallbackThrows_suppressesCallbackExceptionAndCancelsCoordinator() { + RuntimeException strategyError = new RuntimeException("strategy failed"); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(new FailingDrainStrategy(strategyError)) + .build(); + AsyncGate gate = AsyncGate.create(1); + RuntimeException userCallbackError = new RuntimeException("user callback failure"); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create( + options, + gate, + Runnable::run, + t -> { + throw userCallbackError; + }); + acquirePermit(gate); + registerWait(coordinator, () -> {}); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(coordinator.isCancelled()).isTrue(); + assertThat(gate.isCancelled()).isTrue(); + assertThat(strategyError.getSuppressed()).asList().containsExactly(userCallbackError); + } + + @Test + public void dispatchContinuation_whenFailureCallbackThrows_doesNotEscapeAndSuppressesException() { + AsyncGate gate = AsyncGate.create(1); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainNone()) + .build(); + RuntimeException executorError = new RuntimeException("executor error"); + RuntimeException callbackError = new RuntimeException("callback error"); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create( + options, + gate, + task -> { + throw executorError; + }, + t -> { + throw callbackError; + }); + acquirePermit(gate); + AtomicBoolean continuationRan = new AtomicBoolean(false); + registerWait(coordinator, () -> continuationRan.set(true)); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(coordinator.isCancelled()).isTrue(); + assertThat(executorError.getSuppressed()).asList().containsExactly(callbackError); + } + + @Test + public void callCompleted_whenCancelledAndNoCallsInFlight_invokesFailureCallback() { + AsyncGate gate = AsyncGate.create(1); + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AtomicReference capturedFailure = new AtomicReference<>(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, capturedFailure::set); + coordinator.cancel(); + + coordinator.callCompleted(DUMMY_CALL); + + Throwable failure = capturedFailure.get(); + assertThat(failure).isInstanceOf(IllegalStateException.class); + assertThat(failure).hasMessageThat().contains("callCompleted called with no active calls"); + } + + @Test + public void failAndCancel_whenFailureCallbackRethrowsSameThrowable_doesNotThrowSelfSuppression() { + RuntimeException error = new RuntimeException("strategy failure"); + AsyncGate gate = AsyncGate.create(1); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(new FailingDrainStrategy(error)) + .build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create( + options, + gate, + Runnable::run, + t -> { + throw (RuntimeException) t; + }); + acquirePermit(gate); + registerWait(coordinator, () -> {}); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(coordinator.isCancelled()).isTrue(); + assertThat(gate.isCancelled()).isTrue(); + } + + @Test + public void callCompleted_whenDrainDurationOverflowsNanos_failsCoordinatorGracefully() { + CelAsyncDrainStrategy overflowStrategy = + (batch, active) -> CelAsyncDrainAction.waitDuration(Duration.ofSeconds(Long.MAX_VALUE)); + AsyncGate gate = AsyncGate.create(2); + AtomicReference capturedFailure = new AtomicReference<>(); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder().setDrainStrategy(overflowStrategy).build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, capturedFailure::set); + acquirePermit(gate); + acquirePermit(gate); + registerWait(coordinator, () -> {}); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(capturedFailure.get()).isInstanceOf(ArithmeticException.class); + assertThat(coordinator.isCancelled()).isTrue(); + } + + @Test + public void waitForCompletions_whenDrainDurationOverflowsNanos_failsCoordinatorGracefully() { + CelAsyncDrainStrategy overflowStrategy = + (batch, active) -> CelAsyncDrainAction.waitDuration(Duration.ofSeconds(Long.MAX_VALUE)); + AsyncGate gate = AsyncGate.create(2); + AtomicReference capturedFailure = new AtomicReference<>(); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder().setDrainStrategy(overflowStrategy).build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, capturedFailure::set); + acquirePermit(gate); + acquirePermit(gate); + coordinator.callCompleted(DUMMY_CALL); + + WaitResult result = coordinator.waitForCompletions(() -> {}); + + assertThat(result).isEqualTo(WaitResult.CANCELLED); + assertThat(capturedFailure.get()).isInstanceOf(ArithmeticException.class); + assertThat(coordinator.isCancelled()).isTrue(); + } + + @Test + public void failAndCancel_whenCalledMultipleTimes_invokesFailureCallbackOnlyOnce() { + AsyncGate gate = AsyncGate.create(1); + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AtomicInteger failureCount = new AtomicInteger(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create( + options, gate, Runnable::run, t -> failureCount.incrementAndGet()); + + coordinator.callCompleted(DUMMY_CALL); + coordinator.callCompleted(DUMMY_CALL); + + assertThat(failureCount.get()).isEqualTo(1); + } + + @Test + public void create_initialState_hasZeroGenerationsAndCleanDefaults() { + CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.builder().build(); + AsyncGate gate = AsyncGate.create(1); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + + assertThat(coordinator.cycleId()).isEqualTo(0); + assertThat(coordinator.debounceGeneration()).isEqualTo(0); + assertThat(coordinator.isWaiting()).isFalse(); + assertThat(coordinator.isCancelled()).isFalse(); + assertThat(coordinator.hasPendingBatch()).isFalse(); + assertThat(coordinator.hasScheduledDebounceTimer()).isFalse(); + assertThat(coordinator.hasContinuation()).isFalse(); + } + + @Test + public void + callCompleted_whenDebounceTimerPendingAndNextActionIsReevaluate_cancelsPendingDebounceTimerWithoutInterrupt() { + TrackingScheduler scheduler = new TrackingScheduler(); + try { + AtomicInteger callCount = new AtomicInteger(); + CelAsyncDrainStrategy strategy = new DebounceThenReevaluateDrainStrategy(callCount); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(strategy) + .setScheduledExecutorService(scheduler) + .build(); + AsyncGate gate = AsyncGate.create(3); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + acquirePermit(gate); + registerWait(coordinator, () -> {}); + coordinator.callCompleted(DUMMY_CALL); + ScheduledFuture scheduledTask = (ScheduledFuture) scheduler.getQueue().peek(); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(scheduledTask).isNotNull(); + assertThat(scheduledTask.isCancelled()).isTrue(); + assertThat(scheduler.lastMayInterrupt()).hasValue(false); + assertThat(coordinator.hasScheduledDebounceTimer()).isFalse(); + assertThat(coordinator.isWaiting()).isFalse(); + } finally { + scheduler.shutdownNow(); + } + } + + @Test + public void + waitForCompletions_whenPendingBatchAndStrategyWaits_schedulesDebounceTimerAndReturnsRegistered() { + TrackingScheduler scheduler = new TrackingScheduler(); + try { + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainReady(Duration.ofMinutes(5))) + .setScheduledExecutorService(scheduler) + .build(); + AsyncGate gate = AsyncGate.create(2); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + coordinator.callCompleted(DUMMY_CALL); + + WaitResult result = coordinator.waitForCompletions(() -> {}); + + assertThat(result).isEqualTo(WaitResult.REGISTERED); + assertThat(coordinator.hasScheduledDebounceTimer()).isTrue(); + assertThat(coordinator.isWaiting()).isTrue(); + } finally { + scheduler.shutdownNow(); + } + } + + @Test + public void + waitForCompletions_whenWaitingWithDrainAllStrategyAndCallsInFlight_returnsRegistered() { + AsyncGate gate = AsyncGate.create(2); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(CelAsyncDrainStrategy.drainAll()) + .build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + coordinator.callCompleted(DUMMY_CALL); + + WaitResult result = coordinator.waitForCompletions(() -> {}); + + assertThat(result).isEqualTo(WaitResult.REGISTERED); + assertThat(coordinator.isWaiting()).isTrue(); + assertThat(coordinator.hasPendingBatch()).isTrue(); + assertThat(coordinator.hasScheduledDebounceTimer()).isFalse(); + } + + @Test + public void callCompleted_passesActiveCountToDrainStrategy() { + AsyncGate gate = AsyncGate.create(3); + AtomicInteger capturedInFlight = new AtomicInteger(-1); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(new CapturingDrainStrategy(capturedInFlight)) + .build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + acquirePermit(gate); + registerWait(coordinator, () -> {}); + + coordinator.callCompleted(DUMMY_CALL); + + assertThat(capturedInFlight.get()).isEqualTo(2); + } + + @Test + public void waitForCompletions_passesActiveCountToDrainStrategy() { + AsyncGate gate = AsyncGate.create(3); + AtomicInteger capturedInFlight = new AtomicInteger(-1); + CelAsyncEvaluationOptions options = + CelAsyncEvaluationOptions.builder() + .setDrainStrategy(new CapturingDrainStrategy(capturedInFlight)) + .build(); + AsyncCompletionCoordinator coordinator = + AsyncCompletionCoordinator.create(options, gate, Runnable::run, t -> {}); + acquirePermit(gate); + acquirePermit(gate); + acquirePermit(gate); + coordinator.callCompleted(DUMMY_CALL); + + WaitResult result = coordinator.waitForCompletions(() -> {}); + + assertThat(result).isEqualTo(WaitResult.REEVALUATE_NOW); + assertThat(capturedInFlight.get()).isEqualTo(2); + } + + private static void acquirePermit(AsyncGate gate) { + checkState(gate.tryAcquire(), "Failed to acquire permit"); + } + + private static void registerWait(AsyncCompletionCoordinator coordinator, Runnable continuation) { + checkState( + coordinator.waitForCompletions(continuation) == WaitResult.REGISTERED, + "Expected REGISTERED result when waiting for completions"); + } + + /** + * Re-enters {@link AsyncCompletionCoordinator#callCompleted} during the first {@code nextAction} + * call, so the nested pass bumps the debounce generation and schedules a newer timer before the + * outer pass applies {@code staleAction}. + */ + private static final class StaleRacingDrainStrategy implements CelAsyncDrainStrategy { + @SuppressWarnings("Immutable") // Test-only mutable holder. + private final AtomicReference coordinatorRef; + + @SuppressWarnings("Immutable") // Test-only mutable flag. + private final AtomicBoolean first = new AtomicBoolean(true); + + private final CelAsyncDrainAction staleAction; + + @Override + public CelAsyncDrainAction nextAction(List batch, int active) { + if (first.compareAndSet(true, false)) { + coordinatorRef.get().callCompleted(DUMMY_CALL); + return staleAction; + } + return CelAsyncDrainAction.waitDuration(Duration.ofMinutes(5)); + } + + private StaleRacingDrainStrategy( + AtomicReference coordinatorRef, + CelAsyncDrainAction staleAction) { + this.coordinatorRef = coordinatorRef; + this.staleAction = staleAction; + } + } + + private static final class CapturingDrainStrategy implements CelAsyncDrainStrategy { + @SuppressWarnings("Immutable") // Test-only mutable capture. + private final AtomicInteger capturedInFlight; + + @Override + public CelAsyncDrainAction nextAction(List batch, int active) { + capturedInFlight.set(active); + return CelAsyncDrainAction.reevaluate(); + } + + private CapturingDrainStrategy(AtomicInteger capturedInFlight) { + this.capturedInFlight = capturedInFlight; + } + } + + private static final class SingleShotRacingDrainStrategy implements CelAsyncDrainStrategy { + @SuppressWarnings("Immutable") // Test-only mutable holder. + private final AtomicReference coordinatorRef; + + @SuppressWarnings("Immutable") // Test-only mutable flag. + private final AtomicBoolean completed; + + private final CelAsyncDrainAction returnAction; + + @Override + public CelAsyncDrainAction nextAction(List batch, int active) { + if (completed.compareAndSet(false, true)) { + coordinatorRef.get().callCompleted(DUMMY_CALL); + } + return returnAction; + } + + private SingleShotRacingDrainStrategy( + AtomicReference coordinatorRef, + CelAsyncDrainAction returnAction) { + this.coordinatorRef = coordinatorRef; + this.completed = new AtomicBoolean(false); + this.returnAction = returnAction; + } + } + + private static final class FailingDrainStrategy implements CelAsyncDrainStrategy { + @SuppressWarnings("Immutable") // Test-only throwable holder. + private final RuntimeException failure; + + @Override + public CelAsyncDrainAction nextAction(List batch, int active) { + throw failure; + } + + private FailingDrainStrategy(RuntimeException failure) { + this.failure = failure; + } + } + + private static final class RacingDrainStrategy implements CelAsyncDrainStrategy { + @SuppressWarnings("Immutable") // Test-only mutable holder. + private final AtomicReference coordinatorRef; + + @Override + public CelAsyncDrainAction nextAction(List batch, int active) { + if (active > 0) { + coordinatorRef.get().callCompleted(DUMMY_CALL); + } + return CelAsyncDrainAction.waitForMore(); + } + + private RacingDrainStrategy(AtomicReference coordinatorRef) { + this.coordinatorRef = coordinatorRef; + } + } + + private static final class CancellingDrainStrategy implements CelAsyncDrainStrategy { + @SuppressWarnings("Immutable") // Test-only mutable holder. + private final AtomicReference coordinatorRef; + + @Override + public CelAsyncDrainAction nextAction(List batch, int active) { + coordinatorRef.get().cancel(); + return CelAsyncDrainAction.waitForMore(); + } + + private CancellingDrainStrategy(AtomicReference coordinatorRef) { + this.coordinatorRef = coordinatorRef; + } + } + + private static final class TwoPhaseDrainStrategy implements CelAsyncDrainStrategy { + @SuppressWarnings("Immutable") // Test-only mutable counter. + private final AtomicInteger callCount; + + @Override + public CelAsyncDrainAction nextAction(List batch, int active) { + return callCount.incrementAndGet() == 1 + ? CelAsyncDrainAction.waitDuration(Duration.ofMinutes(10)) + : CelAsyncDrainAction.waitForMore(); + } + + private TwoPhaseDrainStrategy(AtomicInteger callCount) { + this.callCount = callCount; + } + } + + private static final class DebounceThenReevaluateDrainStrategy implements CelAsyncDrainStrategy { + @SuppressWarnings("Immutable") // Test-only mutable counter. + private final AtomicInteger callCount; + + @Override + public CelAsyncDrainAction nextAction(List batch, int active) { + return callCount.incrementAndGet() == 1 + ? CelAsyncDrainAction.waitDuration(Duration.ofMinutes(10)) + : CelAsyncDrainAction.reevaluate(); + } + + private DebounceThenReevaluateDrainStrategy(AtomicInteger callCount) { + this.callCount = callCount; + } + } + + private static class TrackingScheduler extends ScheduledThreadPoolExecutor { + private final AtomicReference lastMayInterrupt = new AtomicReference<>(); + + Optional lastMayInterrupt() { + return Optional.ofNullable(lastMayInterrupt.get()); + } + + @Override + public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { + ScheduledFuture delegate = super.schedule(command, delay, unit); + return new TrackingScheduledFuture(delegate, lastMayInterrupt); + } + + TrackingScheduler() { + super(1); + } + } + + private static final class TrackingScheduledFuture implements ScheduledFuture, Runnable { + private final ScheduledFuture delegate; + private final AtomicReference mayInterruptRef; + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + mayInterruptRef.set(mayInterruptIfRunning); + return delegate.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return delegate.isCancelled(); + } + + @Override + public boolean isDone() { + return delegate.isDone(); + } + + @Override + public Void get() throws InterruptedException, ExecutionException { + delegate.get(); + return null; + } + + @Override + public Void get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + delegate.get(timeout, unit); + return null; + } + + @Override + public long getDelay(TimeUnit unit) { + return delegate.getDelay(unit); + } + + @Override + public int compareTo(Delayed o) { + return delegate.compareTo(o); + } + + @Override + public void run() { + if (delegate instanceof Runnable) { + ((Runnable) delegate).run(); + } + } + + private TrackingScheduledFuture( + ScheduledFuture delegate, AtomicReference mayInterruptRef) { + this.delegate = delegate; + this.mayInterruptRef = mayInterruptRef; + } + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel index 5ff4b4d81..38d1d0d70 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel @@ -40,6 +40,9 @@ java_library( "//extensions", "//parser:macro", "//runtime", + "//runtime:async_call", + "//runtime:async_drain_strategy", + "//runtime:async_options", "//runtime:descriptor_type_resolver", "//runtime:dispatcher", "//runtime:function_binding", @@ -49,6 +52,7 @@ java_library( "//runtime:runtime_helpers", "//runtime:standard_functions", "//runtime:unknown_attributes", + "//runtime/planner:async_completion_coordinator", "//runtime/planner:async_gate", "//runtime/planner:program_planner", "//runtime/standard:type",