diff --git a/.bazelrc b/.bazelrc index 968597053..34a59ec39 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 @@ -10,5 +16,8 @@ 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/.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/.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/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 060b83bdd..8a34af31c 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -15,7 +15,7 @@ concurrency: cancel-in-progress: true jobs: - Static-Checks: + Bazel-Build-Java8: runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -30,16 +30,21 @@ 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 - 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 @@ -55,15 +60,13 @@ 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 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. @@ -82,7 +85,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 @@ -92,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 @@ -121,4 +124,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 }} diff --git a/BUILD.bazel b/BUILD.bazel index 024908625..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", ], ) @@ -95,6 +96,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 007adcb3c..bec7543d6 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 = "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 = "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.24.0", repo_name = "cel_spec") +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.26.5") switched_rules = use_extension("@com_google_googleapis//:extensions.bzl", "switched_rules") switched_rules.use_languages(java = True) @@ -35,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" + +GUAVA_VERSION = "33.6.0" + +JLINE_VERSION = "3.30.16" -TRUTH_VERSION = "1.4.4" +TRUTH_VERSION = "1.4.5" -PROTOBUF_JAVA_VERSION = "4.33.5" +PROTOBUF_JAVA_VERSION = "4.35.1" -CEL_VERSION = "0.12.0-SNAPSHOT" +CEL_VERSION = "0.14.0" # Compile only artifacts [ @@ -53,7 +62,7 @@ CEL_VERSION = "0.12.0-SNAPSHOT" ) 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", ]] ] @@ -67,8 +76,8 @@ CEL_VERSION = "0.12.0-SNAPSHOT" ) 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, @@ -81,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, @@ -90,10 +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:" + 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", @@ -121,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") @@ -132,3 +147,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/README.md b/README.md index f46a1f8c6..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.11.1 + 0.14.0 ``` **Gradle** ```gradle -implementation 'dev.cel:cel:0.11.1' +implementation 'dev.cel:cel:0.14.0' ``` Then run this example: @@ -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( @@ -376,14 +376,14 @@ 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 [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-expr/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/bundle/BUILD.bazel b/bundle/BUILD.bazel index 7f21cf219..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( @@ -27,6 +30,12 @@ java_library( java_library( name = "environment_exporter", - visibility = ["//:internal"], 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 0201a5807..be4fade3d 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,11 +19,56 @@ 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_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", + "//runtime:runtime_planner_impl", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +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", @@ -36,16 +79,14 @@ java_library( "//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", @@ -56,6 +97,7 @@ java_library( name = "environment", srcs = [ "CelEnvironment.java", + "TypeSpecifierParser.java", ], tags = [ ], @@ -64,11 +106,13 @@ java_library( ":required_fields_checker", "//:auto_value", "//bundle:cel", + "//checker:proto_type_mask", "//checker:standard_decl", "//common:compiler_common", "//common:container", "//common:options", "//common:source", + "//common/formats:parser_context", "//common/types", "//common/types:type_providers", "//compiler:compiler_builder", @@ -126,13 +170,14 @@ java_library( ":environment", "//:auto_value", "//bundle:cel", + "//checker:checker_builder", "//checker:standard_decl", "//common:compiler_common", "//common:options", "//common/internal:env_visitor", "//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/CelBuilder.java b/bundle/src/main/java/dev/cel/bundle/CelBuilder.java index 1dadaeb39..53eb0126b 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); @@ -203,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. * @@ -281,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); @@ -303,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/CelEnvironment.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java index b54e3ca51..f26d4e3fd 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; @@ -43,15 +44,18 @@ 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; 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; import java.util.Optional; +import java.util.function.ObjIntConsumer; /** * CelEnvironment is a native representation of a CEL environment for compiler and runtime. This @@ -69,9 +73,30 @@ 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", + CelOptions.Builder::maxExpressionCodePointSize, + "cel.limit.parse_error_recovery", + CelOptions.Builder::maxParseErrorRecoveryLimit, + "cel.limit.parse_recursion_depth", + CelOptions.Builder::maxParseRecursionDepth, + "cel.limit.expression_node_count", + CelOptions.Builder::maxParseExpressionNodeCount); + + private static final ImmutableMap FEATURE_HANDLERS = + ImmutableMap.of( + "cel.feature.macro_call_tracking", + CelOptions.Builder::populateMacroCalls, + "cel.feature.backtick_escape_syntax", + CelOptions.Builder::enableQuotedIdentifierSyntax, + "cel.feature.cross_type_numeric_comparisons", + CelOptions.Builder::enableHeterogeneousNumericComparisons); /** Environment source in textual format (ex: textproto, YAML). */ public abstract Optional source(); @@ -79,10 +104,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 @@ -108,6 +131,15 @@ 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(); + + /** 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 { @@ -159,6 +191,22 @@ 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 featureFlags); + + @CanIgnoreReturnValue + public Builder setLimits(Limit... limits) { + return setLimits(ImmutableSet.copyOf(limits)); + } + + public abstract Builder setLimits(ImmutableSet limits); + + public abstract Builder setContextVariable(ContextVariable contextVariable); + abstract CelEnvironment autoBuild(); @CheckReturnValue @@ -186,20 +234,22 @@ public static Builder newBuilder() { return new AutoValue_CelEnvironment.Builder() .setName("") .setDescription("") - .setContainer(CelContainer.ofName("")) .setVariables(ImmutableSet.of()) - .setFunctions(ImmutableSet.of()); + .setFunctions(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 = applyEnvironmentOptions(celOptions); try { CelTypeProvider celTypeProvider = celCompiler.getTypeProvider(); CelCompilerBuilder compilerBuilder = celCompiler .toCompilerBuilder() - .setContainer(container()) + .setOptions(celOptions) .setTypeProvider(celTypeProvider) .addVarDeclarations( variables().stream() @@ -210,10 +260,18 @@ public CelCompiler extend(CelCompiler celCompiler, CelOptions celOptions) .map(f -> f.toCelFunctionDecl(celTypeProvider)) .collect(toImmutableList())); + container().ifPresent(compilerBuilder::setContainer); + addAllCompilerExtensions(compilerBuilder, 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); @@ -222,19 +280,39 @@ 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 = applyEnvironmentOptions(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 applyEnvironmentOptions(CelOptions celOptions) { + CelOptions.Builder optionsBuilder = celOptions.toBuilder(); + for (FeatureFlag featureFlag : features()) { + 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(); + } + private void addAllCompilerExtensions( CelCompilerBuilder celCompilerBuilder, CelOptions celOptions) { // TODO: Add capability to accept user defined exceptions @@ -250,7 +328,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 +342,7 @@ private void addAllRuntimeExtensions(CelRuntimeBuilder celRuntimeBuilder, CelOpt celRuntimeBuilder.addLibraries(celRuntimeLibrary); } } + return celRuntimeBuilder.build(); } private void applyStandardLibrarySubset(CelCompilerBuilder compilerBuilder) { @@ -339,6 +420,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 { @@ -349,6 +441,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 { @@ -361,6 +455,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( @@ -392,6 +488,8 @@ public abstract static class FunctionDecl { public abstract String name(); + public abstract Optional description(); + public abstract ImmutableSet overloads(); /** Builder for {@link FunctionDecl}. */ @@ -404,6 +502,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 @@ -452,6 +552,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(); @@ -470,8 +573,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)); @@ -576,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); } @@ -600,11 +729,19 @@ 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()); } + if (name().equals("dyn")) { + return SimpleType.DYN; + } + CelType simpleType = SimpleType.findByName(name()).orElse(null); if (simpleType != null) { return simpleType; @@ -625,6 +762,39 @@ 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 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. @@ -734,6 +904,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(), @@ -948,4 +1119,9 @@ public static OverloadSelector.Builder newBuilder() { } } } + + @FunctionalInterface + private 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 01410ad0d..6e10edd92 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; @@ -38,9 +40,10 @@ 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; 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,51 @@ 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()); + 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())); + } + if (options.maxParseExpressionNodeCount() != CelOptions.DEFAULT.maxParseExpressionNodeCount()) { + limits.add( + CelEnvironment.Limit.create( + "cel.limit.expression_node_count", options.maxParseExpressionNodeCount())); + } + envBuilder.setLimits(limits.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() { @@ -423,7 +489,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/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java index 8c19fcfa6..821ca6586 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java @@ -22,12 +22,12 @@ 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; 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; @@ -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; @@ -58,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 = @@ -143,6 +144,115 @@ 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 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); @@ -210,6 +320,37 @@ 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": + 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(); @@ -243,6 +384,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( @@ -318,6 +462,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; @@ -331,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)) { @@ -369,6 +516,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; @@ -384,8 +534,26 @@ private static ImmutableSet parseOverloads(ParserContext ctx return overloadSetBuilder.build(); } - private static ImmutableList parseOverloadArguments( - ParserContext ctx, Node node) { + 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 ImmutableList parseOverloadArguments(ParserContext ctx, Node node) { long listValueId = ctx.collectMetadata(node); if (!assertYamlType(ctx, listValueId, node, YamlNodeType.LIST)) { return ImmutableList.of(); @@ -622,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; @@ -631,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(); @@ -756,6 +928,15 @@ private CelEnvironment.Builder parseConfig(ParserContext ctx, Node node) { case "stdlib": builder.setStandardLibrarySubset(parseLibrarySubset(ctx, valueNode)); break; + case "features": + builder.setFeatures(parseFeatures(ctx, valueNode)); + break; + 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/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java index 81f206b94..9d5b4b69e 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java @@ -60,6 +60,8 @@ 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()); + this.multiRepresenters.put(CelEnvironment.Limit.class, new RepresentLimit()); } public static String toYaml(CelEnvironment environment) { @@ -77,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()); @@ -94,6 +94,12 @@ 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()); + } + if (!environment.limits().isEmpty()) { + configMap.put("limits", environment.limits().asList()); + } return represent(configMap.buildOrThrow()); } } @@ -258,4 +264,30 @@ 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()); + } + } + + 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/main/java/dev/cel/bundle/CelFactory.java b/bundle/src/main/java/dev/cel/bundle/CelFactory.java index 6cc6d8192..79acccc93 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelFactory.java +++ b/bundle/src/main/java/dev/cel/bundle/CelFactory.java @@ -14,12 +14,14 @@ 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; 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. */ @@ -33,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()), @@ -44,6 +61,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/bundle/src/main/java/dev/cel/bundle/CelImpl.java b/bundle/src/main/java/dev/cel/bundle/CelImpl.java index bc92cca7a..b8c7c36e9 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelImpl.java +++ b/bundle/src/main/java/dev/cel/bundle/CelImpl.java @@ -54,6 +54,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; @@ -142,6 +143,8 @@ 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. */ static CelBuilder newBuilder( CelCompilerBuilder compilerBuilder, CelRuntimeBuilder celRuntimeBuilder) { @@ -199,6 +202,10 @@ public CelContainer container() { @Override public CelBuilder setContainer(CelContainer container) { compilerBuilder.setContainer(container); + if (runtimeBuilder instanceof CelRuntimeImpl.Builder) { + runtimeBuilder.setContainer(container); + } + return this; } @@ -274,6 +281,18 @@ public CelBuilder addFunctionBindings(Iterable lateBoundFunctionNames) { + runtimeBuilder.addLateBoundFunctions(lateBoundFunctionNames); + return this; + } + @Override public CelBuilder setResultType(CelType resultType) { checkNotNull(resultType); @@ -298,6 +317,11 @@ public CelBuilder setValueProvider(CelValueProvider celValueProvider) { return this; } + @Override + public CelValueProvider valueProvider() { + return runtimeBuilder.valueProvider(); + } + @Override @Deprecated public Builder setTypeProvider(TypeProvider typeProvider) { @@ -308,6 +332,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; } @@ -352,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/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/BUILD.bazel b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel index cd33dd67d..ddd2e7285 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", @@ -17,6 +19,7 @@ java_library( deps = [ "//:java_truth", "//bundle:cel", + "//bundle:cel_impl", "//bundle:environment", "//bundle:environment_exception", "//bundle:environment_exporter", @@ -24,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", @@ -53,7 +57,10 @@ 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", "//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/CelEnvironmentExporterTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java index d6608a9d4..7560a12aa 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. @@ -255,10 +333,40 @@ 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(); } -} + @Test + public void options() { + Cel cel = + CelFactory.standardCelBuilder() + .setOptions( + CelOptions.current() + .maxExpressionCodePointSize(100) + .maxParseErrorRecoveryLimit(10) + .maxParseRecursionDepth(10) + .maxParseExpressionNodeCount(500) + .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), + 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 6bc84a48f..a48ea0ff8 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(); @@ -100,6 +103,122 @@ 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_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), + CelEnvironment.Limit.create("cel.limit.expression_node_count", 500)) + .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); + assertThat(checkerOptions.maxParseExpressionNodeCount()).isEqualTo(500); + + 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_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 = + 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 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 = @@ -342,4 +461,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 d69d0517b..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; @@ -40,8 +42,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 +83,62 @@ 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_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 = @@ -322,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 = @@ -521,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( @@ -619,9 +927,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" + " | ..^"), @@ -672,6 +978,40 @@ 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" + + " | 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; @@ -769,30 +1109,87 @@ 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(), + 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( + 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 7e4be0912..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( @@ -126,6 +188,13 @@ 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)) + .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/bundle/src/test/java/dev/cel/bundle/CelImplTest.java b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java index 9f7083c92..4f82411a3 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java @@ -59,8 +59,8 @@ 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.CelStandardDeclarations; import dev.cel.checker.DescriptorTypeProvider; import dev.cel.checker.ProtoTypeMask; import dev.cel.checker.TypeProvider; @@ -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; @@ -110,10 +111,13 @@ 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; -import dev.cel.testing.testdata.SingleFileProto.SingleFile; +import dev.cel.testing.CelRuntimeFlavor; +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; @@ -229,9 +233,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); @@ -243,9 +245,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); @@ -560,23 +560,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 { @@ -1003,9 +986,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()) @@ -1026,9 +1008,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() @@ -1426,25 +1406,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 { @@ -2109,7 +2070,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 = @@ -2143,20 +2103,93 @@ 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 CelRuntimeFlavor runtimeFlavor) + throws Exception { + Cel cel = setupEnv(runtimeFlavor.builder()); + 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(); + } - assertThat(result).isEqualTo("foo"); + @Test + 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 && " + + "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).isTrue(); + } + + @Test + 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 &&" + + " 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 @@ -2172,7 +2205,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( @@ -2184,7 +2217,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 @@ -2195,7 +2229,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( @@ -2244,4 +2278,45 @@ private static TypeProvider aliasingProvider(ImmutableMap typeAlia } }; } + + 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(); + } + + @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/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/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/checker/src/main/java/dev/cel/checker/CelCheckerBuilder.java b/checker/src/main/java/dev/cel/checker/CelCheckerBuilder.java index e19cf5b70..b14782e27 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. @@ -152,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 df8a82f43..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); } @@ -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); @@ -354,6 +359,7 @@ public CelCheckerBuilder addFileTypes(FileDescriptorSet fileDescriptorSet) { } @Override + @Deprecated public CelCheckerBuilder setStandardEnvironmentEnabled(boolean value) { this.standardEnvironmentEnabled = value; return this; @@ -421,11 +427,6 @@ CelStandardDeclarations standardDeclarations() { return this.standardDeclarations; } - @VisibleForTesting - CelOptions options() { - return this.celOptions; - } - @VisibleForTesting CelTypeProvider celTypeProvider() { return this.celTypeProvider; @@ -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 12ad47c62..bd63c4279 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; @@ -31,6 +33,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. @@ -48,11 +51,15 @@ 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; /** Enumeration of Standard Functions. */ - public enum StandardFunction { + public enum StandardFunction implements CelFunctionDecl.Declarer { // Deprecated - use {@link #IN} OLD_IN( true, @@ -598,6 +605,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) { @@ -1474,6 +1488,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() {} @@ -1484,6 +1508,7 @@ private CelFunctionDecl withOverloads(Iterable overloads) { return newCelFunctionDecl(functionName, ImmutableSet.copyOf(overloads)); } + @Override public CelFunctionDecl functionDecl() { return celFunctionDecl; } @@ -1559,8 +1584,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/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/BUILD.bazel b/checker/src/test/java/dev/cel/checker/BUILD.bazel index 1821a5d85..22b70210d 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", @@ -11,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", @@ -42,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/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/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/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/java/dev/cel/checker/TypesTest.java b/checker/src/test/java/dev/cel/checker/TypesTest.java index 960ebec3f..786e50668 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,373 @@ 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 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"); + 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 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/codelab/README.md b/codelab/README.md index f7d248b13..00d7f729e 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. @@ -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 = diff --git a/common/BUILD.bazel b/common/BUILD.bazel index 4e0d7485c..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( @@ -22,6 +80,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"], @@ -69,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 276db0322..9b7573a7c 100644 --- a/common/ast/BUILD.bazel +++ b/common/ast/BUILD.bazel @@ -11,6 +11,18 @@ 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 = "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"], @@ -41,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/internal/BUILD.bazel b/common/internal/BUILD.bazel index 0a07e0d63..7c33e56b9 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"], @@ -152,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/navigation/BUILD.bazel b/common/navigation/BUILD.bazel index 1dba25b8e..8da2514b8 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,7 +16,22 @@ 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"], ) + +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/BUILD.bazel b/common/src/main/java/dev/cel/common/BUILD.bazel index 38548744c..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", @@ -365,6 +479,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/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(); } 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/common/src/main/java/dev/cel/common/CelOptions.java b/common/src/main/java/dev/cel/common/CelOptions.java index 9cf9a9caa..c4b868bf2 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. @@ -61,6 +60,8 @@ public enum ProtoUnsetFieldOptions { public abstract int maxParseRecursionDepth(); + public abstract int maxParseExpressionNodeCount(); + public abstract boolean populateMacroCalls(); public abstract boolean retainRepeatedUnaryOperators(); @@ -71,6 +72,8 @@ public enum ProtoUnsetFieldOptions { public abstract boolean enableQuotedIdentifierSyntax(); + public abstract boolean enablePrattParser(); + // Type-Checker related options public abstract boolean enableCompileTimeOverloadResolution(); @@ -119,6 +122,8 @@ public enum ProtoUnsetFieldOptions { public abstract boolean enableComprehension(); + public abstract boolean enableTimestampOverflowCheck(); + public abstract int maxRegexProgramSize(); public abstract Builder toBuilder(); @@ -135,11 +140,13 @@ public static Builder newBuilder() { .maxExpressionCodePointSize(100_000) .maxParseErrorRecoveryLimit(30) .maxParseRecursionDepth(250) + .maxParseExpressionNodeCount(1_000_000) .populateMacroCalls(false) .retainRepeatedUnaryOperators(false) .retainUnbalancedLogicalExpressions(false) .enableHiddenAccumulatorVar(true) - .enableQuotedIdentifierSyntax(false) + .enableQuotedIdentifierSyntax(true) + .enablePrattParser(false) // Type-Checker options .enableCompileTimeOverloadResolution(false) .enableHomogeneousLiterals(false) @@ -164,6 +171,7 @@ public static Builder newBuilder() { .unwrapWellKnownTypesOnFunctionDispatch(true) .fromProtoUnsetFieldOption(ProtoUnsetFieldOptions.BIND_DEFAULT) .enableComprehension(true) + .enableTimestampOverflowCheck(true) .maxRegexProgramSize(-1); } @@ -177,6 +185,7 @@ public static Builder current() { .enableUnsignedComparisonAndArithmeticIsUnsigned(true) .enableUnsignedLongs(true) .enableRegexPartialMatch(true) + .enableTimestampEpoch(true) .errorOnDuplicateMapKeys(true) .evaluateCanonicalTypesToNativeValues(true) .errorOnIntWrap(true) @@ -223,6 +232,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); @@ -265,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 /** @@ -292,14 +317,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); /** @@ -427,13 +458,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); /** @@ -515,6 +543,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/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/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/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/BUILD.bazel b/common/src/main/java/dev/cel/common/ast/BUILD.bazel index 3fc709a07..14cb75dd9 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,34 @@ 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", + ], +) + +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, @@ -128,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/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/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/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/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/common/src/main/java/dev/cel/common/internal/BUILD.bazel b/common/src/main/java/dev/cel/common/internal/BUILD.bazel index 912b4de4b..58b15b103 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", @@ -397,22 +396,13 @@ java_library( ) java_library( - name = "proto_java_qualified_names", - srcs = ["ProtoJavaQualifiedNames.java"], + name = "reflection_util", + srcs = ["ReflectionUtil.java"], tags = [ ], deps = [ "//common/annotations", "@maven//:com_google_guava_guava", - "@maven//:com_google_protobuf_protobuf_java", - ], -) - -java_library( - name = "reflection_util", - srcs = ["ReflectionUtil.java"], - deps = [ - "//common/annotations", ], ) 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/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/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/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/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/ProtoAdapter.java b/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java index 962a9d2e9..b1b56afe1 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,27 @@ 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"); + } + + if (!isFieldAnyOrJson(fieldDescriptor)) { + return Optional.empty(); + } } if (fieldDescriptor.isMapField()) { Descriptor entryDescriptor = fieldDescriptor.getMessageType(); @@ -221,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())) @@ -231,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()) { @@ -263,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( @@ -280,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(), @@ -291,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, @@ -370,14 +430,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/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/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java b/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java index 36671842d..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; @@ -402,10 +397,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/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/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/main/java/dev/cel/common/navigation/BUILD.bazel b/common/src/main/java/dev/cel/common/navigation/BUILD.bazel index b43f3c289..4ae2908bc 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,55 @@ 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 = "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 = [ @@ -47,6 +97,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/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..c5a19ff9e --- /dev/null +++ b/common/src/main/java/dev/cel/common/navigation/CelNavigableExprUtil.java @@ -0,0 +1,230 @@ +// 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.common.collect.ImmutableSet; +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 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 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}. + * + *

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 findDeclaringComprehension(expr, variableName).isPresent(); + } + + /** + * 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. + */ + public static boolean areVariablesShadowed( + BaseNavigableExpr expr, Collection variableNames) { + checkNotNull(expr); + checkNotNull(variableNames); + for (String varName : variableNames) { + if (findDeclaringComprehension(expr, varName).isPresent()) { + return true; + } + } + 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/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/BUILD.bazel b/common/src/main/java/dev/cel/common/values/BUILD.bazel index 53ffdda3d..433dcd477 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,16 +71,19 @@ cel_android_library( deps = [ "//common/values:values_android", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven_android//:com_google_guava_guava", ], ) 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 +92,70 @@ 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_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//: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, @@ -108,6 +164,7 @@ java_library( deps = [ ":cel_byte_string", ":cel_value", + ":preadapted_list", "//:auto_value", "//common/annotations", "//common/types", @@ -118,6 +175,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, @@ -126,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", @@ -154,7 +244,6 @@ java_library( ], deps = [ ":cel_byte_string", - ":values", "//common/annotations", "//common/internal:proto_time_utils", "//common/internal:well_known_proto", @@ -189,6 +278,7 @@ java_library( ], deps = [ ":base_proto_cel_value_converter", + ":preadapted_list", ":values", "//:auto_value", "//common:options", @@ -201,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", ], ) @@ -244,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", ], ) @@ -271,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", ], @@ -322,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", @@ -337,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/BaseProtoCelValueConverter.java b/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java index b05a21e24..9fc218abe 100644 --- a/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/BaseProtoCelValueConverter.java @@ -98,6 +98,8 @@ protected Object fromWellKnownProto(MessageLiteOrBuilder message, WellKnownProto return UnsignedLong.valueOf(((UInt32Value) message).getValue()); case UINT64_VALUE: return UnsignedLong.fromLongBits(((UInt64Value) message).getValue()); + 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/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/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 ae0b40ef7..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,9 +20,12 @@ 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.Map.Entry; import java.util.Optional; +import java.util.RandomAccess; +import java.util.function.Function; /** * {@code CelValueConverter} handles bidirectional conversion between native Java objects to {@link @@ -37,45 +40,121 @@ 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; } - /** 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 || value instanceof CelPreAdaptedList) { + return value instanceof CelValue ? unwrap((CelValue) value) : value; + } - if (celValue instanceof OptionalValue) { - OptionalValue optionalValue = (OptionalValue) celValue; - if (optionalValue.isZeroValue()) { - return Optional.empty(); + return mapContainer(value, maybeUnwrapFunction); + } + + /** + * 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) { + + // 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(); + } } - return Optional.of(optionalValue.value()); + // Zero allocations if unmodified + return value; } - return celValue.value(); + // Fallback for lists that are unordered + if (value instanceof Collection) { + Collection collection = (Collection) value; + ImmutableList.Builder builder = + ImmutableList.builderWithExpectedSize(collection.size()); + for (Object element : collection) { + builder.add(mapper.apply(element)); + } + return builder.build(); + } + + if (value instanceof Map) { + Map map = (Map) value; + 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 value; + } + + return value; } - /** - * Canonicalizes an inbound {@code value} into a suitable Java object representation for - * evaluation. - */ public Object toRuntimeValue(Object value) { Preconditions.checkNotNull(value); - if (value instanceof CelValue) { + if (value instanceof CelValue || value instanceof CelPreAdaptedList) { 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); } @@ -97,31 +176,28 @@ protected Object normalizePrimitive(Object value) { return value; } - private ImmutableList toListValue(Collection iterable) { - Preconditions.checkNotNull(iterable); - - ImmutableList.Builder listBuilder = - ImmutableList.builderWithExpectedSize(iterable.size()); - for (Object entry : iterable) { - listBuilder.add(toRuntimeValue(entry)); - } + /** Adapts a {@link CelValue} to a plain old Java Object. */ + private Object unwrap(CelValue celValue) { + Preconditions.checkNotNull(celValue); - return listBuilder.build(); - } + if (celValue instanceof OptionalValue) { + OptionalValue optionalValue = (OptionalValue) celValue; + if (optionalValue.isZeroValue()) { + return Optional.empty(); + } - private ImmutableMap toMapValue(Map map) { - Preconditions.checkNotNull(map); + return Optional.of(maybeUnwrap(optionalValue.value())); + } - 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); + if (celValue instanceof ErrorValue) { + return celValue; } - return mapBuilder.buildOrThrow(); + return celValue.value(); } - protected CelValueConverter() {} + protected CelValueConverter() { + this.maybeUnwrapFunction = this::maybeUnwrap; + this.toRuntimeValueFunction = this::toRuntimeValue; + } } 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/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/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/main/java/dev/cel/common/values/ProtoCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java index c7b829e13..a4280e1ea 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java @@ -67,10 +67,13 @@ 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); + case FIELD_MASK: + return ProtoMessageValue.create( + (Message) message, celDescriptorPool, this, celOptions.enableJsonFieldNames()); default: return super.fromWellKnownProto(message, wellKnownProto); } @@ -154,11 +157,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); } @@ -167,6 +172,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); } 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/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 e402bb429..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(); @@ -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/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/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/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/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/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/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/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/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/common/src/test/java/dev/cel/common/navigation/BUILD.bazel b/common/src/test/java/dev/cel/common/navigation/BUILD.bazel index 74ec3e080..0a29dfe8a 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", @@ -19,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..9391881fa --- /dev/null +++ b/common/src/test/java/dev/cel/common/navigation/CelNavigableExprUtilTest.java @@ -0,0 +1,617 @@ +// 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 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(); + 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(); + } + + @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(); + } + + @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"); + } +} 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/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/common/src/test/java/dev/cel/common/values/BUILD.bazel b/common/src/test/java/dev/cel/common/values/BUILD.bazel index ab7eae8dd..76c761567 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", @@ -24,6 +26,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", @@ -32,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/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/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/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/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/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/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 { 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..b5c29129b 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoMessageValueTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoMessageValueTest.java @@ -23,6 +23,7 @@ import com.google.protobuf.Any; import com.google.protobuf.ByteString; import com.google.protobuf.DynamicMessage; +import com.google.protobuf.FieldMask; import com.google.protobuf.FloatValue; import com.google.protobuf.Int32Value; import com.google.protobuf.Int64Value; @@ -303,6 +304,46 @@ 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(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")); + } + + @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), 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..978222869 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(); } @@ -112,44 +128,60 @@ 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 = - 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 +195,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 +210,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 +229,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 +270,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 +281,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/common/values/BUILD.bazel b/common/values/BUILD.bazel index f1fa107b6..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"], @@ -47,6 +59,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/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/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/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel index ab7468e54..4abc705c3 100644 --- a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -20,15 +20,20 @@ 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", "//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 +62,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", @@ -72,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", @@ -100,42 +108,22 @@ _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", - # 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/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", # Skip until fixed. "fields/qualified_identifier_resolution/map_value_repeat_key_heterogeneous", - # TODO: Add strings.format and strings.quote. - "string_ext/quote", + # TODO: Add strings.format.quote. "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", @@ -159,17 +147,42 @@ _TESTS_TO_SKIP = [ "type_deductions/legacy_nullable_types/null_assignable_to_timestamp_parameter_candidate", ] +_TESTS_TO_SKIP_PLANNER = [ + # TODO: Add strings.format. + "string_ext/format", + "string_ext/format_errors", + + # TODO: This is actually a user experience degradation. + # Not worth fixing until we see a concrete need. + "basic/functions/unbound_is_runtime_error", + + # 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 +190,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..82b4cf812 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,25 +27,42 @@ 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.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; 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 java.util.Optional; import org.junit.runners.model.Statement; // Qualifying proto2/proto3 TestAllTypes makes it less clear. @@ -56,7 +71,6 @@ public final class ConformanceTest extends Statement { private static final CelOptions OPTIONS = CelOptions.current() - .enableTimestampEpoch(true) .enableHeterogeneousNumericComparisons(true) .enableProtoDifferencerEquality(true) .enableOptionalSyntax(true) @@ -72,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( @@ -83,6 +98,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) @@ -105,7 +135,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) @@ -118,15 +148,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(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()); + + if (usePlanner) { + builder.setContainer(CelContainer.ofName(test.getContainer())); + } + + return builder.build(); + } private static ImmutableMap getBindings(SimpleTest test) throws Exception { ImmutableMap.Builder bindings = @@ -140,7 +180,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())); @@ -157,13 +198,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() { @@ -177,9 +220,11 @@ public boolean shouldSkip() { @Override public void evaluate() throws Throwable { CelValidationResult response = getParser(test).parse(test.getExpr(), test.getName()); - assertThat(response.hasError()).isFalse(); - response = getChecker(test).check(response.getAst()); - assertThat(response.hasError()).isFalse(); + assertThat(response.getErrors()).isEmpty(); + if (!test.getDisableCheck()) { + response = getChecker(test).check(response.getAst()); + } + assertThat(response.getErrors()).isEmpty(); Type resultType = CelProtoTypes.celTypeToType(response.getAst().getResultType()); if (test.getCheckOnly()) { @@ -188,10 +233,16 @@ 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); 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; @@ -203,7 +254,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: @@ -216,7 +267,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; @@ -225,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/conformance/src/test/java/dev/cel/conformance/ConformanceTestRunner.java b/conformance/src/test/java/dev/cel/conformance/ConformanceTestRunner.java index 89598ed3e..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; @@ -43,20 +42,23 @@ public final class ConformanceTestRunner extends ParentRunner { private final ImmutableSortedMap testFiles; private final ImmutableList testsToSkip; + private final boolean usePlanner; private static ImmutableSortedMap loadTestFiles() { List testPaths = 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); @@ -75,6 +77,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 +102,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/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..e4d80eccf --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel @@ -0,0 +1,36 @@ +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", + "//:java_truth", + "//bundle:cel", + "//policy:parser_factory", + "//policy:validation_exception", + "//policy/testing:k8s_test_tag_handler", + "//runtime:function_binding", + "//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", + "@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", + ], +) + +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/PolicyConformanceTest.java b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java new file mode 100644 index 000000000..5727eb5ee --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.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.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; +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 java.util.Locale; +import java.util.Map; +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() + .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.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; + 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) + .addFileTypes( + 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"); + + if (Files.exists(yamlConfigPath)) { + contextBuilder.setConfigFile(yamlConfigPath.toString()); + } else if (Files.exists(textprotoConfigPath)) { + contextBuilder.setConfigFile(textprotoConfigPath.toString()); + } + + 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 new file mode 100644 index 000000000..62812b124 --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTestRunner.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.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 String POLICY_YAML_FILE_NAME = "policy.yaml"; + 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(); + } + File[] topLevelDirs = dir.listFiles(File::isDirectory); + if (topLevelDirs == null) { + return ImmutableList.of(); + } + + 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; + + 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..b53d982bb --- /dev/null +++ b/conformance/src/test/java/dev/cel/conformance/policy/cel_policy_conformance_test.bzl @@ -0,0 +1,54 @@ +# 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) + + # 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, + 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 + ) 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/extensions/BUILD.bazel b/extensions/BUILD.bazel index c6a029106..f9c2aee45 100644 --- a/extensions/BUILD.bazel +++ b/extensions/BUILD.bazel @@ -56,3 +56,14 @@ 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"], +) + +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 ed2d19d6f..554aadbde 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 = [ @@ -34,6 +36,7 @@ java_library( ":encoders", ":lists", ":math", + ":native", ":optional_library", ":protos", ":regex", @@ -42,6 +45,7 @@ java_library( ":strings", "//common:options", "//extensions:extension_library", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", ], ) @@ -121,7 +125,6 @@ java_library( ":extension_library", "//checker:checker_builder", "//common:compiler_common", - "//common:options", "//common/ast", "//common/exceptions:numeric_overflow", "//common/internal:comparison_functions", @@ -142,6 +145,8 @@ java_library( deps = [ "//common:compiler_common", "//common/ast", + "//common/ast:cel_block", + "//common/types", "//compiler:compiler_builder", "//extensions:extension_library", "//parser:macro", @@ -184,6 +189,7 @@ java_library( "//common/types", "//common/values", "//common/values:cel_byte_string", + "//common/values:cel_value", "//compiler:compiler_builder", "//extensions:extension_library", "//parser:macro", @@ -268,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", @@ -306,6 +314,7 @@ java_library( "//common:options", "//common/ast", "//common/types", + "//common/values:mutable_map_value", "//compiler:compiler_builder", "//extensions:extension_library", "//parser:macro", @@ -316,3 +325,27 @@ 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/exceptions:invalid_argument", + "//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/CelBindingsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java index 5eb2c2e8c..9fea7f481 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java @@ -22,7 +22,12 @@ 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.CelBlock; 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; @@ -55,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; @@ -62,7 +76,8 @@ public int version() { @Override public ImmutableSet functions() { - return ImmutableSet.of(); + // TODO: Add bindings for block once decorator support is available. + return ImmutableSet.of(CEL_BLOCK_FUNCTION_DECL); } @Override diff --git a/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java index 23663f02e..7391eb16d 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; @@ -47,30 +48,38 @@ 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); - enum Function { + 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 { MAP_INSERT( CelFunctionDecl.newFunctionDeclaration( MAP_INSERT_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; + public CelFunctionDecl functionDecl() { + return functionDecl; + } + String getFunction() { return functionDecl.name(); } @@ -80,20 +89,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; @@ -102,12 +116,12 @@ static CelExtensionLibrary library() { private final ImmutableSet functions; CelComprehensionsExtensions() { - this.functions = ImmutableSet.copyOf(Function.values()); + this.functions = ImmutableSet.of(Function.MAP_INSERT); } @Override public void setCheckerOptions(CelCheckerBuilder checkerBuilder) { - functions.forEach(function -> checkerBuilder.addFunctionDeclarations(function.functionDecl)); + functions.forEach(function -> checkerBuilder.addFunctionDeclarations(function.functionDecl())); } @Override @@ -118,29 +132,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 @@ -182,38 +185,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()) { - throw new IllegalArgumentException( - String.format("insert failed: key '%s' already exists", entry.getKey())); - } else { - resultBuilder.put(entry.getKey(), entry.getValue()); - } + for (Object key : mapToMerge.keySet()) { + checkArgument( + !equality.findInMap(targetMap, key).isPresent(), + "insert failed: key '%s' already exists", + key); + } + + if (targetMap instanceof MutableMapValue) { + MutableMapValue wrapper = (MutableMapValue) targetMap; + wrapper.putAll(mapToMerge); + return wrapper; } - return resultBuilder.putAll(targetMap).buildOrThrow(); + + 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()) { - 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; + 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( @@ -481,9 +492,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); @@ -492,6 +502,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/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/main/java/dev/cel/extensions/CelExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java index 2d14ed118..446fa26e7 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java @@ -15,12 +15,15 @@ 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; +import java.util.stream.Stream; /** * Collections of CEL Extensions. @@ -121,12 +124,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 +134,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 +150,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 +167,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); } /** @@ -319,6 +351,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. * @@ -328,18 +372,25 @@ 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()) + 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()); } @@ -354,7 +405,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/CelListsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java index a91edd822..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 { @@ -128,15 +135,53 @@ 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", - 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"))))); + 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; @@ -147,7 +192,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 +288,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,24 +398,32 @@ 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) { + 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 { - 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); } + 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"); @@ -398,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); @@ -415,59 +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, CelOptions options) { - List[] array = keyValuePairs.toArray(new List[0]); - Arrays.sort( - array, - new CelObjectByKeyComparator( - new CelObjectComparator(options.enableHeterogeneousNumericComparisons()))); - 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/main/java/dev/cel/extensions/CelMathExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java index 57c8c1378..63108aa0c 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,26 +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; - this.functionBindingsULongSigned = functionBindingsULongSigned; - this.functionBindingsULongUnsigned = functionBindingsULongUnsigned; + this.functionBindings = bindings; } } @@ -674,10 +678,8 @@ private static final class Library implements CelExtensionLibrarybuilder() .addAll(version1.functions) .add(Function.SQRT) - .build(), - enableUnsignedLongs); + .build()); } @Override @@ -724,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); } @@ -778,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)); + } }); } @@ -879,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/main/java/dev/cel/extensions/CelNativeTypesExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java new file mode 100644 index 000000000..f150a8437 --- /dev/null +++ b/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java @@ -0,0 +1,1118 @@ +// 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.exceptions.CelInvalidArgumentException; +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.Array; +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()); + + // 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) + .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.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)) { + 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(); + queue.addAll(TypeReferenceCollector.collect(genericPropType)); + compiledGetter = compileGetter(getter); + } else if (field != null) { + propType = field.getType(); + genericPropType = field.getGenericType(); + queue.addAll(TypeReferenceCollector.collect(genericPropType)); + 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, 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}). + */ + 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 (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; + } + + if (Map.class.isAssignableFrom(rawType)) { + discover(ReflectionUtil.resolveGenericParameter(token, Map.class, 0)); + discover(ReflectionUtil.resolveGenericParameter(token, Map.class, 1)); + return; + } + + if (rawType == Optional.class) { + discover(ReflectionUtil.resolveGenericParameter(token, Optional.class, 0)); + return; + } + + // Custom types are non-builtin, public classes + if (!JAVA_TO_DEFAULT_VALUE_MAP.containsKey(rawType) + && Modifier.isPublic(rawType.getModifiers())) { + collectedTypes.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 (Enum.class.isAssignableFrom(method.getDeclaringClass()) + && ENUM_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(); + } + if (targetType.isArray()) { + return Array.newInstance(targetType.getComponentType(), 0); + } + + 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)); + } + + if (clazz.isArray() && clazz != byte[].class) { + return convertArrayToList(value); + } + + 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 (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) { + return convertMapToNative((Map) value, targetType, genericType); + } + + return downcastPrimitives(value, targetType); + } + + // Safe reflection collection cast. + @SuppressWarnings("unchecked") + 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); + + 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 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); + 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 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) { + 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 baa8acb59..85ee9c756 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; @@ -50,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; @@ -93,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( @@ -207,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); @@ -240,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; @@ -342,54 +400,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))); } } @@ -397,9 +470,6 @@ private static ImmutableList elideOptionalCollection(Collection 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( @@ -487,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/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..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; @@ -137,6 +135,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 +173,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", @@ -238,7 +257,8 @@ String getFunction() { Function(CelFunctionDecl functionDecl, CelFunctionBinding... functionBindings) { this.functionDecl = functionDecl; - this.functionBindings = ImmutableSet.copyOf(functionBindings); + this.functionBindings = + CelFunctionBinding.fromOverloads(functionDecl.name(), functionBindings); } } @@ -448,6 +468,64 @@ 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 (isMalformedUtf16(s, i)) { + 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 boolean isMalformedUtf16(String s, int index) { + char currentChar = s.charAt(index); + 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); } @@ -503,14 +581,18 @@ private static String replace(String text, String searchString, String replaceme return sb.append(textCpa.slice(start, textCpa.length())).toString(); } - private static List split(String str, String separator) { + private static String reverse(String s) { + return new StringBuilder(s).reverse().toString(); + } + + 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 { @@ -525,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) { @@ -546,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); } /** @@ -559,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; @@ -572,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 10c5217e8..e6ee73aba 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,9 +506,23 @@ 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 +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. @@ -1042,4 +1069,60 @@ 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 + +## Native Types + +The `nativeTypes` extension allows registering native Java types (POJOs) to be +used in CEL expressions. + +All POJO classes are exposed to CEL using their fully qualified canonical name. +For example, if you have a class `com.example.Account`: + +```java +package com.example; +public class Account { + public int id; +} +``` + +The type `com.example.Account` would be exported to CEL using its full name. If +you set the container to `com.example` on the compiler, you can use it simply +as `Account`: `Account{id: 1234}` would create a new `Account` instance with the +`id` field populated. + +Properties are discovered by reflectively scanning public fields and public +getter methods of public classes. For field selection (reading) and object +creation (writing), resolution happens in the following order of precedence: + +1. Standard JavaBeans getter (e.g., `getFoo()`) or setter (e.g., `setFoo(...)`) +2. Boolean getter (e.g., `isFoo()`) for boolean properties +3. Prefix-less getter (e.g., `foo()`) matching a declared field name +4. Public field directly (e.g., `public String foo`) + +### Type Mapping + +The type-mapping between Java and CEL is as follows: + +| Java type | CEL type | +| :--- | :--- | +| `boolean`, `Boolean` | `bool` | +| `byte[]` | `bytes` | +| `float`, `Float`, `double`, `Double` | `double` | +| `int`, `Integer`, `long`, `Long` | `int` | +| `com.google.common.primitives.UnsignedLong` | `uint` | +| `String` | `string` | +| `java.time.Duration` | `duration` | +| `java.time.Instant` | `timestamp` | +| `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 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). +* Final fields are supported only in a **read-only** capacity; they cannot be populated when instantiating new struct values. 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/BUILD.bazel b/extensions/src/test/java/dev/cel/extensions/BUILD.bazel index d5155f662..f7b996610 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", @@ -11,21 +13,27 @@ java_library( "//:java_truth", "//bundle:cel", "//common:cel_ast", + "//common:cel_exception", "//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", + "//common/exceptions:invalid_argument", "//common/types", "//common/types:type_providers", "//common/values", "//common/values:cel_byte_string", + "//common/values:cel_value_provider", "//compiler", "//compiler:compiler_builder", "//extensions", "//extensions:extension_library", "//extensions:lite_extensions", "//extensions:math", + "//extensions:native", "//extensions:optional_library", "//extensions:sets", "//extensions:sets_function", @@ -34,9 +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/CelBindingsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java index bc98c9816..00fcad473 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelBindingsExtensionsTest.java @@ -22,21 +22,19 @@ import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; -import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.bundle.Cel; 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 java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -44,18 +42,18 @@ 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(); +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 public void library() { @@ -63,7 +61,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"); } @@ -92,9 +91,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(); } @@ -102,9 +99,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", @@ -115,18 +114,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(); } @@ -134,7 +131,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"); } @@ -142,70 +139,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); @@ -214,32 +217,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); @@ -248,32 +251,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); @@ -282,38 +284,39 @@ 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); } + + } diff --git a/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java index 34696b688..42dc3e07d 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelComprehensionsExtensionsTest.java @@ -15,57 +15,61 @@ 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.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.exceptions.CelAttributeNotFoundException; import dev.cel.common.exceptions.CelDivideByZeroException; 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 org.junit.Assume; 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() + .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(); + @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(); @@ -101,11 +105,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 +127,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 +152,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 +174,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 +198,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 @@ -229,6 +213,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," @@ -238,24 +223,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 +258,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 +301,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 { - CelValidationException e = - assertThrows(CelValidationException.class, () -> CEL_COMPILER.compile(expr).getAst()); + Assume.assumeFalse(isParseOnly); + CelValidationResult result = cel.compile(expr); + CelValidationException e = assertThrows(CelValidationException.class, () -> result.getAst()); assertThat(e).hasMessageThat().contains(err); } @@ -339,34 +323,47 @@ 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"); } + + @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."); + } + + } diff --git a/extensions/src/test/java/dev/cel/extensions/CelEncoderExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelEncoderExtensionsTest.java index 7eed3dd5a..afeaa9105 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelEncoderExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelEncoderExtensionsTest.java @@ -20,35 +20,32 @@ import com.google.common.collect.ImmutableMap; import com.google.testing.junit.testparameterinjector.TestParameterInjector; -import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.bundle.Cel; 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 org.junit.Assume; 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().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(); + + @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 public void library() { @@ -63,22 +60,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 +75,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 +83,42 @@ 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"); } + + } 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..3a509b003 --- /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 CelRuntimeFlavor runtimeFlavor; + @TestParameter 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/CelExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java index 61922f70f..279ad7013 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", @@ -184,10 +185,16 @@ public void getAllFunctionNames() { "distinct", "reverse", "sort", - "lists.@sortByAssociatedKeys", + "@sortByAssociatedKeys", "regex.replace", "regex.extract", "regex.extractAll", + "value", + "hasValue", + "optional.none", + "optional.of", + "optional.unwrap", + "optional.ofNonZeroValue", "cel.@mapInsert"); } } diff --git a/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java index c4739b18b..1f893c7ba 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java @@ -22,38 +22,37 @@ 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.CelOptions; 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; 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; @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(); +public class CelListsExtensionsTest extends CelExtensionTestBase { + + @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 public void functionList_byVersion() { @@ -69,7 +68,7 @@ public void functionList_byVersion() { "distinct", "reverse", "sort", - "lists.@sortByAssociatedKeys"); + "@sortByAssociatedKeys"); } @Test @@ -89,10 +88,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 +105,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 +122,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 +138,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 +146,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 (runtimeFlavor.equals(CelRuntimeFlavor.PLANNER)) { + 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 +165,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 +177,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 +205,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 +226,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 @@ -237,10 +238,18 @@ 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 = 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 +257,23 @@ 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'}") + @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, - () -> CEL.createProgram(CEL.compile(expression).getAst()).eval())) + assertThat(assertThrows(CelEvaluationException.class, () -> eval(cel, expression))) .hasCauseThat() .hasMessageThat() .contains(expectedError); @@ -288,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\"}," @@ -295,10 +307,15 @@ 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 = CEL.createProgram(CEL.compile(expression).getAst()).eval(); + Object result = eval(cel, expression); - assertThat(result).isEqualTo(expectedResult(expected)); + assertThat(result).isEqualTo(eval(cel, expected)); } @Test @@ -308,36 +325,35 @@ 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'}") - public void sortBy_throws_validationException(String expression, String expectedError) - throws Exception { - assertThat( - assertThrows( - CelValidationException.class, - () -> CEL.createProgram(CEL.compile(expression).getAst()).eval())) - .hasMessageThat() - .contains(expectedError); - } - - @Test @TestParameters( - "{expression: '[[1, 2], [\"a\", \"b\"]].sortBy(e, e[0])', " - + "expectedError: 'List elements must have the same type'}") + "{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: 'List elements must be comparable'}") - public void sortBy_throws_evaluationException(String expression, String expectedError) + + "expectedError: 'found no matching overload for ''@sortByAssociatedKeys'''}") + public void sortBy_throws_validationException(String expression, String expectedError) throws Exception { - assertThat( - assertThrows( - CelEvaluationException.class, - () -> CEL.createProgram(CEL.compile(expression).getAst()).eval())) - .hasCauseThat() + CelValidationResult result = cel.compile(expression); + assertThat(assertThrows(CelValidationException.class, () -> result.getAst())) .hasMessageThat() .contains(expectedError); } - private static Object expectedResult(String expression) - throws CelEvaluationException, CelValidationException { - return CEL.createProgram(CEL.compile(expression).getAst()).eval(); + @Test + 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"); } } diff --git a/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelMathExtensionsTest.java index bcdfb0a21..5b57f1fb2 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 private CelRuntimeFlavor runtimeFlavor; + @TestParameter private 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'"); } @@ -802,13 +735,20 @@ 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}") 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 +760,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 +772,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 +786,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 +796,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 +811,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,11 +823,11 @@ 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() - .contains("ERROR: :1:10: For input string: \"-9223372036854775809\""); + .contains("ERROR: :1:10: invalid int literal: -9223372036854775809"); } @Test @@ -896,9 +836,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 +854,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 +866,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 +878,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 +890,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 +903,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,23 +913,20 @@ 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() - .contains("ERROR: :1:33: For input string: \"9223372036854775809\""); + .contains("ERROR: :1:33: invalid int literal: 9223372036854775809"); } @Test @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 +935,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 +948,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 +957,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 +969,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 +982,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 +992,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 +1004,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 +1017,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 +1027,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 +1040,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 +1051,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 +1067,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 +1080,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 +1089,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 +1104,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()); + } } diff --git a/extensions/src/test/java/dev/cel/extensions/CelNativeTypesExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelNativeTypesExtensionsTest.java new file mode 100644 index 000000000..0b378f0d7 --- /dev/null +++ b/extensions/src/test/java/dev/cel/extensions/CelNativeTypesExtensionsTest.java @@ -0,0 +1,1473 @@ +// 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.truth.Truth.assertThat; +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.collect.ImmutableSet; +import com.google.common.primitives.UnsignedLong; +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.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; +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.CelValueProvider; +import dev.cel.common.values.StructValue; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class CelNativeTypesExtensionsTest { + + @TestParameter boolean isParseOnly; + + private static final CelNativeTypesExtensions NATIVE_TYPE_EXTENSIONS = + CelExtensions.nativeTypes( + TestAllTypesPublicFieldsPojo.class, + TestPrivateConstructorPojo.class, + ComprehensiveTestAllTypes.class, + TestGetterSetterPojo.class, + TestMissingNoArgConstructorPojo.class, + TestPrivateFieldPojo.class, + TestDeepConversionPojo.class, + TestPrecedencePojo.class, + TestPrefixLessGetterPojo.class, + TestChildPojo.class, + TestPackagePrivatePojo.class, + TestPackagePrivateWithGetterPojo.class, + TestWildcardPojo.class, + ComprehensiveTestNestedType.class, + TestNestedSliceType.class, + TestMapVal.class, + TestCustomCollectionPojo.class, + TestNestedGenericsPojo.class, + TestNestedSimplePojo.class, + TestGetterFieldTypeMismatchPojo.class, + TestAbstractPojo.class, + TestURLPojo.class, + PojoWithEnum.class, + TestArrayPojo.class); + + private static final Cel CEL = + CelFactory.plannerCelBuilder() + .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest")) + .addCompilerLibraries(NATIVE_TYPE_EXTENSIONS) + .addRuntimeLibraries(NATIVE_TYPE_EXTENSIONS) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .build(); + + private Object eval(String expr) throws Exception { + return eval(expr, ImmutableMap.of()); + } + + private Object eval(String expr, Map 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 { + TestPrivateConstructorPojo result = + (TestPrivateConstructorPojo) eval("TestPrivateConstructorPojo{value:" + " 'hello'}"); + + assertThat(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 { + 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); + } + + @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_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( + 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 + 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 valueAst = + celCompiler + .compile( + "dev.cel.extensions.CelNativeTypesExtensionsTest.TestPrefixLessGetterPojo{}.value") + .getAst(); + CelAbstractSyntaxTree nameAst = + celCompiler + .compile( + "dev.cel.extensions.CelNativeTypesExtensionsTest.TestPrefixLessGetterPojo{}.name") + .getAst(); + CelRuntime.Program valueProgram = celRuntime.createProgram(valueAst); + CelRuntime.Program nameProgram = celRuntime.createProgram(nameAst); + + Object valueResult = valueProgram.eval(); + Object nameResult = nameProgram.eval(); + + assertThat(valueResult).isEqualTo("hello"); + assertThat(nameResult).isEqualTo("my_name"); + } + + @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); + + TestAllTypesPublicFieldsPojo pojo = (TestAllTypesPublicFieldsPojo) program.eval(); + 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(""); + 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 = + 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 String[] arrayVal; + 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"; + private String name = "my_name"; + + public String value() { + return value; + } + + public String name() { + return name; + } + } + + 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[] strings; + public int[] ints; + public TestNestedType[] nesteds; + public int[][] matrix; + public TestNestedType[][] nestedMatrix; + public byte[][] byteArrays; + } + + 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"; + } + } + + 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(); + } + +} diff --git a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java index dd94333c3..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; @@ -34,6 +36,9 @@ 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.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; @@ -43,14 +48,18 @@ 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.expr.conformance.proto3.NestedTestAllTypes; import dev.cel.expr.conformance.proto3.TestAllTypes; 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.CelUnknownSet; +import dev.cel.runtime.PartialVars; import java.time.Duration; import java.time.Instant; import java.util.List; @@ -63,6 +72,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,16 +109,26 @@ private enum ConstantTestCases { } } - private static CelBuilder newCelBuilder() { + private CelBuilder newCelBuilder() { return newCelBuilder(Integer.MAX_VALUE); } - private static CelBuilder newCelBuilder(int version) { - return CelFactory.standardCelBuilder() - .setOptions( - CelOptions.current() - .enableTimestampEpoch(true) - .build()) + private CelBuilder newCelBuilder(int version) { + CelBuilder celBuilder; + switch (testMode) { + case PLANNER_PARSE_ONLY: + case PLANNER_CHECKED: + celBuilder = CelFactory.plannerCelBuilder(); + break; + case LEGACY_CHECKED: + celBuilder = CelFactory.standardCelBuilder(); + break; + default: + throw new IllegalArgumentException("Unknown test mode: " + testMode); + } + + return celBuilder + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build()) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .addMessageTypes(TestAllTypes.getDescriptor()) @@ -181,7 +208,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 +225,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 +243,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 +254,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 +266,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 +279,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 +293,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 +305,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 +316,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 +327,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 +339,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 +349,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 +360,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 +373,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 +388,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 +408,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 +428,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 +449,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 +466,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 +490,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 +503,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 +533,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 +552,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 +575,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 +592,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,13 +602,23 @@ 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(); 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 = @@ -591,7 +626,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) @@ -600,6 +635,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 = @@ -607,7 +666,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 +680,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 +692,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 +714,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 +732,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 +743,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 +756,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 +777,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 +794,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 +814,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 +832,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 +857,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 +880,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 +899,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 +917,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) @@ -876,11 +935,13 @@ public void optionalIndex_onMap_returnsOptionalValue() throws Exception { public void optionalIndex_onMapWithUnknownInput_returnsUnknownResult(String source) throws Exception { 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(); + Object result = + cel.createProgram(ast) + .eval(PartialVars.of(CelAttributePattern.fromQualifiedIdentifier("x"))); - assertThat(InterpreterUtil.isUnknown(result)).isTrue(); + assertThat(result).isInstanceOf(CelUnknownSet.class); } @Test @@ -894,7 +955,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 +969,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 +982,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 +996,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 +1011,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) @@ -966,11 +1027,13 @@ public void optionalIndex_onListWithUnknownInput_returnsUnknownResult() throws E .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(); + Object result = + cel.createProgram(ast) + .eval(PartialVars.of(CelAttributePattern.fromQualifiedIdentifier("x"))); - assertThat(InterpreterUtil.isUnknown(result)).isTrue(); + assertThat(result).isInstanceOf(CelUnknownSet.class); } @Test @@ -980,13 +1043,36 @@ 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())); 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(result).isInstanceOf(CelUnknownSet.class); + } + @Test // LHS @TestParameters("{expression: 'optx.or(optional.of(1))'}") @@ -1001,11 +1087,13 @@ public void optionalChainedFunctions_lhsIsUnknown_returnsUnknown(String expressi .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(); + Object result = + cel.createProgram(ast) + .eval(PartialVars.of(CelAttributePattern.fromQualifiedIdentifier("optx"))); - assertThat(InterpreterUtil.isUnknown(result)).isTrue(); + assertThat(result).isInstanceOf(CelUnknownSet.class); } @Test @@ -1021,7 +1109,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 +1121,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 +1144,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 +1172,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 +1198,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 +1216,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 +1226,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 +1248,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 +1277,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 +1285,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 +1306,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 +1317,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 +1342,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 +1359,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 +1381,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 +1403,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 +1428,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 +1450,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 +1466,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 +1483,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 +1509,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 +1524,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 +1541,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 +1553,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 +1568,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))); @@ -1486,6 +1576,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 { @@ -1494,8 +1646,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 +1662,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 +1671,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 +1697,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 +1707,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 +1719,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 +1729,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 +1738,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 +1757,85 @@ 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"); + } + + @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(); + if (testMode.equals(TestMode.PLANNER_PARSE_ONLY)) { + return ast; + } + + return compiler.check(ast).getAst(); + } } diff --git a/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java index 15f6df5be..f46ea5b1a 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelProtoExtensionsTest.java @@ -26,8 +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; import dev.cel.common.CelOptions; @@ -35,8 +33,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 +40,24 @@ 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 org.junit.Assume; 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(); - - private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder() - .addFileTypes(TestAllTypesExtensions.getDescriptor()) - .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 = TestAllTypes.newBuilder() @@ -106,10 +99,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 +118,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 +129,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 +142,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 +190,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 +204,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 +231,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 +244,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 +272,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 +325,10 @@ 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); } + + } diff --git a/extensions/src/test/java/dev/cel/extensions/CelRegexExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelRegexExtensionsTest.java index 8a1bef014..97d0cc90c 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelRegexExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelRegexExtensionsTest.java @@ -20,25 +20,26 @@ import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import com.google.testing.junit.testparameterinjector.TestParameters; -import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.bundle.Cel; 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 java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) -public final class CelRegexExtensionsTest { +public final class CelRegexExtensionsTest extends CelExtensionTestBase { + + @Override + protected Cel newCelEnv() { + return runtimeFlavor + .builder() + .addCompilerLibraries(CelExtensions.regex()) + .addRuntimeLibraries(CelExtensions.regex()) + .build(); + } - private static final CelCompiler COMPILER = - CelCompilerFactory.standardCelCompilerBuilder().addLibraries(CelExtensions.regex()).build(); - private static final CelRuntime RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder().addLibraries(CelExtensions.regex()).build(); @Test public void library() { @@ -80,11 +81,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 +90,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 +111,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 +120,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 +130,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 +143,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 +156,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 +180,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 +192,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 +204,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 +238,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 +254,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 +263,6 @@ public void extractAll_multipleCaptureGroups_throwsException(String target, Stri .hasMessageThat() .contains("Regular expression has more than one capturing group:"); } + + } diff --git a/extensions/src/test/java/dev/cel/extensions/CelSetsExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelSetsExtensionsTest.java index 1aac5a023..091d456f5 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelSetsExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelSetsExtensionsTest.java @@ -19,8 +19,10 @@ 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.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; @@ -30,47 +32,46 @@ 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 org.junit.Assume; 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(); +public final class CelSetsExtensionsTest extends CelExtensionTestBase { + private static final CelOptions CEL_OPTIONS = + CelOptions.current().enableHeterogeneousNumericComparisons(true).build(); + + @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 public void library() { @@ -87,22 +88,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 +112,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 +121,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 +140,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 +157,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 +170,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 +178,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 +196,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 +213,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 +228,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 +243,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 +257,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 +282,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 +300,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 +325,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 +343,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 +372,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])"); assertThat(evaluatedResult).isEqualTo(true); } @@ -471,15 +395,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])"); assertThat(evaluatedResult).isEqualTo(true); } @@ -488,44 +411,55 @@ 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])"); 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(); - - assertThrows(CelEvaluationException.class, () -> celRuntime.createProgram(ast).eval()); + 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()); + } } + + } diff --git a/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java b/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java index 6ea9b702c..4b242ddcd 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelStringExtensionsTest.java @@ -18,43 +18,45 @@ 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 java.util.List; +import org.junit.Assume; 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(); +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 public void library() { @@ -70,7 +72,9 @@ public void library() { "lastIndexOf", "lowerAscii", "replace", + "reverse", "split", + "strings.quote", "substring", "trim", "upperAscii"); @@ -90,10 +94,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); } @@ -106,10 +108,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); } @@ -125,10 +124,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); } @@ -159,10 +155,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); } @@ -180,34 +174,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'"); } @@ -293,11 +283,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); } @@ -349,11 +338,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); } @@ -366,35 +354,33 @@ 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); - + ImmutableMap variables = ImmutableMap.of("limit", 2147483648L); // INT_MAX + 1 CelEvaluationException exception = assertThrows( - CelEvaluationException.class, - () -> program.eval(ImmutableMap.of("limit", 2147483648L))); // INT_MAX + 1 + CelEvaluationException.class, () -> eval("'test'.split('', limit)", variables)); assertThat(exception) .hasMessageThat() @@ -414,11 +400,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); } @@ -442,11 +427,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); } @@ -456,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 { - CelAbstractSyntaxTree ast = COMPILER.compile("s.substring(beginIndex)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - + ImmutableMap variables = ImmutableMap.of("s", string, "beginIndex", beginIndex); CelEvaluationException exception = assertThrows( - CelEvaluationException.class, - () -> program.eval(ImmutableMap.of("s", string, "beginIndex", beginIndex))); + CelEvaluationException.class, () -> eval("s.substring(beginIndex)", variables)); String exceptionMessage = String.format( @@ -480,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 { - CelAbstractSyntaxTree ast = COMPILER.compile("s.substring(beginIndex)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - + ImmutableMap variables = ImmutableMap.of("s", string, "beginIndex", beginIndex); CelEvaluationException exception = assertThrows( - CelEvaluationException.class, - () -> program.eval(ImmutableMap.of("s", string, "beginIndex", beginIndex))); + CelEvaluationException.class, () -> eval("s.substring(beginIndex)", variables)); String exceptionMessage = String.format( @@ -503,15 +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 { - CelAbstractSyntaxTree ast = COMPILER.compile("s.substring(beginIndex, endIndex)").getAst(); - CelRuntime.Program program = RUNTIME.createProgram(ast); - + ImmutableMap variables = + ImmutableMap.of("s", string, "beginIndex", beginIndex, "endIndex", endIndex); CelEvaluationException exception = assertThrows( CelEvaluationException.class, - () -> - program.eval( - 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); @@ -520,13 +495,11 @@ 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); - + ImmutableMap variables = + ImmutableMap.of("beginIndex", 2147483648L); // INT_MAX + 1 CelEvaluationException exception = assertThrows( - CelEvaluationException.class, - () -> program.eval(ImmutableMap.of("beginIndex", 2147483648L))); // INT_MAX + 1 + CelEvaluationException.class, () -> eval("'abcd'.substring(beginIndex)", variables)); assertThat(exception) .hasMessageThat() @@ -538,13 +511,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() @@ -561,10 +534,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); } @@ -586,10 +556,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); } @@ -600,26 +567,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() @@ -648,10 +610,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); } @@ -680,10 +640,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); } @@ -695,13 +653,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"); } @@ -726,11 +681,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); } @@ -777,11 +731,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); } @@ -795,14 +748,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"); @@ -810,13 +761,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() @@ -833,10 +784,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); } @@ -845,10 +793,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); } @@ -872,11 +817,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); } @@ -891,20 +832,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'"); } @@ -933,11 +871,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); } @@ -967,11 +904,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); } @@ -985,10 +921,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); } @@ -1020,11 +954,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); } @@ -1095,11 +1028,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); } @@ -1113,14 +1045,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"); @@ -1128,13 +1058,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() @@ -1161,13 +1091,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); } @@ -1186,13 +1111,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); } @@ -1271,15 +1191,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); } @@ -1332,28 +1247,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() @@ -1404,10 +1314,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); } @@ -1420,10 +1327,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); } @@ -1439,34 +1343,103 @@ 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'"); 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 { + Object evaluatedResult = eval("s.reverse()", ImmutableMap.of("s", string)); + + assertThat(evaluatedResult).isEqualTo(expectedResult); + } + + @Test + @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 { + Object evaluatedResult = eval("s.reverse()", ImmutableMap.of("s", string)); + + 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 { + Object evaluatedResult = eval("strings.quote(s)", ImmutableMap.of("s", string)); + + assertThat(evaluatedResult).isEqualTo(expectedResult); + } + + @Test + public void quote_singleWithDoubleQuotes() throws Exception { + String expr = "strings.quote('single-quote with \"double quote\"')"; + String expected = "\"\\\"single-quote with \\\\\\\"double quote\\\\\\\"\\\"\""; + Object evaluatedResult = eval(expr + " == " + expected); + + assertThat(evaluatedResult).isEqualTo(true); + } + + @Test + public void quote_escapesSpecialCharacters() throws Exception { + Object evaluatedResult = + eval( + "strings.quote(s)", + 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 quote_escapesMalformed_endWithHighSurrogate() throws Exception { + 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 { + assertThat(eval("strings.quote(s)", ImmutableMap.of("s", "bad pair \uD83DA"))) + .isEqualTo("\"bad pair \uFFFDA\""); + } + + @Test + public void quote_escapesMalformed_unpairedLowSurrogate() throws Exception { + assertThat(eval("strings.quote(s)", ImmutableMap.of("s", "bad pair \uDC00A"))) + .isEqualTo("\"bad pair \uFFFDA\""); + } + @Test public void stringExtension_compileUnallowedFunction_throws() { CelCompiler celCompiler = @@ -1474,23 +1447,31 @@ 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, () -> celRuntime.createProgram(ast).eval()); + assertThrows(CelEvaluationException.class, () -> customRuntimeCel.createProgram(ast).eval()); } + + } 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, 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/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/AstMutator.java b/optimizer/src/main/java/dev/cel/optimizer/AstMutator.java index 59f842e29..ca9fb8bdf 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); @@ -664,8 +812,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 +825,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 @@ -775,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); @@ -790,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 -> { @@ -807,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( @@ -839,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; } @@ -982,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 { + + abstract long exprIdToReplace(); + + 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) + abstract static class Replacement { + + abstract CelMutableExpr expr(); + + abstract CelMutableAst ast(); + + abstract Kind kind(); + + static Replacement ofExpr(CelMutableExpr expr) { + return AutoOneOf_AstMutator_SubtreeReplacement_Replacement.expr( + Preconditions.checkNotNull(expr)); + } + + static Replacement ofAst(CelMutableAst ast) { + return AutoOneOf_AstMutator_SubtreeReplacement_Replacement.ast( + Preconditions.checkNotNull(ast)); + } + + /** Kind of {@link Replacement}. */ + enum Kind { + EXPR, + AST + } + } + } } diff --git a/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel b/optimizer/src/main/java/dev/cel/optimizer/BUILD.bazel index e9e8994a2..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,9 +68,12 @@ java_library( ":ast_optimizer", ":optimization_exception", ":optimizer_builder", + ":optimizer_listener", "//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/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 4ac8764f1..7e14a8dfc 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java +++ b/optimizer/src/main/java/dev/cel/optimizer/CelOptimizerImpl.java @@ -15,54 +15,163 @@ 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; 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 ImmutableSet listeners; - CelOptimizerImpl(Cel cel, ImmutableSet astOptimizers) { + CelOptimizerImpl( + Cel cel, + ImmutableSet astOptimizers, + ImmutableSet listeners) { this.cel = cel; this.astOptimizers = astOptimizers; + this.listeners = listeners; } @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."); } + 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.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); + } + + 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(); } - } 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; } + 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 (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).", + macroExpr.id(), + astExpr.exprKind().getKind(), + macroExpr.exprKind().getKind())); + } + }); + } + } + + 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); @@ -72,10 +181,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 @@ -91,9 +202,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/main/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel index 7984cf3ba..8219753fd 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -28,15 +28,24 @@ java_library( "//common/ast", "//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", + "//common/values", + "//common/values:cel_value", + "//common/values:cel_value_provider", "//extensions:optional_library", "//optimizer:ast_optimizer", "//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", + "@maven//:org_jspecify_jspecify", ], ) @@ -57,12 +66,14 @@ java_library( "//common:mutable_ast", "//common:mutable_source", "//common/ast", + "//common/ast:cel_block", "//common/ast:mutable_expr", "//common/navigation", "//common/navigation:common", "//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", @@ -86,6 +97,8 @@ java_library( "//common:operator", "//common/ast", "//common/ast:mutable_expr", + "//common/navigation:common", + "//common/navigation:expr_util", "//common/navigation:mutable_navigation", "//common/types", "//common/types:type_providers", @@ -98,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/ConstantFoldingOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizer.java index ada73ce56..aa724fdb6 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; @@ -21,49 +22,77 @@ 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; +import dev.cel.bundle.CelBuilder; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelMutableAst; import dev.cel.common.CelSource; 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; -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; +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; +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.AstMutator.SubtreeReplacement; import dev.cel.optimizer.CelAstOptimizer; import dev.cel.optimizer.CelOptimizationException; +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; 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.Map.Entry; +import java.util.Objects; import java.util.Optional; +import org.jspecify.annotations.Nullable; /** * Performs optimization for inlining constant scalar and aggregate literal values within function * 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()); @@ -92,54 +121,97 @@ 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(); + 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(); - - CelMutableAst mutableAst = CelMutableAst.fromCelAst(ast); - int iterCount = 0; - boolean continueFolding = true; - while (continueFolding) { - if (iterCount >= constantFoldingOptions.maxIterationLimit()) { - throw new IllegalStateException("Max iteration count reached."); - } - iterCount++; - continueFolding = false; - - ImmutableList foldableExprs = - CelNavigableMutableAst.fromAst(mutableAst) - .getRoot() - .allNodes() - .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, foldableExpr.expr()); - if (!mutatedResult.isPresent()) { - // Evaluate the call then fold - mutatedResult = maybeFold(optimizerEnv, mutableAst, foldableExpr); - } + Cel optimizerEnv = builder.setResultType(SimpleType.DYN).build(); - if (!mutatedResult.isPresent()) { - // Skip this expr. It's neither prune-able nor foldable. - continue; - } + CelMutableAst initialMutableAst = CelMutableAst.fromCelAst(ast); + ImmutableMap identTypes = precomputeIdentTypes(initialMutableAst); - continueFolding = true; - mutableAst = mutatedResult.get(); - } + CelMutableAst mutableAst = + foldConstants(optimizerEnv, valueProvider, identTypes, initialMutableAst); + mutableAst = pruneOptionalElements(mutableAst); + + if (mutableAst == initialMutableAst) { + return OptimizationResult.create(ast); } - // 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()); } + 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. + // 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)); + } + return ImmutableMap.copyOf(mutableIdentTypes); + } + + 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; + } + mutableAst = astMutator.replaceSubtree(mutableAst, replacement.get()); + } + throw new IllegalStateException("Max iteration count reached."); + } + + 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())); + } + } + return Optional.empty(); + } + private boolean canFold(CelNavigableMutableExpr navigableExpr) { switch (navigableExpr.getKind()) { case CALL: @@ -182,6 +254,9 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) { if (functionName.equals(Operator.EQUALS.getFunction()) || functionName.equals(Operator.NOT_EQUALS.getFunction())) { + if (CelNavigableExprUtil.hasComprehensionVariable(navigableExpr)) { + return false; + } if (mutableCall.args().stream() .anyMatch(node -> isExprConstantOfKind(node, CelConstant.Kind.BOOLEAN_VALUE)) || mutableCall.args().stream() @@ -191,7 +266,7 @@ private boolean canFold(CelNavigableMutableExpr navigableExpr) { } if (functionName.equals(Operator.IN.getFunction())) { - return canFoldInOperator(navigableExpr); + return !CelNavigableExprUtil.hasComprehensionVariable(navigableExpr); } // Default case: all call arguments must be constants. If the argument is a container (ex: @@ -220,34 +295,6 @@ 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 areChildrenArgConstant(CelNavigableMutableExpr expr) { if (expr.getKind().equals(Kind.CONSTANT)) { return true; @@ -277,13 +324,17 @@ private static boolean isNestedComprehension(CelNavigableMutableExpr expr) { return false; } - private Optional maybeFold( - Cel cel, CelMutableAst mutableAst, CelNavigableMutableExpr node) + private Optional maybeFold( + Cel cel, @Nullable CelValueProvider valueProvider, CelNavigableMutableExpr node) throws CelOptimizationException { + if (!node.getKind().equals(Kind.COMPREHENSION) + && CelNavigableExprUtil.hasComprehensionVariable(node)) { + return Optional.empty(); + } Object result; try { - result = evaluateExpr(cel, CelMutableExprConverter.fromMutableExpr(node.expr())); - } catch (CelValidationException | CelEvaluationException e) { + result = evaluateExpr(cel, node); + } catch (CelEvaluationException | CelValidationException e) { throw new CelOptimizationException( "Constant folding failure. Failed to evaluate subtree due to: " + e.getMessage(), e); } @@ -293,21 +344,26 @@ 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, node.expr()); } - return maybeAdaptEvaluatedResult(result) - .map(celExpr -> astMutator.replaceSubtree(mutableAst, celExpr, node.id())); + return maybeAdaptEvaluatedResult(cel.getTypeProvider(), valueProvider, result); } - 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(); } @@ -318,12 +374,14 @@ private Optional maybeAdaptEvaluatedResult(Object result) { } else if (result instanceof Map) { Map map = (Map) result; List mapEntries = new ArrayList<>(); - for (Entry entry : map.entrySet()) { - CelMutableExpr adaptedKey = maybeAdaptEvaluatedResult(entry.getKey()).orElse(null); + for (Map.Entry entry : map.entrySet()) { + 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(); } @@ -348,43 +406,78 @@ 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) return Optional.empty(); } - 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. + private Optional maybeRewriteOptional( + CelTypeProvider typeProvider, + CelValueProvider valueProvider, + Optional optResult, + CelMutableExpr expr) { + 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(newOptionalNoneExpr()); + } - 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())); + 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(); } - return Optional.empty(); + CelMutableExpr newOptionalOfCall = + CelMutableExpr.ofCall( + CelMutableCall.create(Function.OPTIONAL_OF.getFunction(), adaptedResult)); + + return Optional.of(newOptionalOfCall); + } + + 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. */ - private Optional maybePruneBranches( - CelMutableAst mutableAst, CelMutableExpr expr) { + private Optional maybePruneBranches( + CelMutableAst mutableAst, Map identTypes, CelMutableExpr expr) { if (!expr.getKind().equals(Kind.CALL)) { return Optional.empty(); } @@ -393,7 +486,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); @@ -404,7 +497,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)) { @@ -413,23 +506,34 @@ 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); 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(CelMutableExpr.ofConstant(CelConstant.ofValue(true))); + } + + CelType needleType = + mutableAst + .getType(needle.id()) + .orElseGet(() -> identTypes.get(needle.ident().name())); + + if (needleType != null && isSafeForExactEquality(needleType)) { + return Optional.of(CelMutableExpr.ofConstant(CelConstant.ofValue(true))); + } } } } @@ -437,8 +541,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(); @@ -446,7 +550,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( @@ -454,7 +560,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( @@ -464,14 +572,14 @@ 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( - CelMutableAst mutableAst, CelMutableExpr expr) { + private Optional maybeShortCircuitCall( + CelMutableAst mutableAst, Map identTypes, CelMutableExpr expr) { CelMutableCall call = expr.call(); boolean shortCircuit = false; boolean skip = true; @@ -491,7 +599,7 @@ private Optional maybeShortCircuitCall( } if (arg.constant().booleanValue() == shortCircuit) { - return Optional.of(astMutator.replaceSubtree(mutableAst, arg, expr.id())); + return Optional.of(arg); } } @@ -499,10 +607,15 @@ 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) { - 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(remainingArg); + } + return Optional.empty(); } // TODO: Support folding variadic AND/ORs. @@ -510,6 +623,57 @@ private Optional maybeShortCircuitCall( "Folding variadic logical operator is not supported yet."); } + private static 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; + } + 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()) @@ -568,7 +732,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; } @@ -579,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()); } @@ -609,7 +784,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)); @@ -650,7 +825,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)); @@ -674,13 +849,22 @@ 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()) + .map(CelAttributePattern::fromQualifiedIdentifier) + .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. */ @@ -690,6 +874,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 { @@ -702,6 +893,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. @@ -729,7 +931,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() {} @@ -739,6 +942,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/main/java/dev/cel/optimizer/optimizers/InliningOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/InliningOptimizer.java index e4051f82f..696b6749b 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; @@ -27,21 +25,21 @@ 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.CelNavigableMutableAst; +import dev.cel.common.navigation.CelNavigableExprUtil; 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; 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 @@ -103,31 +101,36 @@ 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) { - 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()))); + }); + } + + if (mutableAst == initialMutableAst) { + return OptimizationResult.create(ast); } return OptimizationResult.create(astMutator.renumberIdsConsecutively(mutableAst).toParsedAst()); @@ -222,23 +225,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) { 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..c50c07c29 --- /dev/null +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java @@ -0,0 +1,523 @@ +// 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.types.TypeParamType; +import dev.cel.common.types.TypeType; +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, leaf type is 3rd argument)
+ *   request.user.age -> cel.@attribute(request,
+ *       [[user_num, "user", type_code], [age_num, "age", type_code, default_val]], int)
+ *
+ *   // 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 = -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, + ListType.create(SimpleType.DYN), + TypeType.create(TYPE_PARAM_T))); + + @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)); + 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) { + if (field.isMapField()) { + return CEL_MAP_TYPE_CODE; + } + 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(); + } + + 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 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()); + } + + 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 { + + 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/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SubexpressionOptimizer.java index ce9a5dc77..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,13 +34,13 @@ 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; 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; @@ -54,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; @@ -64,6 +64,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; @@ -96,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); @@ -160,27 +159,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++; + String blockIdentifier = CelBlock.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()) { @@ -219,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 @@ -228,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)); } /** @@ -238,64 +233,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) { @@ -649,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. */ 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 748e7ee89..539f7d341 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", @@ -19,6 +21,7 @@ java_library( "//common/ast", "//common/ast:mutable_expr", "//common/navigation", + "//common/navigation:common", "//common/types", "//compiler", "//extensions", @@ -29,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 cb0bff6c6..0867ac0e0 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,46 @@ @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(); + + 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() { @@ -131,4 +175,208 @@ 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) + .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) + .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) + .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) + .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); + } + + @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(); + } } 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..1fd34709a 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", @@ -9,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", @@ -17,27 +18,39 @@ java_library( "//common:container", "//common:mutable_ast", "//common:options", + "//common:proto_ast", "//common/ast", "//common/navigation:mutable_navigation", "//common/types", "//extensions", "//extensions:optional_library", + # "//java/com/google/testing/testsize:annotations", "//optimizer", + "//optimizer:ast_optimizer", "//optimizer:optimization_exception", "//optimizer:optimizer_builder", "//optimizer/optimizers:common_subexpression_elimination", "//optimizer/optimizers:constant_folding", "//optimizer/optimizers:inlining", + "//optimizer/optimizers:select_optimizer", "//parser:macro", "//parser:unparser", "//runtime", "//runtime:function_binding", + "//runtime:partial_vars", + "//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", + "@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", + "@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 e259a7a35..74b078097 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/ConstantFoldingOptimizerTest.java @@ -18,10 +18,13 @@ 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; 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.CelFunctionDecl; @@ -29,7 +32,12 @@ 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; +import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.extensions.CelExtensions; import dev.cel.extensions.CelOptionalLibrary; @@ -41,57 +49,99 @@ 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; @RunWith(TestParameterInjector.class) public class ConstantFoldingOptimizerTest { private static final CelOptions CEL_OPTIONS = - CelOptions.current().populateMacroCalls(true).enableTimestampEpoch(true).build(); - private static final Cel CEL = - CelFactory.standardCelBuilder() - .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(); - - private static final CelOptimizer CEL_OPTIMIZER = - CelOptimizerFactory.standardCelOptimizerBuilder(CEL) - .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) + CelOptions.current() + .populateMacroCalls(true) + .enableHeterogeneousNumericComparisons(true) .build(); private static final CelUnparser CEL_UNPARSER = CelUnparserFactory.newUnparser(); + @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(); + } + + 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)) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .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()) + .addMessageTypes(dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor()) + .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) + .setOptions(CEL_OPTIONS) + .addCompilerLibraries( + CelExtensions.comprehensions(), + CelExtensions.bindings(), + CelOptionalLibrary.INSTANCE, + CelExtensions.math(), + CelExtensions.strings(), + CelExtensions.sets(CEL_OPTIONS), + CelExtensions.encoders(CEL_OPTIONS)) + .addRuntimeLibraries( + CelOptionalLibrary.INSTANCE, + CelExtensions.math(), + CelExtensions.strings(), + CelExtensions.sets(CEL_OPTIONS), + CelExtensions.encoders(CEL_OPTIONS)) + .build(); + } + @Test @TestParameters("{source: 'null', expected: 'null'}") @TestParameters("{source: '1 + 2', expected: '3'}") @@ -103,17 +153,16 @@ public class ConstantFoldingOptimizerTest { @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( @@ -127,7 +176,48 @@ public class ConstantFoldingOptimizerTest { @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]'}") @@ -178,6 +268,8 @@ public class ConstantFoldingOptimizerTest { @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'}") @@ -204,20 +296,49 @@ public class ConstantFoldingOptimizerTest { @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: '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'}") + @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'}") - @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: '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'}") @@ -235,12 +356,79 @@ public class ConstantFoldingOptimizerTest { @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\"'}") + @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 { - CelAbstractSyntaxTree ast = CEL.compile(source).getAst(); + CelAbstractSyntaxTree ast = cel.compile(source).getAst(); - CelAbstractSyntaxTree optimizedAst = CEL_OPTIMIZER.optimize(ast); + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + + 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); } @@ -282,15 +470,21 @@ 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\")'}") + @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 = - CelFactory.standardCelBuilder() + runtimeFlavor + .builder() .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 +524,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() + runtimeFlavor + .builder() .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 +577,38 @@ 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)'}") + @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(); + CelAbstractSyntaxTree ast = cel.compile(source).getAst(); - CelAbstractSyntaxTree optimizedAst = CEL_OPTIMIZER.optimize(ast); + CelAbstractSyntaxTree optimizedAst = 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 = cel.compile("get_true() == get_true()").getAst(); ConstantFoldingOptions options = ConstantFoldingOptions.newBuilder().addFoldableFunctions("get_true").build(); CelOptimizer optimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers(ConstantFoldingOptimizer.newInstance(options)) .build(); @@ -401,9 +617,207 @@ 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 = CelFactory.standardCelBuilder().setResultType(SimpleType.STRING).build(); + Cel cel = runtimeFlavor.builder().setResultType(SimpleType.STRING).build(); CelOptimizer optimizer = CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers(ConstantFoldingOptimizer.getInstance()) @@ -419,10 +833,11 @@ public void constantFold_withExpectedResultTypeSet_success() throws Exception { public void constantFold_withMacroCallPopulated_comprehensionsAreReplacedWithNotSet() throws Exception { Cel cel = - CelFactory.standardCelBuilder() + runtimeFlavor + .builder() .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 +907,9 @@ public void constantFold_withMacroCallPopulated_comprehensionsAreReplacedWithNot @Test public void constantFold_astProducesConsistentlyNumberedIds() throws Exception { - CelAbstractSyntaxTree ast = CEL.compile("[1] + [2] + [3]").getAst(); + CelAbstractSyntaxTree ast = cel.compile("[1] + [2] + [3]").getAst(); - CelAbstractSyntaxTree optimizedAst = CEL_OPTIMIZER.optimize(ast); + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); assertThat(optimizedAst.getExpr().toString()) .isEqualTo( @@ -509,25 +924,37 @@ 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 = - CelFactory.standardCelBuilder() - .setOptions(CelOptions.current().maxParseRecursionDepth(200).build()) + runtimeFlavor + .builder() + .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).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 = 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/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/SelectOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java new file mode 100644 index 000000000..83057e44d --- /dev/null +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java @@ -0,0 +1,1042 @@ +// 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.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; +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.CelAstOptimizer; +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]], int)"), + PROTO3_SINGLE_MESSAGE_FIELD_SELECT( + "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]], int)"), + PROTO2_SINGLE_MESSAGE_FIELD_SELECT( + "proto2_msg.single_nested_message", + "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]]," + + " 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]], int)"), + PROTO2_CHAINED_MESSAGE_FIELD_SELECT( + "nested_msg.child.payload", + "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). + 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]], 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]], 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]], uint)"), + PROTO3_ZERO_UINT32( + "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]], 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\"]], string)"), + PROTO3_ZERO_STRING( + "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]], 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]], 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]], double)"), + PROTO3_ZERO_DOUBLE( + "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\"]]," + + " bytes)"), + PROTO3_ZERO_BYTES( + "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]], int)"), + PROTO3_ZERO_ENUM( + "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]], int)"), + PROTO3_SFIXED64( + "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, []]], list)"), + PROTO3_REPEATED_PRIMITIVE( + "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, []]], list)"), + + // Well-known types + PROTO3_TIMESTAMP( + "msg.single_timestamp", + "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\")]]," + + " google.protobuf.Duration)"), + + // Map selects + MAP_FIELD_INDEXING( + "msg.map_int64_message[1].bb", + "cel.@attribute(" + + "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]], 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]], 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\"]])"), + 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\", -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\", -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]], int) > 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]], int)"); + } + + @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]], int)"); + } + + @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]], int)"); + } + + @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", + ImmutableList.of(Object.class, List.class, Object.class), + args -> 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", + ImmutableList.of(Object.class, List.class, Object.class), + args -> args[1])) + .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", + ImmutableList.of(Object.class, List.class, Object.class), + args -> 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", + ImmutableList.of(Object.class, List.class, Object.class), + args -> 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]], int)"); + } + + @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]], int)"); + } + + @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]], int)"); + 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]], int)"); + assertThat(CEL_UNPARSER.unparse(proto3Optimized)).isEqualTo("msg.single_int64"); + } + + private enum CompilerRejectionTestCase { + ATTRIBUTE_AT_SIGN( + SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL, + "cel.@attribute(msg, [], int)", + "token recognition error at: '@'"), + ATTRIBUTE_OVERLOAD( + SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL, + "cel_attribute_list(msg, [], int)", + "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" + + " args {\n" + + " id: 13\n" + + " ident_expr {\n" + + " name: \"int\"\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); + } + + @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"); + } +} 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..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,7 +24,6 @@ // import com.google.testing.testsize.MediumTest; import dev.cel.bundle.Cel; import dev.cel.bundle.CelBuilder; -import dev.cel.bundle.CelFactory; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; @@ -43,6 +42,8 @@ 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; import org.junit.Test; @@ -51,6 +52,43 @@ // @MediumTest @RunWith(TestParameterInjector.class) public class SubexpressionOptimizerBaselineTest extends BaselineTestCase { + 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(); private static final TestAllTypes TEST_ALL_TYPES_INPUT = TestAllTypes.newBuilder() @@ -67,7 +105,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() @@ -79,6 +116,7 @@ public class SubexpressionOptimizerBaselineTest extends BaselineTestCase { @Before public void setUp() { + this.cel = setupCelEnv(runtimeFlavor.builder()); overriddenBaseFilePath = ""; } @@ -90,45 +128,70 @@ protected String baselineFileName() { return overriddenBaseFilePath; } + @TestParameter CelRuntimeFlavor runtimeFlavor; + + private Cel cel; + @Test public void allOptimizers_producesSameEvaluationResult( @TestParameter CseTestOptimizer cseTestOptimizer, @TestParameter CseTestCase cseTestCase) throws Exception { skipBaselineVerification(); - CelAbstractSyntaxTree ast = 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 = CEL.createProgram(ast).eval(inputMap); + Object expectedEvalResult = cel.createProgram(ast).eval(inputMap); - CelAbstractSyntaxTree optimizedAst = cseTestOptimizer.cseOptimizer.optimize(ast); + CelAbstractSyntaxTree optimizedAst = cseTestOptimizer.newCseOptimizer(cel).optimize(ast); - Object optimizedEvalResult = CEL.createProgram(optimizedAst).eval(inputMap); + Object optimizedEvalResult = cel.createProgram(optimizedAst).eval(inputMap); + assertThat(optimizedEvalResult).isEqualTo(expectedEvalResult); + } + + @Test + public void allOptimizers_producesSameEvaluationResult_parsedOnly( + @TestParameter CseTestCase cseTestCase, @TestParameter CseTestOptimizer cseTestOptimizer) + throws Exception { + skipBaselineVerification(); + if (runtimeFlavor.equals(CelRuntimeFlavor.LEGACY)) { + return; + } + 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 = cel.createProgram(ast).eval(inputMap); + + CelAbstractSyntaxTree optimizedAst = cseTestOptimizer.newCseOptimizer(cel).optimize(ast); + CelAbstractSyntaxTree parsedOnlyOptimizedAst = + CelAbstractSyntaxTree.newParsedAst(optimizedAst.getExpr(), optimizedAst.getSource()); + + Object optimizedEvalResult = cel.createProgram(parsedOnlyOptimizedAst).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 = 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(cel).optimize(ast); } catch (Exception e) { testOutput().printf("[%s]: Optimization Error: %s", optimizerName, e); continue; } if (!resultPrinted) { Object optimizedEvalResult = - CEL.createProgram(optimizedAst) + 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 +208,22 @@ 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 = 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(cel).optimize(ast); if (!resultPrinted) { Object optimizedEvalResult = - CEL.createProgram(optimizedAst) + 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 +242,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 = cel.compile(cseTestCase.source).getAst(); + CelAbstractSyntaxTree optimizedAst = + newCseOptimizer(cel, cseTestOptimizer.option).optimize(ast); testOutput().println(optimizedAst.getExpr()); } } @@ -193,7 +257,7 @@ 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()); + cel, SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()); runLargeTestCases(celOptimizer); } @@ -202,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( - CEL, + cel, SubexpressionOptimizerOptions.newBuilder() .populateMacroCalls(true) .subexpressionMaxRecursionDepth(1) @@ -215,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( - CEL, + cel, SubexpressionOptimizerOptions.newBuilder() .populateMacroCalls(true) .subexpressionMaxRecursionDepth(2) @@ -228,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( - CEL, + cel, SubexpressionOptimizerOptions.newBuilder() .populateMacroCalls(true) .subexpressionMaxRecursionDepth(3) @@ -238,15 +302,14 @@ 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 = cel.compile(cseTestCase.source).getAst(); CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); Object optimizedEvalResult = - 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); @@ -260,34 +323,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().enableTimestampEpoch(true).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)) @@ -316,17 +351,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(Cel cel) { + return SubexpressionOptimizerBaselineTest.newCseOptimizer(cel, option); + } + + // Defers building the optimizer until the test runs + 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 735cd24f0..209dba3a5 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; @@ -52,52 +51,89 @@ 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 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.function.ThrowingRunnable; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) public class SubexpressionOptimizerTest { - private static final Cel CEL = newCelBuilder().build(); - - private static final Cel CEL_FOR_EVALUATING_BLOCK = - CelFactory.standardCelBuilder() - .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(); + } + + 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 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(); @@ -105,8 +141,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( @@ -116,8 +151,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(cel) .addAstOptimizers(SubexpressionOptimizer.newInstance(options)) .build(); } @@ -131,15 +166,53 @@ 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 = cel.compile("size('a') + size('a') == 2").getAst(); CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); - assertThat(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 = cel.compile("\"abc\".charAt(10) + \"abc\".charAt(10)").getAst(); + CelOptimizer optimizedOptimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(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 = 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 = cel.compile("size(\"a\") == 1 ? x.y : x.y").getAst(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(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 = + 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\")"), @@ -170,7 +243,7 @@ private enum CseNoOpTestCase { @Test public void cse_withCelBind_noop(@TestParameter CseNoOpTestCase testCase) throws Exception { - CelAbstractSyntaxTree ast = CEL.compile(testCase.source).getAst(); + CelAbstractSyntaxTree ast = cel.compile(testCase.source).getAst(); CelAbstractSyntaxTree optimizedAst = newCseOptimizer(SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()) @@ -182,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 = CEL.compile(testCase.source).getAst(); + CelAbstractSyntaxTree ast = cel.compile(testCase.source).getAst(); CelAbstractSyntaxTree optimizedAst = newCseOptimizer(SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()) @@ -195,7 +268,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(); + cel.compile("['foo'].map(x, [x+x]) + ['foo'].map(x, [x+x, x+x])").getAst(); CelOptimizer celOptimizer = newCseOptimizer( SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()); @@ -211,10 +284,10 @@ 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") + 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(cel) .addAstOptimizers( ConstantFoldingOptimizer.getInstance(), SubexpressionOptimizer.newInstance( @@ -229,10 +302,10 @@ 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") + 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(cel) .addAstOptimizers( SubexpressionOptimizer.newInstance( SubexpressionOptimizerOptions.newBuilder().build()), @@ -247,9 +320,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 = cel.compile("size(x) + size(x)").getAst(); CelOptimizer optimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers( SubexpressionOptimizer.newInstance( SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()), @@ -272,7 +345,7 @@ public void iterationLimitReached_throws() throws Exception { largeExprBuilder.append("+"); } } - CelAbstractSyntaxTree ast = CEL.compile(largeExprBuilder.toString()).getAst(); + CelAbstractSyntaxTree ast = cel.compile(largeExprBuilder.toString()).getAst(); CelOptimizationException e = assertThrows( @@ -288,9 +361,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 = cel.compile("size(x) + size(x)").getAst(); CelOptimizer optimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(CEL) + CelOptimizerFactory.standardCelOptimizerBuilder(cel) .addAstOptimizers( SubexpressionOptimizer.newInstance( SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()), @@ -304,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; } } @@ -323,9 +400,22 @@ 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 = celForEvaluatingBlock.createProgram(ast).eval(); + + assertThat(evaluatedResult).isEqualTo(testCase.expectedResult); + } + + @Test + public void block_success_parsedOnly(@TestParameter BlockTestCase testCase) throws Exception { + if (runtimeFlavor.equals(CelRuntimeFlavor.LEGACY)) { + return; + } + CelAbstractSyntaxTree ast = + compileUsingInternalFunctions(testCase.source, /* parsedOnly= */ true); + + Object evaluatedResult = celForEvaluatingBlock.createProgram(ast).eval(); - assertThat(evaluatedResult).isNotNull(); + assertThat(evaluatedResult).isEqualTo(testCase.expectedResult); } @Test @@ -518,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"); @@ -530,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"); } @@ -540,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"); @@ -555,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"); @@ -572,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"); @@ -584,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 @@ -596,19 +696,79 @@ 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, () -> cel.createProgram(ast).eval()); assertThat(e).hasMessageThat().contains("/ by zero"); 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) */ - private static CelAbstractSyntaxTree compileUsingInternalFunctions(String expression) + private CelAbstractSyntaxTree compileUsingInternalFunctions(String expression, boolean parsedOnly) throws CelValidationException { - CelAbstractSyntaxTree astToModify = CEL_FOR_EVALUATING_BLOCK.compile(expression).getAst(); + CelAbstractSyntaxTree astToModify = celForEvaluatingBlock.compile(expression).getAst(); CelMutableAst mutableAst = CelMutableAst.fromCelAst(astToModify); CelNavigableMutableAst.fromAst(mutableAst) .getRoot() @@ -630,6 +790,14 @@ private static CelAbstractSyntaxTree compileUsingInternalFunctions(String expres indexExpr.ident().setName(internalIdentName); }); - return CEL_FOR_EVALUATING_BLOCK.check(mutableAst.toParsedAst()).getAst(); + if (parsedOnly) { + return mutableAst.toParsedAst(); + } + return celForEvaluatingBlock.check(mutableAst.toParsedAst()).getAst(); + } + + private CelAbstractSyntaxTree compileUsingInternalFunctions(String expression) + throws CelValidationException { + return compileUsingInternalFunctions(expression, false); } } 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/parser/BUILD.bazel b/parser/BUILD.bazel index 1e662c3c5..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"], @@ -11,16 +12,49 @@ 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"], ) +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"], @@ -31,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/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 e32c50ee8..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,13 +9,35 @@ package( ], ) +# 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", - "ExpressionBalancer.java", "Parser.java", ] +# keep sorted +PRATT_PARSER_SOURCES = [ + "Lexer.java", + "PrattParser.java", +] + # keep sorted PARSER_BUILDER_SOURCES = [ "CelParser.java", @@ -49,25 +72,116 @@ 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, tags = [ ], deps = [ + ":antlr_parser", ":macro", + ":parser_base", ":parser_builder", + ":pratt_parser", + "//common:cel_source", + "//common:cel_validation_result", + "//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_issue", "//common:cel_source", - "//common:compiler_common", + "//common:cel_validation_result", "//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", @@ -75,6 +189,46 @@ java_library( ], ) +java_library( + name = "pratt_parser", + srcs = PRATT_PARSER_SOURCES, + tags = [ + ], + deps = [ + ":macro", + "//common:cel_ast", + "//common:cel_issue", + "//common:cel_source", + "//common:cel_validation_result", + "//common:operator", + "//common:options", + "//common:source_location", + "//common/ast", + "//common/internal", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + +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, @@ -83,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", ], @@ -96,7 +264,7 @@ java_library( ], deps = [ "//:auto_value", - "//common:compiler_common", + "//common:cel_issue", "//common:operator", "//common:source_location", "//common/ast", @@ -106,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 615b073ef..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,184 +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()); } - /** 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/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/main/java/dev/cel/parser/Lexer.java b/parser/src/main/java/dev/cel/parser/Lexer.java new file mode 100644 index 000000000..602a6ef00 --- /dev/null +++ b/parser/src/main/java/dev/cel/parser/Lexer.java @@ -0,0 +1,645 @@ +// 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"), + + // 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; + 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 + 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 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 >= size) { + return makeToken(TokenType.END, start, start); + } + int c = content.get(position); + switch (c) { + case '.': + { + if (position + 1 < 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); + 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 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); + } + + private void advance(int n) { + position += n; + } + + private boolean match(int c) { + return position < 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 < size) { + int cp = content.get(position); + if (predicate.test(cp)) { + advance(1); + return true; + } + } + return false; + } + + private void consumeLine() { + while (position < size) { + if (content.get(position) == '\n') { + advance(1); + return; + } + advance(1); + } + } + + private void consumeWhitespaceAndComments() { + while (position < size) { + int c = content.get(position); + switch (c) { + case '\f': + case '\n': + case ' ': + case '\r': + case 11: // \v + case '\t': + position++; + break; + case '/': + if (position + 1 < size && content.get(position + 1) == '/') { + consumeLine(); + break; + } else { + return; + } + default: + return; + } + } + } + + private boolean consumeDigits() { + int start = position; + while (position < size && isDigit(content.get(position))) { + position++; + } + return position > start; + } + + private boolean consumeHexDigits() { + int start = position; + while (position < size && isHexDigit(content.get(position))) { + position++; + } + return position > start; + } + + 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 < 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 = size; + return false; + } + + private boolean consumeUntilAfterTripleQuote(int quote, boolean isRaw) { + int pos = position; + boolean escaped = false; + while (pos < size) { + int cc = content.get(pos); + if (!isRaw && cc == '\\') { + escaped = !escaped; + } else { + if ((isRaw || !escaped) + && pos + 2 < size + && cc == quote + && content.get(pos + 1) == quote + && content.get(pos + 2) == quote) { + position = pos + 3; + return true; + } + escaped = false; + } + pos++; + } + position = size; + return false; + } + + private Token consumeStringLiteral(int start, int quote, boolean isBytes, boolean isRaw) { + advance(1); + boolean isTripleQuote = + position + 1 < 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 >= 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 < 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 < 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 < size + && content.get(position) == '.' + && position + 1 < 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 < size && isIdentTrailing(content.get(position))) { + position++; + } + int end = position; + String word = content.substring(start, end); + TokenType keywordType = KEYWORDS.get(word); + if (keywordType != null) { + return makeToken(keywordType, start, end); + } + return makeToken(TokenType.IDENT, start, end, word); + } +} 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/Parser.java b/parser/src/main/java/dev/cel/parser/Parser.java index af860e936..9b2a5aad8 100644 --- a/parser/src/main/java/dev/cel/parser/Parser.java +++ b/parser/src/main/java/dev/cel/parser/Parser.java @@ -14,1368 +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); - 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); + if (options.enablePrattParser()) { + return PrattParser.parse(source, options, parser.getMacros()); } - 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) { - - 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(); + return AntlrParser.parse(source, options, parser.getMacros().values()); } - @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(); - } - } - - 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 ExprFactory( - org.antlr.v4.runtime.Parser recognizer, - CelSource.Builder sourceInfo, - String accumulatorVarName) { - 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; - } - - // 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; - } - - @Override - public String getAccumulatorVarName() { - return accumulatorVarName; - } - - // Internal methods used by the parser but not part of the public API. - @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)); - } - - // Implementation of CelExprFactory. - - @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 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 (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. - // Do not call this method directly from within the parser, use nextExprId(int). - return nextExprId(peekPosition()); - } - - 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 new file mode 100644 index 000000000..829ac1dcc --- /dev/null +++ b/parser/src/main/java/dev/cel/parser/PrattParser.java @@ -0,0 +1,1315 @@ +// 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.CelCodePointArray; +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.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import org.jspecify.annotations.Nullable; + +/** 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; + + 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, NO_POSITION, NO_POSITION); + + /** 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; + 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[] 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 { + final Lexer.Token token; + long id; + + UnaryOp(Lexer.Token token) { + this.token = token; + } + } + + private final CelSource source; + private final CelCodePointArray content; + private final CelOptions options; + private final ImmutableMap macros; + private final Lexer lexer; + + /** + * 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; + 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( + LOCALE, + "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(); + prattParser.copyPositionsTo(sourceBuilder); + 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.content = source.getContent(); + this.options = options; + this.macros = ImmutableMap.copyOf(macros); + 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); + } + + 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 String getTokenText(Lexer.Token tok) { + if (tok.text != null) { + return tok.text; + } + 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); + if (isRecoveryLimitExceeded()) { + return END_TOKEN; + } + } + return tok; + } + + private Lexer.Token nextToken() { + currentToken = peekToken; + if (isRecoveryLimitExceeded()) { + peekToken = END_TOKEN; + 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; + } + + // Find the next delimiter to prevent a cascade of spurious secondary errors. + private void synchronizeOnDelimiter() { + if (isRecoveryLimitExceeded()) { + peekToken = END_TOKEN; + 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( + LOCALE, + "expression node limit (%d) exceeded", + options.maxParseExpressionNodeCount())); + nodeLimitExceeded = true; + } + if (!nodeLimitExceeded && position >= 0) { + setPosition(id, position); + } + return id; + } + + private long nextId(Lexer.Token token) { + return nextId(token.start); + } + + private long nextId() { + return nextId(NO_POSITION); + } + + private void setPosition(long id, Lexer.Token token) { + if (token.start >= 0) { + 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; + } + return nextId(getPosition(id)); + } + + private void eraseId(long 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 = + position >= 0 + ? source.getOffsetLocation(position).orElse(CelSourceLocation.NONE) + : 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( + LOCALE, "More than %d parse errors.", options.maxParseErrorRecoveryLimit()))); + peekToken = END_TOKEN; + } + 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()) { + reportRecursionLimit(token.start); + return true; + } + return false; + } + + private void reportRecursionLimit(int position) { + if (!recursionLimitExceeded) { + recursionLimitExceeded = true; + reportError( + position, + String.format( + LOCALE, + "Expression recursion limit exceeded. limit: %d", + options.maxParseRecursionDepth())); + } + } + + private CelExpr parseExpr() { + if (recursionLimitExceeded || errorCount > options.maxParseErrorRecoveryLimit()) { + return ERROR; + } + 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; + while (true) { + Lexer.TokenType tok = peekToken.type; + if (tok == Lexer.TokenType.QUESTION && minPrec <= 0) { + lhs = parseTernary(lhs); + continue; + } + + BinaryOpInfo opInfo = binaryOps[tok.ordinal()]; + if (opInfo == null || opInfo.precedence < minPrec) { + break; + } + + if (opInfo.isLogical) { + lhs = parseBalancedLogicalChain(lhs, opInfo); + continue; + } + + Lexer.Token opTok = nextToken(); + if (recursionDepth + chainDepth > options.maxParseRecursionDepth()) { + reportRecursionLimit(opTok.start); + return ERROR; + } + chainDepth++; + 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.ofCall( + opId, Operator.CONDITIONAL.getFunction(), ImmutableList.of(lhs, trueExpr, falseExpr)); + } + + private CelExpr parseBalancedLogicalChain(CelExpr lhs, BinaryOpInfo opInfo) { + 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) { + 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, opsCount - 1); + } + + private CelExpr balancedTree(String op, CelExpr[] terms, long[] ops, int lo, int hi) { + int mid = (lo + hi + 1) / 2; + 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.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() { + 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) { + 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) { + if (checkRecursion(chainDepth, peekToken)) { + return ERROR; + } + chainDepth++; + 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 field = + CelExpr.ofConstant(nextId(getLeftmostPosition(lhs)), CelConstant.ofValue(idText)); + 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); + lhs = + expanded.isPresent() + ? expanded.get() + : CelExpr.ofCall(callId, Optional.of(lhs), idText, args); + } else { + lhs = CelExpr.ofSelect(nextId(dotTok), lhs, idText, /* isTestOnly= */ false); + } + } else if (tok == Lexer.TokenType.LEFT_BRACKET) { + if (checkRecursion(chainDepth, peekToken)) { + return ERROR; + } + chainDepth++; + 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 = buildBinaryCall(opId, opName, lhs, index); + } else if (tok == Lexer.TokenType.LEFT_BRACE) { + String structName = extractStructName(lhs); + if (structName == null) { + break; + } + lhs = parseStruct(nextId(peekToken.start), structName); + } else { + break; + } + } + currentLhsDepth = chainDepth; + return lhs; + } + + 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(peekToken), /* isNegative= */ true); + } + if (peekToken.type == Lexer.TokenType.FLOAT) { + return parseDoubleLiteral(nextId(peekToken), /* isNegative= */ true); + } + } + + if (checkRecursion(0, 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 buildUnaryCall(opId, opName, operand); + } + + 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) { + if (checkRecursion(chainDepth, op.token)) { + return ERROR; + } + chainDepth++; + } + + 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 = buildUnaryCall(ops.get(i).id, opName, operand); + } + + 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.ofCall(callId, name, args); + } + long id = nextId(leadingDot ? firstTok : idTok); + return CelExpr.ofIdent(id, name); + } + + private CelExpr parsePrimary() { + switch (peekToken.type) { + case LEFT_PAREN: + { + int groupingParenCount = countGroupingParentheses(); + if (checkRecursion(groupingParenCount, peekToken)) { + return ERROR; + } + 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.ofConstant(nextId(nextToken()), Constants.NULL); + case TRUE: + case FALSE: + { + Lexer.Token tok = nextToken(); + return CelExpr.ofConstant( + nextId(tok), tok.type == Lexer.TokenType.TRUE ? Constants.TRUE : Constants.FALSE); + } + 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); + 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) { + boolean optional = false; + if (peekToken.type == Lexer.TokenType.QUESTION) { + Lexer.Token q = nextToken(); + optional = true; + if (!options.enableOptionalSyntax()) { + reportError(q.start, "unsupported syntax '?'"); + } + } + elements.add(parseExpr()); + if (optional) { + optionalIndices.add(elemIndex); + } + elemIndex++; + if (peekToken.type == Lexer.TokenType.COMMA) { + nextToken(); + } else { + break; + } + } + expect(Lexer.TokenType.RIGHT_BRACKET, "expected ']'"); + return CelExpr.ofList(listId, elements.build(), optionalIndices.build()); + } + + private CelExpr parseMap() { + Lexer.Token openTok = nextToken(); + long mapId = nextId(openTok); + ImmutableList.Builder entries = ImmutableList.builder(); + 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(); + entries.add(CelExpr.ofMapEntry(entryId, key, value, optional)); + if (peekToken.type == Lexer.TokenType.COMMA) { + nextToken(); + } else { + break; + } + } + expect(Lexer.TokenType.RIGHT_BRACE, "expected '}'"); + return CelExpr.ofMap(mapId, entries.build()); + } + + private CelExpr parseStruct(long objId, String structName) { + nextToken(); + 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) { + 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(); + entries.add(CelExpr.ofStructEntry(fieldId, fieldName, value, optional)); + if (peekToken.type == Lexer.TokenType.COMMA) { + nextToken(); + } else { + break; + } + } + expect(Lexer.TokenType.RIGHT_BRACE, "expected '}'"); + return CelExpr.ofStruct(objId, structName, entries.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.ofConstant(id, constExpr); + } catch (ParseException e) { + reportSyntaxError(tok, "invalid int literal: " + text); + 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.ofConstant(nextId(tok), constExpr); + } catch (ParseException e) { + reportSyntaxError(tok, "invalid uint literal: " + value); + 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); + return CelExpr.ofConstant(id, constExpr); + } catch (ParseException e) { + reportSyntaxError(tok, "invalid double literal: " + text); + 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.ofConstant(nextId(tok), constExpr); + } 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.ofConstant(nextId(tok), constExpr); + } 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 @Nullable String extractStructName(CelExpr expr) { + if (expr.exprKind().getKind() == CelExpr.ExprKind.Kind.IDENT) { + String name = expr.ident().name(); + eraseId(expr.id()); + return name; + } + if (expr.exprKind().getKind() == CelExpr.ExprKind.Kind.SELECT) { + if (expr.select().testOnly()) { + return null; + } + CelExpr operand = expr.select().operand(); + eraseId(expr.id()); + String prefix = extractStructName(operand); + return prefix != null ? prefix + "." + expr.select().field() : null; + } + return null; + } + + private int getLeftmostPosition(CelExpr expr) { + while (expr.exprKind().getKind() == CelExpr.ExprKind.Kind.SELECT) { + expr = expr.select().operand(); + } + return getPosition(expr.id()); + } + + 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 macro; + } + key = CelMacro.formatVarArgKey(id, receiverStyle); + return macros.get(key); + } + + private Optional tryExpandMacro( + long exprId, String function, @Nullable CelExpr target, ImmutableList args) { + if (function.isEmpty() || macros.isEmpty()) { + return Optional.empty(); + } + boolean isReceiver = (target != null); + int argCount = args.size(); + CelMacro macro = lookupMacro(function, argCount, isReceiver); + if (macro == null) { + return Optional.empty(); + } + if (nodeLimitExceeded) { + reportError( + getPosition(exprId), "could not expand macro: expression node limit exceeded"); + return Optional.empty(); + } + + if ((target != null && target.equals(ERROR)) || hasError(args)) { + eraseId(exprId); + return Optional.of(ERROR); + } + + int macroPosition = getPosition(exprId); + CelExpr targetExpr = (target != null ? target : CelExpr.ofNotSet(0)); + Optional expandedExpr = expandMacro(macroPosition, macro, targetExpr, args); + + if (expandedExpr.isPresent()) { + if (options.populateMacroCalls()) { + recordMacroCall(expandedExpr.get().id(), function, target, args); + } + eraseId(exprId); + return expandedExpr; + } + 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); + } finally { + macroExprFactory.popPosition(); + } + } + + 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())) { + callExpr.setTarget(CelExpr.newBuilder().setId(target.id()).build()); + } else { + callExpr.setTarget(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; + } + + // Fast path: if the next non-whitespace character is not '(', leading open parens is 1. + int pos = peekToken.end; + 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 '('. + break; + } + if (c == '(') { + break; + } + // Next significant token is definitely not '('. + return 1; + } + pos++; + } + + 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 = getPosition(exprId); + if (pos < 0) { + return CelSourceLocation.NONE; + } + return source.getOffsetLocation(pos).orElse(CelSourceLocation.NONE); + } + + @Override + protected CelSourceLocation currentSourceLocationForMacro() { + int pos = + !macroPositions.isEmpty() + ? peekPosition() + : (currentToken != null ? currentToken.start : NO_POSITION); + if (pos < 0) { + return CelSourceLocation.NONE; + } + 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/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/parser/src/test/java/dev/cel/parser/BUILD.bazel b/parser/src/test/java/dev/cel/parser/BUILD.bazel index 1b1668ce3..db5a647b7 100644 --- a/parser/src/test/java/dev/cel/parser/BUILD.bazel +++ b/parser/src/test/java/dev/cel/parser/BUILD.bazel @@ -1,21 +1,32 @@ load("@rules_java//java:defs.bzl", "java_library") +load("//:cel_android_rules.bzl", "cel_android_local_test") load("//:testing.bzl", "junit4_test_suites") -package(default_applicable_licenses = [ - "//:license", -]) +package( + default_applicable_licenses = [ + "//:license", + ], +) + +ANDROID_TESTS = [ + "CelLiteParserAndroidTest.java", +] java_library( name = "tests", testonly = True, - srcs = glob(["*Test.java"]), + srcs = glob( + ["*Test.java"], + exclude = ["TmpPrattParserTest.java"] + ANDROID_TESTS, + ), resources = ["//parser/src/test/resources:baselines"], deps = [ - "//:auto_value", "//: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", @@ -25,15 +36,16 @@ java_library( "//common/values:cel_byte_string", "//extensions:optional_library", "//parser", + "//parser:lite_parser_factory", "//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", @@ -42,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."); + } +} diff --git a/parser/src/test/java/dev/cel/parser/CelParserImplTest.java b/parser/src/test/java/dev/cel/parser/CelParserImplTest.java index 1e7b44fab..09d578a36 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserImplTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserImplTest.java @@ -17,6 +17,8 @@ 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.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; @@ -26,8 +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; @@ -37,11 +42,19 @@ 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 +70,8 @@ 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 +90,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 +110,16 @@ 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 +127,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 +144,7 @@ public void build_standardMacroKeyConflictsWithCustomMacro_throws() { assertThrows( IllegalArgumentException.class, () -> - CelParserImpl.newBuilder() + newParserBuilder() .setStandardMacros(CelStandardMacro.HAS) .addMacros(customMacro) .build()); @@ -136,7 +152,8 @@ 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 +161,7 @@ public void build_containsNoMacros() { public void setParserLibrary_success() { CelParserImpl parser = (CelParserImpl) - CelParserImpl.newBuilder() + newParserBuilder() .addLibraries( new CelParserLibrary() { @Override @@ -164,10 +181,16 @@ 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 = assertThrows(CelValidationException.class, parseResult::getAst); assertThat(exception.getErrors()).hasSize(1); @@ -221,9 +244,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,20 +264,88 @@ 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 public void parse_exprUnderMaxRecursionLimit_doesNotThrow( @TestParameter MaxParseRecursionDepthTestCase testCase) throws CelValidationException { 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(); + assertThat(parseResult.getAst()).isNotNull(); + } + + @Test + public void parse_nodeLimitExceeded_throws() { + CelParser parser = + newParserBuilder() + .setOptions( + CelOptions.newBuilder() + .enablePrattParser(enablePrattParser) + .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 = + newParserBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setOptions( + CelOptions.newBuilder() + .enablePrattParser(enablePrattParser) + .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 = + newParserBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .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(); assertThat(parseResult.getAst()).isNotNull(); } @@ -265,7 +359,7 @@ public void parse_exprUnderMaxRecursionLimit_doesNotThrow( @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) @@ -276,13 +370,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(); @@ -292,7 +386,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() {}); @@ -304,7 +398,7 @@ public void toParserBuilder_isImmutable() { @Test public void toParserBuilder_collectionProperties_copied() { CelParserBuilder celParserBuilder = - CelParserFactory.standardCelParserBuilder() + newParserBuilder() .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .addMacros( CelMacro.newGlobalMacro( @@ -319,4 +413,80 @@ 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(); + } + } + + @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(); + } } + + diff --git a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java index 58b45ddab..0f9fb36b7 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java @@ -14,24 +14,13 @@ 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 static com.google.common.collect.ImmutableMap.toImmutableMap; +import static com.google.common.truth.Truth.assertThat; -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.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; @@ -42,30 +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.CelAdorner; import dev.cel.testing.CelDebug; -import java.util.ArrayDeque; -import java.util.Deque; +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, @@ -75,201 +100,508 @@ 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 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; + } - private static final CelParser PARSER_WITH_OLD_ACCU_VAR = - PARSER - .toParserBuilder() - .setOptions( - CelOptions.current() - .populateMacroCalls(true) - .enableHiddenAccumulatorVar(false) - .build()) - .build(); + 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"); + runTest("a || b && c || d && e || f && g || h && i || j && k || l"); + + // 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, "1 + +"); - runTest(PARSER, "\"\\xFh\""); - runTest(PARSER, "\"\\a\\b\\f\\n\\r\\t\\v\\'\\\"\\\\\\? Illegal escape \\>\""); - 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( - PARSER, + "-[-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( + OPTIONS, "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[" + "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[" + "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[" @@ -278,237 +610,202 @@ 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]"); - - CelParser parserWithQuotedFields = - CelParserImpl.newBuilder() - .setOptions(CelOptions.current().enableQuotedIdentifierSyntax(true).build()) - .build(); - runTest(parserWithQuotedFields, "`bar`"); - runTest(parserWithQuotedFields, "foo.``"); - runTest(parserWithQuotedFields, "foo.`$bar`"); - - 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(.`.`"); + 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]"); } @Test - 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); + 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)"); } - private void runTest(CelParser parser, String expression, boolean validateParseOutput) { - testOutput().println("I: " + expression); + private void runAntlrTest(CelOptions options, String expression) { + testOutput().println("I: " + sanitizeForBaseline(expression)); testOutput().println("=====>"); - CelSource source = CelSource.newBuilder(expression).setDescription("").build(); - CelValidationResult parseResult = parser.parse(source); - - try { - CelProtoAbstractSyntaxTree protoAst = - CelProtoAbstractSyntaxTree.fromCelAst(parseResult.getAst()); - ParsedExpr parsedExpr = protoAst.toParsedExpr(); - if (validateParseOutput) { - testOutput() - .println( - "P: " - + CelDebug.toAdornedDebugString(parsedExpr.getExpr(), new KindAndIdAdorner())); - String locationOutput = - CelDebug.toAdornedDebugString( - parsedExpr.getExpr(), new LocationAdorner(parsedExpr.getSourceInfo())); - if (!locationOutput.isEmpty()) { - testOutput().println("L: " + locationOutput); - } + 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); } - - String macroOutput = convertMacroCallsToString(parsedExpr.getSourceInfo()); - if (!macroOutput.isEmpty()) { - testOutput().println("M: " + macroOutput); + 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(); } - 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)); + @Test + public void source_info() throws Exception { + runSourceInfoTest("[{}, {'field': true}].exists(i, has(i.field))"); } - 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 void runTest(String expression) { + runTest(OPTIONS, expression); } - 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()); - } + private void runTest(CelOptions options, String expression) { + runTest(options, expression, true); } - @AutoValue - @Immutable - abstract static class LineAndColumn { - - public abstract int getLine(); - - public abstract int getColumn(); + private void runTest(CelOptions options, String expression, boolean validateParseOutput) { + runTest(options, MACROS, expression, validateParseOutput); } - 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())); - } + private void runTest( + CelOptions options, + Map macros, + String expression, + boolean validateParseOutput) { + testOutput().println("I: " + sanitizeForBaseline(expression)); + testOutput().println("=====>"); - @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())); - } + ParseOutput antlrResult = + parse( + options.toBuilder().enablePrattParser(false).build(), + macros, + expression, + validateParseOutput); + ParseOutput prattResult = + parse( + options.toBuilder().enablePrattParser(true).build(), + macros, + expression, + validateParseOutput); + + assertThat(prattResult.isError()).isEqualTo(antlrResult.isError()); + if (!antlrResult.isError()) { + if (validateParseOutput) { + assertThat(prattResult.pOutput).isEqualTo(antlrResult.pOutput); + testOutput().println("P: " + antlrResult.pOutput); - 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++; + assertThat(prattResult.lOutput).isEqualTo(antlrResult.lOutput); + if (!Strings.isNullOrEmpty(antlrResult.lOutput)) { + testOutput().println("L: " + antlrResult.lOutput); } } - 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; + assertThat(prattResult.mOutput).isEqualTo(antlrResult.mOutput); + if (!Strings.isNullOrEmpty(antlrResult.mOutput)) { + testOutput().println("M: " + antlrResult.mOutput); } + } else { + testOutput().println("E/A: " + sanitizeForBaseline(antlrResult.errorMessage)); + testOutput().println("E/P: " + sanitizeForBaseline(prattResult.errorMessage)); } - return null; + + testOutput().println(); } - private static final Joiner JOINER = Joiner.on('.'); + private void runSourceInfoTest(String expression) throws Exception { + testOutput().println("I: " + expression); + testOutput().println("=====>"); + 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(); - 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); + 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 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(); + private static String sanitizeForBaseline(String text) { + if (text == null) { + return null; } - return JOINER.join(parts); + 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 new file mode 100644 index 000000000..710ff2fcf --- /dev/null +++ b/parser/src/test/java/dev/cel/parser/PrattParserTest.java @@ -0,0 +1,594 @@ +// 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}"); + 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("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"); + runTest("a || b && c || d && e || f && g || h && i || j && k || l"); + + // 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: " + sanitizeForBaseline(expression)); + 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: " + 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 54% rename from parser/src/test/resources/parser.baseline rename to parser/src/test/resources/parser_core_syntax.baseline index 37b8ef3cc..34997c9d8 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,321 @@ 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: _||_( + _<_( + 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 && c && d || e && f && g && h =====> -P: "\""^#1:string# -L: "\""^#1[1,0]# +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: [1,3,4][0] +I: a || b && c || d && e || f && g || h && i || j && k || l =====> -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#, + 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: 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 +1187,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 +1279,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 +1325,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 9f4b96825..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,196 +12,517 @@ 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: ((@)) +=====> +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 + | ((@)) + | ..^ + +I: 1 + $ +=====> +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: ó ¢ +»»ó 0  +»»\u007f0"""\""\"""\""\"""\""\"""\""\"""\"\"""\""\"""\""\"""\""\"""\"!\"""\""\"""\""\" +=====> +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: '\udead' == '\ufffd' +=====> +E/A: ERROR: :1:1: Invalid unicode code point + | '\udead' == '\ufffd' + | ^ +E/P: ERROR: :1:1: Invalid unicode code point + | '\udead' == '\ufffd' + | ^ I: a | b =====> -E: ERROR: :1:3: token recognition error at: '| ' +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: ? +I: '3# < 10" '& tru ^^ =====> -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} - | ? - | .^ +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: 1 + $ +I: '?' =====> -E: 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/A: ERROR: :1:1: Invalid unicode code point + | '?' + | ^ +E/P: ERROR: :1:1: Invalid unicode code point + | '?' + | ^ -I: 1.all(2, 3) +I: '?' =====> -E: ERROR: :1:7: The argument must be a simple name - | 1.all(2, 3) - | ......^ +E/A: ERROR: :1:1: Invalid unicode code point + | '?' + | ^ +E/P: ERROR: :1:1: Invalid unicode code point + | '?' + | ^ -I: 1.exists(2, 3) +I: r"\?" =====> -E: ERROR: :1:10: The argument must be a simple name - | 1.exists(2, 3) - | .........^ +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: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" +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 + | ? + | ^ + +I: a ? b ((?)) +=====> +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: a ? b @ +=====> +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: -[-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/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: "\a\b\f\n\r\t\v\'\"\\\? Illegal escape \>" +I: as break const continue else for function if import in let loop package namespace return var void while =====> -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 \>" +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: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 \>" +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: 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 + | [1, 2, 3].map(var, var * 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 @@ -214,7 +535,7 @@ ERROR: :1:26: reserved identifier: 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: '😁' @@ -223,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(.`.` - | ........^ +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 new file mode 100644 index 000000000..fb9d94e58 --- /dev/null +++ b/parser/src/test/resources/pratt_parser_core_syntax.baseline @@ -0,0 +1,1313 @@ +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,3]# + +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,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: 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,11]# + +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,12]# + +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,6]# + +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,7]# + +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,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 +=====> +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,0]# +)^#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,3]# +)^#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,3]# +)^#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^#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: _||_( + _<_( + 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 && c && d || e && f && g && h +=====> +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: 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: _?_:_( + 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^#4:bool# + )^#3:Expr.Call# + )^#2:Expr.Call#, + false^#6:bool# + )^#5:Expr.Call#, + 2^#8:int64#, + 3^#9:int64# +)^#7:Expr.Call# +L: _?_:_( + _||_( + _&&_( + false^#1[1,0]#, + !_( + 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]# + +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^#7:Expr.Ident#, + c^#9:Expr.Ident# + )^#8:Expr.Call# +)^#6:Expr.Call# +L: _&&_( + _[?_]( + _?._( + a^#1[1,0]#, + "b"^#3[1,0]# + )^#2[1,1]#, + 0^#5[1,6]# + )^#4[1,4]#, + _[?_]( + a^#7[1,12]#, + c^#9[1,15]# + )^#8[1,13]# +)^#6[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]# \ 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 new file mode 100644 index 000000000..d21e65ab3 --- /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  +»»\u007f0"""\""\"""\""\"""\""\"""\""\"""\"\"""\""\"""\""\"""\""\"""\"!\"""\""\"""\""\" +=====> +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: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: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: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: :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: 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: 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: 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:33: 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..f6ce2f6b0 --- /dev/null +++ b/parser/src/test/resources/pratt_parser_literals.baseline @@ -0,0 +1,469 @@ +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: 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: ERROR: :1:3: Syntax error: invalid int literal: 9223372036854775808 + | -(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 + | 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 +=====> +P: Infinity^#1:double# +L: Infinity^#1[1,0]# + +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..688a355d5 --- /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^#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]# \ No newline at end of file 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 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 52b0b1ba7..79c7a1f05 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", ], ) @@ -178,6 +179,7 @@ java_library( name = "compiled_rule", srcs = ["CelCompiledRule.java"], deps = [ + ":policy", "//:auto_value", "//bundle:cel", "//common:cel_ast", @@ -214,6 +216,7 @@ java_library( "//common/types", "//common/types:type_providers", "//optimizer", + "//optimizer:ast_optimizer", "//optimizer:optimization_exception", "//optimizer:optimizer_builder", "//optimizer/optimizers:common_subexpression_elimination", @@ -242,22 +245,26 @@ java_library( java_library( name = "rule_composer", srcs = ["RuleComposer.java"], - visibility = ["//visibility:private"], deps = [ ":compiled_rule", - "//:auto_value", + ":policy", "//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", + "//common/types", + "//common/types:cel_types", + "//common/types:type_providers", "//extensions:optional_library", "//optimizer:ast_optimizer", "//optimizer:mutable_ast", - "@maven//:com_google_errorprone_error_prone_annotations", "@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 36f1685fc..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,23 +44,35 @@ 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) && 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; @@ -154,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 9980d0cad..2df6f4e5f 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; @@ -27,6 +28,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; @@ -38,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(); @@ -52,6 +60,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() @@ -73,12 +85,13 @@ 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); - // 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 +103,18 @@ 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); + } + @CanIgnoreReturnValue public Builder addImport(Import value) { importList.add(value); @@ -102,15 +127,33 @@ 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) { - 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 +161,7 @@ public Builder putMetadata(Map map) { public CelPolicy build() { setImports(ImmutableList.copyOf(importList)); + setMetadata(ImmutableMap.copyOf(metadata)); return autoBuild(); } } @@ -138,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. */ @@ -190,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(); } } @@ -247,7 +296,7 @@ public abstract static class Builder implements RequiredFieldsChecker { abstract Optional id(); - abstract Optional result(); + public abstract Optional result(); abstract Optional explanation(); @@ -323,4 +372,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/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..d57ad3260 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; @@ -43,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; @@ -63,6 +65,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 @@ -89,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()); } @@ -140,19 +144,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); @@ -182,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. @@ -237,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: @@ -250,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); @@ -259,14 +266,29 @@ 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) { + // 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. + boolean isExhaustive = + isTriviallyTrue + && (compiledMatch.result().kind().equals(Kind.OUTPUT) + || !compiledMatch.result().rule().hasOptionalOutput()); + + if (compiledRule.semantic() == EvaluationSemantic.FIRST_MATCH + && isExhaustive + && i != matchCount - 1) { if (compiledMatch.result().kind().equals(Kind.OUTPUT)) { compilerContext.addIssue( compiledMatch.sourceId(), @@ -280,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()); @@ -339,6 +367,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 +391,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 +400,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; } } diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java b/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java index 43595c4ab..8db1d725c 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; @@ -27,7 +28,9 @@ 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; import dev.cel.policy.CelPolicy.Match.Result; import dev.cel.policy.CelPolicy.Variable; @@ -47,6 +50,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; @@ -126,28 +131,93 @@ 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)); 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); @@ -189,7 +259,7 @@ private void parseImport( continue; } - policyBuilder.addImport(Import.create(valueId, ctx.newValueString(value))); + policyBuilder.addImport(Import.create(valueId, ctx.newYamlString(value))); } } @@ -202,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); @@ -212,17 +284,33 @@ 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)); 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)) + .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)) + .setSemantic(EvaluationSemantic.AGGREGATE); + break; + default: tagVisitor.visitRuleTag(ctx, tagId, fieldName, value, policyBuilder, ruleBuilder); break; @@ -267,7 +355,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 +363,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 +374,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 +444,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 +473,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); @@ -409,6 +497,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, @@ -449,8 +613,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/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/main/java/dev/cel/policy/RuleComposer.java b/policy/src/main/java/dev/cel/policy/RuleComposer.java index 7bbde7685..f98152c62 100644 --- a/policy/src/main/java/dev/cel/policy/RuleComposer.java +++ b/policy/src/main/java/dev/cel/policy/RuleComposer.java @@ -18,27 +18,37 @@ 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.base.Preconditions; 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.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; 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; 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 { @@ -48,22 +58,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, /* asList= */ false); + 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, boolean asList) { cel = cel.toCelBuilder() .addVarDeclarations( @@ -72,94 +71,286 @@ 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. + 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. + CelType lastOutputType = null; 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); + + Step currentStep; + long currentSourceId; + String validationMessage; 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: 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); - assertComposedAstIsValid( - cel, - matchAst, - "conflicting output types found.", - matchOutput.sourceId(), - lastOutputId); - lastOutputId = matchOutput.sourceId(); - continue; + // 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. + CelMutableAst matchOutputAst = CelMutableAst.fromCelAst(matchOutput.ast()); + currentStep = + Step.newNonOptionalStep( + !isTriviallyTrue, condAst, returnList ? newList(matchOutputAst) : matchOutputAst); + + 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(); - RuleOptimizationResult 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; - } + Step nestedRule = optimizeRule(cel, matchNestedRule, returnList); + currentStep = new Step(nestedRule.isOptional, !isTriviallyTrue, condAst, nestedRule.expr); + currentSourceId = getFirstOutputSourceId(matchNestedRule); + 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; + } + + Preconditions.checkState(output != null, "Policy contains no outputs."); + CelMutableAst resultExpr = output.expr; + resultExpr = inlineCompiledVariables(resultExpr, compiledRule.variables()); + resultExpr = astMutator.renumberIdsConsecutively(resultExpr); + + return output.isOptional + ? Step.newUnconditionalOptionalStep(newTrueLiteral(), resultExpr) + : Step.newUnconditionalNonOptionalStep(newTrueLiteral(), resultExpr); + } + + private @Nullable Step createBaseStep(boolean returnList, boolean hasOptionalOutput) { + if (returnList) { + 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) { + // 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); + } + + // 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. - if (isOptionalResult && isTriviallyTrue) { - matchAst = astMutator.newMemberCall(nestedRuleAst, Function.OR.getFunction(), matchAst); - } else { - matchAst = + 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( - Operator.CONDITIONAL.getFunction(), - CelMutableAst.fromCelAst(conditionAst), - nestedRuleAst, - matchAst); - } + Function.OPTIONAL_OF.getFunction(), accumulatedStep.expr))); + } else { + return Step.newUnconditionalNonOptionalStep( + trueCondition, + astMutator.newMemberCall(currentStep.expr, "orValue", accumulatedStep.expr)); + } + } + } - assertComposedAstIsValid( - cel, - matchAst, - String.format( - "failed composing the subrule '%s' due to conflicting output types.", - matchNestedRule.ruleId().map(ValueString::value).orElse("")), - lastOutputId); - break; + 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)); } + } - CelMutableAst result = inlineCompiledVariables(matchAst, compiledRule.variables()); + 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()); - result = astMutator.renumberIdsConsecutively(result); + } else { + conditionalListPart = currentListPart; + } - return RuleOptimizationResult.create(result, isOptionalResult); + if (accumulatedStep == null) { + return Step.newUnconditionalNonOptionalStep(trueCondition, conditionalListPart); + } + + 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) + && expr.call().function().equals("optional.none") + && expr.call().args().isEmpty(); + } + + private static CelMutableAst newTrueLiteral() { + return CelMutableAst.of( + CelMutableExpr.ofConstant(CelConstant.ofValue(true)), CelMutableSource.newInstance()); } private CelMutableAst inlineCompiledVariables( @@ -186,30 +377,78 @@ private CelMutableAst inlineCompiledVariables( return mutatedAst; } - static RuleComposer newInstance( - CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) { - return new RuleComposer(compiledRule, variablePrefix, iterationLimit); - } - - 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 RuleComposer(CelCompiledRule compiledRule, String variablePrefix, int iterationLimit) { - this.compiledRule = checkNotNull(compiledRule); - this.variablePrefix = variablePrefix; - this.astMutator = AstMutator.newInstance(iterationLimit); + 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 + // 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 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 +464,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/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 9106caf70..6a76cf3b0 100644 --- a/policy/src/test/java/dev/cel/policy/BUILD.bazel +++ b/policy/src/test/java/dev/cel/policy/BUILD.bazel @@ -1,12 +1,17 @@ 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", testonly = True, srcs = glob(["*.java"]), + data = [ + "@cel_policy//conformance:testdata", + ], resources = [ "//testing:policy_test_resources", ], @@ -27,16 +32,19 @@ java_library( "//parser:parser_factory", "//parser:unparser", "//policy", + "//policy:compiled_rule", "//policy:compiler_factory", "//policy:parser", "//policy:parser_factory", - "//policy:policy_parser_context", + "//policy:rule_composer", "//policy:source", "//policy:validation_exception", + "//policy/testing:k8s_test_tag_handler", "//runtime", "//runtime:function_binding", - "//runtime:late_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 336a392ff..db1a36dcf 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyCompilerImplTest.java @@ -17,37 +17,45 @@ 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; 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.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; 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.testdata.SingleFileProto.SingleFile; +import dev.cel.testing.CelRuntimeFlavor; +import dev.cel.testing.testdata.SingleFile; 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; @@ -61,7 +69,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 private CelRuntimeFlavor runtimeFlavor; @Test public void compileYamlPolicy_success(@TestParameter TestYamlPolicy yamlPolicy) throws Exception { @@ -101,13 +114,289 @@ 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 evalYamlPolicy_aggregate() throws Exception { + String policySource = + "name: \"aggregate_policy\"\n" + + "rule:\n" + + " aggregate:\n" + + " - condition: 'true'\n" + + " output: '\"PII\"'\n" + + " - condition: 'true'\n" + + " output: '\"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" + + " output: '\"CSE1\"'\n" + + " - condition: \"size(resource.payload) > 5\"\n" + + " output: '\"CSE2\"'\n" + + " - condition: 'true'\n" + + " output: '\"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 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 = + "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" + + " output: \"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).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 + 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" + + " output: \"'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" + + " output: \"'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" + + " output: \"'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 { // 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()); @@ -195,7 +484,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) @@ -208,7 +497,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 @@ -244,7 +556,7 @@ public void evaluateYamlPolicy_withCanonicalTestData( } @Test - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") // Test only public void evaluateYamlPolicy_nestedRuleProducesOptionalOutput() throws Exception { Cel cel = newCel(); String policySource = @@ -258,11 +570,10 @@ 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> - assertThat(evalResult).hasValue(Optional.of(true)); + // Result is Optional containing true + assertThat(evalResult).hasValue(true); } @Test @@ -278,7 +589,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" @@ -297,8 +613,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); } @@ -319,12 +634,30 @@ public void evaluateYamlPolicy_withSimpleVariable() throws Exception { CelAbstractSyntaxTree compiledPolicyAst = CelPolicyCompilerFactory.newPolicyCompiler(cel).build().compile(policy); - boolean evalResult = (boolean) cel.createProgram(compiledPolicyAst).eval(); 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, + CelPolicy.EvaluationSemantic.FIRST_MATCH); + 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; @@ -340,7 +673,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()) { @@ -358,8 +691,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 +701,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(); } @@ -456,10 +792,16 @@ 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"), + 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; @@ -469,15 +811,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/CelPolicyYamlParserTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java index f8327c255..a881afb03 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java @@ -22,8 +22,10 @@ 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.CelPolicy.Invariant; +import dev.cel.policy.CelPolicy.Variable; import dev.cel.policy.PolicyTestHelper.TestYamlPolicy; +import dev.cel.policy.testing.K8sTagHandler; import org.junit.Test; import org.junit.runner.RunWith; @@ -99,6 +101,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 +160,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(); } @@ -180,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", @@ -203,6 +328,26 @@ 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" + + " - output: '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" + + " - 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" + + " | ..^"), ILLEGAL_YAML_TYPE_ON_RULE_VALUE( "rule: illegal", "ERROR: :1:7: Got yaml node type tag:yaml.org,2002:str, wanted type(s)" @@ -386,7 +531,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/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java b/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java index 8d9e0084b..3fe2e3322 100644 --- a/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java +++ b/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java @@ -18,33 +18,31 @@ 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 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; 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. */ +@AutoBazelRepository final class PolicyTestHelper { + private static final Runfiles runfiles = createRunfiles(); + 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,15 +59,31 @@ 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, "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, @@ -93,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, @@ -127,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); } @@ -154,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; } @@ -198,7 +229,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 +239,7 @@ public void setInput(Map input) { this.input = input; } - public void setOutput(String output) { + public void setOutput(Object output) { this.output = output; } @@ -220,7 +251,7 @@ public Map getInput() { return input; } - public String getOutput() { + public Object getOutput() { return output; } @@ -249,112 +280,31 @@ 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(); } - private static String readFile(String path) throws IOException { - return Resources.toString(getResource(path), UTF_8); + static boolean hasRunfile(String rlocationPath) { + String resolvedPath = runfiles.rlocation(Ascii.toLowerCase(rlocationPath)); + return resolvedPath != null && new File(resolvedPath).exists(); } - 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.newValueString(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.newValueString(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.newValueString(node); - conditionValue = - conditionValue.toBuilder().setValue("!(" + conditionValue.value() + ")").build(); - matchBuilder.setCondition(conditionValue); - break; - case "messageExpression": - matchBuilder.setResult(Result.ofOutput(ctx.newValueString(node))); - break; - default: - TagVisitor.super.visitMatchTag(ctx, id, tagName, node, policyBuilder, matchBuilder); - break; - } + private static Runfiles createRunfiles() { + try { + return Runfiles.preload().withSourceRepository(AutoBazelRepository_PolicyTestHelper.NAME); + } catch (IOException e) { + throw new RuntimeException("Failed to initialize Runfiles", e); } } 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"], +) 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/publish/BUILD.bazel b/publish/BUILD.bazel index cb13a70b5..2fb948cea 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 @@ -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", ] @@ -28,6 +29,10 @@ 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: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", @@ -129,6 +134,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", @@ -315,3 +332,63 @@ 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 library 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"], +) + +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", +) + +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 ea793eee5..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.12.0-SNAPSHOT" +CEL_VERSION = "0.14.0" 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 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/repositories.bzl b/repositories.bzl index 8e9a9ba47..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, @@ -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 = "01bcc1c3f7c9c5e442fa940013cd6d029af2baf7" + cel_policy_sha = "8e3ddc74e918c2a5910794387354a236da601694dbf7b6921f8a7babf7b78181" + 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, diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index 074ef2059..e1acc4261 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -9,6 +9,10 @@ package( java_library( name = "runtime", exports = [ + ":async_call", + ":async_drain_strategy", + ":async_observer", + ":async_options", ":descriptor_message_provider", ":evaluation_exception", ":function_overload", @@ -226,11 +230,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 = [ @@ -330,6 +344,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"], @@ -338,9 +357,74 @@ 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", ], ) + +java_library( + name = "accumulated_unknowns", + visibility = ["//:internal"], + exports = [ + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns", + ], +) + +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"], +) + +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"], +) + +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/planner/BUILD.bazel b/runtime/planner/BUILD.bazel index 8da29f270..0a4ef8a84 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"], @@ -9,3 +10,28 @@ java_library( name = "program_planner", 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"], + 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"], +) + +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/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 0746a5b83..9518e1601 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -44,15 +44,11 @@ LITE_RUNTIME_IMPL_SOURCES = [ "LiteRuntimeImpl.java", ] -# keep sorted -LITE_PROGRAM_IMPL_SOURCES = [ - "LiteProgramImpl.java", -] - # keep sorted FUNCTION_BINDING_SOURCES = [ "CelFunctionBinding.java", "FunctionBindingImpl.java", + "InternalCelFunctionBinding.java", ] # keep sorted @@ -129,7 +125,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 +146,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", @@ -214,6 +208,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", @@ -230,6 +225,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", @@ -248,6 +244,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", @@ -414,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", ], ) @@ -506,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", ], @@ -523,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", ], @@ -730,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", ], ) @@ -742,6 +735,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", @@ -756,6 +750,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", @@ -789,7 +784,9 @@ cel_android_library( java_library( name = "function_overload", srcs = [ + "CelAsyncFunctionOverload.java", "CelFunctionOverload.java", + "OptimizedFunctionOverload.java", ], tags = [ ], @@ -804,7 +801,11 @@ java_library( cel_android_library( name = "function_overload_android", srcs = [ + "CelAsyncFunctionOverload.java", "CelFunctionOverload.java", + "OptimizedFunctionOverload.java", + ], + tags = [ ], deps = [ ":evaluation_exception", @@ -820,14 +821,16 @@ java_library( tags = [ ], deps = [ + ":async_options", ":descriptor_type_resolver", ":dispatcher", ":evaluation_exception", ":evaluation_listener", ":function_binding", ":function_resolver", + ":partial_vars", ":program", - ":proto_message_runtime_helpers", + ":proto_message_runtime_equality", ":runtime", ":runtime_equality", ":standard_functions", @@ -849,6 +852,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", @@ -864,7 +872,7 @@ java_library( tags = [ ], deps = [ - ":cel_value_runtime_type_provider", + ":async_options", ":descriptor_message_provider", ":descriptor_type_resolver", ":dispatcher", @@ -887,9 +895,8 @@ 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:add", "//runtime/standard:int", "//runtime/standard:timestamp", "@maven//:com_google_code_findbugs_annotations", @@ -908,7 +915,9 @@ java_library( deps = [ ":runtime", ":runtime_legacy_impl", + ":runtime_planner_impl", "//common:options", + "@maven//:com_google_errorprone_error_prone_annotations", ], ) @@ -919,12 +928,14 @@ java_library( ], deps = [ ":activation", + ":async_options", ":evaluation_exception", ":evaluation_listener", ":function_binding", ":function_resolver", ":interpretable", ":interpreter", + ":partial_vars", ":program", ":proto_message_activation_factory", ":runtime_equality", @@ -955,15 +966,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", ], ) @@ -973,53 +984,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", - ":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", - ":program_android", - ":variable_resolver", - "//:auto_value", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", ], ) @@ -1029,20 +1012,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", @@ -1135,46 +1119,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"], @@ -1186,6 +1130,7 @@ java_library( ":unknown_attributes", "//common/annotations", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", "@maven//:org_jspecify_jspecify", ], ) @@ -1193,7 +1138,6 @@ java_library( cel_android_library( name = "interpreter_util_android", srcs = ["InterpreterUtil.java"], - visibility = ["//visibility:private"], deps = [ ":accumulated_unknowns_android", ":evaluation_exception", @@ -1201,6 +1145,7 @@ cel_android_library( "//common/annotations", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", ], ) @@ -1219,7 +1164,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", @@ -1236,15 +1180,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", ], ) @@ -1260,20 +1204,24 @@ 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", ], ) cel_android_library( name = "accumulated_unknowns_android", srcs = ["AccumulatedUnknowns.java"], - visibility = ["//visibility:private"], deps = [ ":unknown_attributes_android", + "//common/annotations", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", ], ) @@ -1283,9 +1231,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", ], @@ -1297,14 +1248,155 @@ 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", + ], +) + +java_library( + name = "partial_vars", + srcs = ["PartialVars.java"], + tags = [ + ], + deps = [ + ":variable_resolver", + "//:auto_value", + "//runtime:unknown_attributes", + "@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_android//:com_google_guava_guava", + ], +) + +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 = "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"], @@ -1313,8 +1405,10 @@ java_library( deps = [ ":evaluation_exception", ":function_resolver", + ":partial_vars", ":variable_resolver", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", ], ) @@ -1326,9 +1420,10 @@ cel_android_library( deps = [ ":evaluation_exception", ":function_resolver_android", + ":partial_vars_android", ":variable_resolver", - "//:auto_value", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", ], ) 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/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/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/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/CelAttribute.java b/runtime/src/main/java/dev/cel/runtime/CelAttribute.java index 6080dbaa1..8db377abc 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; @@ -105,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) { @@ -184,9 +185,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 +211,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()); diff --git a/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionBinding.java index c7b63926b..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; @@ -51,21 +53,46 @@ 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), (args) -> impl.apply((T) args[0])); + return from( + overloadId, + ImmutableList.of(arg), + new OptimizedFunctionOverload() { + @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); + } + }); } /** * 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), (args) -> impl.apply((T1) args[0], (T2) args[1])); + overloadId, + ImmutableList.of(arg1, arg2), + new OptimizedFunctionOverload() { + @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}. */ @@ -75,6 +102,75 @@ 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( String functionName, CelFunctionBinding... overloadBindings) { @@ -84,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/CelFunctionOverload.java b/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java index 3e30a2146..c5f75096d 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java +++ b/runtime/src/main/java/dev/cel/runtime/CelFunctionOverload.java @@ -26,6 +26,7 @@ public interface CelFunctionOverload { /** Evaluate a set of arguments throwing a {@code CelException} on error. */ Object apply(Object[] args) throws CelEvaluationException; + /** * Helper interface for describing unary functions where the type-parameter is used to improve * compile-time correctness of function bindings. @@ -57,27 +58,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); + } - if (!paramType.isAssignableFrom(arg.getClass())) { + 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); + } + + 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/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/main/java/dev/cel/runtime/CelLateFunctionBindings.java b/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java index c1f4b236f..2da08120c 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java +++ b/runtime/src/main/java/dev/cel/runtime/CelLateFunctionBindings.java @@ -63,9 +63,14 @@ 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(), - (args) -> binding.getDefinition().apply(args), + binding.getDefinition(), binding.isStrict(), binding.getArgTypes()); } 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/CelResolvedOverload.java b/runtime/src/main/java/dev/cel/runtime/CelResolvedOverload.java index 2bcdf3a2d..fbe9a3289 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; /** @@ -29,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(); @@ -52,27 +56,67 @@ 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(getFunctionName(), ImmutableList.of(getOverloadId())); + } + + public Object invoke(Object arg) throws CelEvaluationException { + if (isDynamicDispatch() + || CelFunctionOverload.canHandle(arg, getParameterTypes(), isStrict())) { + return getOptimizedDefinition().apply(arg); + } + throw new CelOverloadNotFoundException(getFunctionName(), ImmutableList.of(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(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, List> parameterTypes) { + OptimizedFunctionOverload optimizedDef = + (definition instanceof OptimizedFunctionOverload) + ? (OptimizedFunctionOverload) definition + : definition::apply; return new AutoValue_CelResolvedOverload( - overloadId, ImmutableList.copyOf(parameterTypes), isStrict, definition); + functionName, + overloadId, + ImmutableList.copyOf(parameterTypes), + isStrict, + definition, + optimizedDef); } /** @@ -81,4 +125,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/CelRuntime.java b/runtime/src/main/java/dev/cel/runtime/CelRuntime.java index 416bca132..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. @@ -90,6 +97,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/CelRuntimeBuilder.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java index 87f11fde2..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; @@ -167,7 +168,17 @@ public interface CelRuntimeBuilder { @CanIgnoreReturnValue CelRuntimeBuilder setValueProvider(CelValueProvider celValueProvider); - /** Enable or disable the standard CEL library functions and variables. */ + /** Returns the configured {@link CelValueProvider}, or null if not set. */ + CelValueProvider valueProvider(); + + /** + * 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); @@ -204,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/CelRuntimeFactory.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeFactory.java index 322985b22..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,13 +25,48 @@ 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 .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 44377db09..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; @@ -45,19 +47,26 @@ 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; +/** + * {@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(); @@ -88,11 +97,36 @@ 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)); } + 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() { @@ -114,7 +148,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 @@ -129,27 +169,83 @@ public Object eval( return program.eval(resolver, lateBoundFunctionResolver); } + @Override + 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 { - 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 @@ -158,7 +254,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 @@ -167,7 +268,19 @@ 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 + 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 @@ -180,16 +293,23 @@ 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()) .setContainer(CelContainer.newBuilder().build()) - .setExtensionRegistry(ExtensionRegistry.getEmptyRegistry()); + .setExtensionRegistry(ExtensionRegistry.getEmptyRegistry()) + .setAsyncEvaluationOptions(CelAsyncEvaluationOptions.defaultOptions()); } + /** 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); @@ -211,13 +331,21 @@ 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(); abstract CelTypeProvider typeProvider(); - abstract CelValueProvider valueProvider(); + @Override + public abstract CelValueProvider valueProvider(); abstract CelStandardFunctions standardFunctions(); @@ -323,6 +451,7 @@ public Builder setTypeFactory(Function typeFactory) { } @Override + @Deprecated public Builder setStandardEnvironmentEnabled(boolean value) { throw new UnsupportedOperationException( "Unsupported. Subset the environment using setStandardFunctions instead."); @@ -365,7 +494,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(), @@ -373,7 +507,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(), @@ -395,7 +534,8 @@ private static CelDescriptorPool newDescriptorPool( @Override public CelRuntime build() { - assertAllowedCelOptions(options()); + CelOptions options = options(); + assertAllowedCelOptions(options); CelDescriptors celDescriptors = CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(fileDescriptorsBuilder().build()); @@ -407,15 +547,7 @@ 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 = - 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) { @@ -427,6 +559,13 @@ public CelRuntime build() { } } + if (valueProvider() != null) { + protoMessageValueProvider = + CombinedCelValueProvider.combine(protoMessageValueProvider, valueProvider()); + } + setValueProvider(protoMessageValueProvider); + CelValueConverter celValueConverter = protoMessageValueProvider.celValueConverter(); + CelTypeProvider messageTypeProvider = ProtoMessageTypeProvider.newBuilder() .setCelDescriptors(celDescriptors) @@ -443,7 +582,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 ebd678f24..144de7e9d 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java @@ -19,8 +19,8 @@ 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.util.concurrent.ListeningExecutorService; import com.google.errorprone.annotations.CanIgnoreReturnValue; import javax.annotation.concurrent.ThreadSafe; import com.google.protobuf.DescriptorProtos.FileDescriptorSet; @@ -43,8 +43,8 @@ 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; import dev.cel.runtime.standard.TimestampFunction.TimestampOverload; import java.util.Arrays; @@ -78,7 +78,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. @@ -86,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) { @@ -103,7 +104,8 @@ public CelRuntimeBuilder toRuntimeBuilder() { .setExtensionRegistry(extensionRegistry) .addFileTypes(fileDescriptors) .addLibraries(celRuntimeLibraries) - .addFunctionBindings(celFunctionBindings); + .addFunctionBindings(celFunctionBindings) + .setAsyncEvaluationOptions(asyncEvaluationOptions); if (customTypeFactory != null) { builder.setTypeFactory(customTypeFactory); @@ -113,8 +115,8 @@ public CelRuntimeBuilder toRuntimeBuilder() { builder.setStandardFunctions(overriddenStandardFunctions); } - if (celValueProvider != null) { - builder.setValueProvider(celValueProvider); + if (asyncExecutor != null) { + builder.setAsyncExecutor(asyncExecutor); } return builder; @@ -136,8 +138,9 @@ public static final class Builder implements CelRuntimeBuilder { @VisibleForTesting final ImmutableSet.Builder celRuntimeLibraries; @VisibleForTesting Function customTypeFactory; - @VisibleForTesting CelValueProvider celValueProvider; @VisibleForTesting CelStandardFunctions overriddenStandardFunctions; + @VisibleForTesting CelAsyncEvaluationOptions asyncEvaluationOptions; + @VisibleForTesting @Nullable ListeningExecutorService asyncExecutor; private CelOptions options; @@ -209,8 +212,13 @@ 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 + public CelValueProvider valueProvider() { + throw new UnsupportedOperationException("valueProvider is not supported for legacy runtime"); } @Override @@ -220,6 +228,7 @@ public CelRuntimeBuilder setTypeFactory(Function typeFa } @Override + @Deprecated public CelRuntimeBuilder setStandardEnvironmentEnabled(boolean value) { standardEnvironmentEnabled = value; return this; @@ -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() { @@ -303,41 +325,47 @@ public CelRuntimeLegacyImpl build() { } } - ImmutableMap.Builder functionBindingsBuilder = - ImmutableMap.builder(); + DefaultDispatcher.Builder dispatcherBuilder = DefaultDispatcher.newBuilder(); for (CelFunctionBinding standardFunctionBinding : newStandardFunctionBindings(runtimeEquality)) { - functionBindingsBuilder.put( - standardFunctionBinding.getOverloadId(), standardFunctionBinding); + String functionName = standardFunctionBinding.getOverloadId(); + if (standardFunctionBinding instanceof InternalCelFunctionBinding) { + functionName = ((InternalCelFunctionBinding) standardFunctionBinding).getFunctionName(); + } + dispatcherBuilder.addOverload( + functionName, + 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()) { + String functionName = customBinding.getOverloadId(); + if (customBinding instanceof InternalCelFunctionBinding) { + functionName = ((InternalCelFunctionBinding) customBinding).getFunctionName(); + } + dispatcherBuilder.addOverload( + functionName, + customBinding.getOverloadId(), + customBinding.getArgTypes(), + customBinding.isStrict(), + customBinding.getDefinition()); + } + CelValueConverter celValueConverter = CelValueConverter.getDefaultInstance(); RuntimeTypeProvider runtimeTypeProvider; if (options.enableCelValue()) { - CelValueProvider messageValueProvider = celValueProvider; - - if (messageValueProvider == null) { - messageValueProvider = ProtoMessageValueProvider.newInstance(options, dynamicProto); - } - - runtimeTypeProvider = CelValueRuntimeTypeProvider.newInstance(messageValueProvider); + throw new UnsupportedOperationException( + "enableCelValue is not supported for legacy runtime"); } else { runtimeTypeProvider = new DescriptorMessageProvider(runtimeTypeFactory, options); } DefaultInterpreter interpreter = new DefaultInterpreter( - DescriptorTypeResolver.create(), + DescriptorTypeResolver.create(celValueConverter), runtimeTypeProvider, dispatcherBuilder.build(), options); @@ -349,10 +377,11 @@ public CelRuntimeLegacyImpl build() { extensionRegistry, customTypeFactory, overriddenStandardFunctions, - celValueProvider, fileDescriptors, runtimeLibraries, - ImmutableList.copyOf(customFunctionBindings.values())); + ImmutableList.copyOf(customFunctionBindings.values()), + asyncEvaluationOptions, + asyncExecutor); } private ImmutableSet newStandardFunctionBindings( @@ -427,6 +456,8 @@ private Builder() { this.celRuntimeLibraries = ImmutableSet.builder(); this.extensionRegistry = ExtensionRegistry.getEmptyRegistry(); this.customTypeFactory = null; + this.asyncEvaluationOptions = CelAsyncEvaluationOptions.defaultOptions(); + this.asyncExecutor = null; } } @@ -437,19 +468,21 @@ private CelRuntimeLegacyImpl( ExtensionRegistry extensionRegistry, @Nullable Function customTypeFactory, @Nullable CelStandardFunctions overriddenStandardFunctions, - @Nullable CelValueProvider celValueProvider, ImmutableSet fileDescriptors, ImmutableSet celRuntimeLibraries, - ImmutableList celFunctionBindings) { + ImmutableList celFunctionBindings, + CelAsyncEvaluationOptions asyncEvaluationOptions, + @Nullable ListeningExecutorService asyncExecutor) { this.interpreter = interpreter; this.options = options; this.standardEnvironmentEnabled = standardEnvironmentEnabled; this.extensionRegistry = extensionRegistry; this.customTypeFactory = customTypeFactory; this.overriddenStandardFunctions = overriddenStandardFunctions; - this.celValueProvider = celValueProvider; 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/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/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/CelValueRuntimeTypeProvider.java b/runtime/src/main/java/dev/cel/runtime/CelValueRuntimeTypeProvider.java deleted file mode 100644 index e071289ca..000000000 --- a/runtime/src/main/java/dev/cel/runtime/CelValueRuntimeTypeProvider.java +++ /dev/null @@ -1,150 +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.CelValue; -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 maybeUnwrapCelValue( - 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 maybeUnwrapCelValue(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 maybeUnwrapCelValue(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); - } - - private CelValueRuntimeTypeProvider( - CelValueProvider valueProvider, BaseProtoCelValueConverter protoCelValueConverter) { - this.valueProvider = checkNotNull(valueProvider); - this.protoCelValueConverter = checkNotNull(protoCelValueConverter); - } -} diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java index 0d13f13be..0a467db81 100644 --- a/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java +++ b/runtime/src/main/java/dev/cel/runtime/DefaultDispatcher.java @@ -17,18 +17,19 @@ 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; 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; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -124,57 +125,107 @@ 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 String functionName(); - abstract ImmutableMap.Builder overloadsBuilder(); + abstract ImmutableList> argTypes(); + + abstract boolean isStrict(); + + abstract CelFunctionOverload overload(); + + private static OverloadEntry of( + String functionName, + ImmutableList> argTypes, + boolean isStrict, + CelFunctionOverload overload) { + return new AutoValue_DefaultDispatcher_Builder_OverloadEntry( + functionName, argTypes, isStrict, overload); + } + } + + private final Map overloads; @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); - overloadsBuilder() - .put( - overloadId, - CelResolvedOverload.of( - overloadId, - args -> guardedOp(overloadId, args, argTypes, isStrict, overload), - isStrict, - argTypes)); + OverloadEntry newEntry = OverloadEntry.of(functionName, 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) { - /** 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); + 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(overloadId, incoming.argTypes(), isStrict, mergedOverload); + } + + throw new IllegalArgumentException("Duplicate overload ID binding: " + overloadId); } - throw new CelOverloadNotFoundException(functionName); + public DefaultDispatcher build() { + ImmutableMap.Builder resolvedOverloads = ImmutableMap.builder(); + for (Map.Entry entry : overloads.entrySet()) { + String overloadId = entry.getKey(); + OverloadEntry overloadEntry = entry.getValue(); + CelFunctionOverload overloadImpl = overloadEntry.overload(); + + resolvedOverloads.put( + overloadId, + CelResolvedOverload.of( + overloadEntry.functionName(), + overloadId, + overloadImpl, + overloadEntry.isStrict(), + overloadEntry.argTypes())); + } + + return new DefaultDispatcher(resolvedOverloads.buildOrThrow()); + } + + private Builder() { + this.overloads = new HashMap<>(); + } } DefaultDispatcher(ImmutableMap overloads) { diff --git a/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java b/runtime/src/main/java/dev/cel/runtime/DefaultInterpreter.java index e49658190..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) { @@ -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(); } @@ -782,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) @@ -832,6 +837,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 +880,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 +921,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/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/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/FunctionBindingImpl.java b/runtime/src/main/java/dev/cel/runtime/FunctionBindingImpl.java index 48c0eb47a..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(); @@ -126,7 +153,7 @@ private DynamicDispatchBinding( } @Immutable - static final class DynamicDispatchOverload implements CelFunctionOverload { + static final class DynamicDispatchOverload implements OptimizedFunctionOverload { private final String functionName; private final ImmutableSet overloadBindings; @@ -145,7 +172,42 @@ public Object apply(Object[] args) throws CelEvaluationException { .collect(toImmutableList())); } - private DynamicDispatchOverload( + @Override + public Object apply(Object arg) throws CelEvaluationException { + for (CelFunctionBinding overload : overloadBindings) { + if (CelFunctionOverload.canHandle(arg, overload.getArgTypes(), overload.isStrict())) { + OptimizedFunctionOverload def = (OptimizedFunctionOverload) overload.getDefinition(); + return def.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())) { + OptimizedFunctionOverload def = (OptimizedFunctionOverload) overload.getDefinition(); + return def.apply(arg1, arg2); + } + } + throw new CelOverloadNotFoundException( + functionName, + overloadBindings.stream() + .map(CelFunctionBinding::getOverloadId) + .collect(toImmutableList())); + } + + 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/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/InterpreterUtil.java b/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java index f84897ac2..8c817d055 100644 --- a/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java +++ b/runtime/src/main/java/dev/cel/runtime/InterpreterUtil.java @@ -14,7 +14,9 @@ package dev.cel.runtime; +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; @@ -50,17 +52,16 @@ 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; } - 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. @@ -91,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/LiteProgramImpl.java b/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java deleted file mode 100644 index 5e57f497b..000000000 --- a/runtime/src/main/java/dev/cel/runtime/LiteProgramImpl.java +++ /dev/null @@ -1,58 +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"); - } - - 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 0e5c5cf30..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."); } @@ -162,32 +202,65 @@ public CelLiteRuntime build() { functionBindingsBuilder .buildOrThrow() .forEach( - (String overloadId, CelFunctionBinding func) -> - dispatcherBuilder.addOverload( - overloadId, func.getArgTypes(), func.isStrict(), func.getDefinition())); - - Interpreter interpreter = - new DefaultInterpreter( - TypeResolver.create(), - CelValueRuntimeTypeProvider.newInstance(celValueProvider), + (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()); + }); + + 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(); } } @@ -196,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/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/PartialVars.java b/runtime/src/main/java/dev/cel/runtime/PartialVars.java new file mode 100644 index 000000000..f195880d0 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/PartialVars.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; + +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(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); + } + + /** + * 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..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; @@ -43,4 +44,36 @@ 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; + + /** 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 d0e64429b..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; @@ -60,6 +61,58 @@ 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 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); @@ -102,6 +155,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/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/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/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/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/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/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 0a7ebbfb3..d838e8d53 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"], @@ -17,23 +18,29 @@ java_library( ":error_metadata", ":eval_and", ":eval_attribute", + ":eval_binary", + ":eval_block", ":eval_conditional", ":eval_const", ":eval_create_list", ":eval_create_map", ":eval_create_struct", + ":eval_exhaustive_and", + ":eval_exhaustive_conditional", + ":eval_exhaustive_or", ":eval_fold", + ":eval_index", ":eval_late_bound_call", + ":eval_optional_or", + ":eval_optional_or_value", + ":eval_optional_select_field", ":eval_or", ":eval_test_only", ":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", @@ -41,6 +48,7 @@ java_library( "//common:options", "//common/annotations", "//common/ast", + "//common/ast:cel_block", "//common/exceptions:overload_not_found", "//common/types", "//common/types:type_providers", @@ -54,30 +62,38 @@ java_library( "@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 = "planned_program", srcs = ["PlannedProgram.java"], + tags = [ + ], deps = [ ":error_metadata", - ":execution_frame", ":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", + "//runtime:partial_vars", "//runtime:program", "//runtime:resolved_overload", "//runtime:variable_resolver", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", ], ) @@ -85,21 +101,10 @@ java_library( name = "eval_const", srcs = ["EvalConstant.java"], deps = [ - ":execution_frame", ":planned_interpretable", + "//common/ast", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", - ], -) - -java_library( - name = "interpretable_attribute", - srcs = ["InterpretableAttribute.java"], - deps = [ - ":planned_interpretable", - ":qualifier", - "@maven//:com_google_errorprone_error_prone_annotations", ], ) @@ -108,24 +113,30 @@ java_library( 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", - ":execution_frame", ":planned_interpretable", - ":qualifier", "//common:container", + "//common/ast", "//common/exceptions:attribute_not_found", "//common/types", "//common/types:type_providers", "//common/values", - "//common/values:cel_value", + "//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", @@ -133,91 +144,118 @@ 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", - "//common/values", + ":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 = "string_qualifier", - srcs = ["StringQualifier.java"], + name = "async_gate", + srcs = ["AsyncGate.java"], + tags = [ + ], deps = [ - ":qualifier", - "//common/exceptions:attribute_not_found", - "//common/values", + "@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 = "eval_attribute", - srcs = ["EvalAttribute.java"], + name = "async_completion_coordinator", + srcs = ["AsyncCompletionCoordinator.java"], + tags = [ + ], deps = [ - ":attribute", - ":execution_frame", - ":interpretable_attribute", - ":qualifier", - "//runtime:interpretable", + ":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 = "eval_test_only", - srcs = ["EvalTestOnly.java"], + name = "activation_wrapper", + srcs = ["ActivationWrapper.java"], + deps = ["//runtime:interpretable"], +) + +java_library( + name = "error_metadata", + srcs = ["ErrorMetadata.java"], deps = [ - ":execution_frame", - ":interpretable_attribute", - ":presence_test_qualifier", - ":qualifier", - "//runtime:evaluation_exception", - "//runtime:interpretable", + "//runtime:metadata", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", ], ) java_library( - name = "eval_zero_arity", - srcs = ["EvalZeroArity.java"], + name = "eval_and", + srcs = ["EvalAnd.java"], deps = [ ":eval_helpers", - ":execution_frame", ":planned_interpretable", + "//common/ast", "//common/values", - "//runtime:evaluation_exception", + "//runtime:accumulated_unknowns", "//runtime:interpretable", - "//runtime:resolved_overload", + "@maven//:com_google_guava_guava", ], ) java_library( - name = "eval_unary", - srcs = ["EvalUnary.java"], + name = "eval_binary", + srcs = ["EvalBinary.java"], deps = [ ":eval_helpers", - ":execution_frame", ":planned_interpretable", + "//common/ast", "//common/values", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", "//runtime:resolved_overload", @@ -225,97 +263,184 @@ java_library( ) java_library( - name = "eval_var_args_call", - srcs = ["EvalVarArgsCall.java"], + name = "eval_index", + srcs = ["EvalIndex.java"], deps = [ ":eval_helpers", - ":execution_frame", ":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_late_bound_call", - srcs = ["EvalLateBoundCall.java"], + name = "eval_block", + srcs = ["EvalBlock.java"], deps = [ - ":eval_helpers", - ":execution_frame", ":planned_interpretable", - "//common/exceptions:overload_not_found", - "//common/values", + "//common/ast", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "eval_conditional", + srcs = ["EvalConditional.java"], + deps = [ + ":planned_interpretable", + "//common/ast", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", - "//runtime:resolved_overload", "@maven//:com_google_guava_guava", ], ) java_library( - name = "eval_or", - srcs = ["EvalOr.java"], + name = "eval_create_struct", + srcs = ["EvalCreateStruct.java"], deps = [ ":eval_helpers", - ":execution_frame", ":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_and", - srcs = ["EvalAnd.java"], + name = "eval_exhaustive_and", + srcs = ["EvalExhaustiveAnd.java"], deps = [ ":eval_helpers", - ":execution_frame", ":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_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_conditional", - srcs = ["EvalConditional.java"], + name = "eval_helpers", + srcs = ["EvalHelpers.java"], deps = [ - ":execution_frame", + ":localized_evaluation_exception", ":planned_interpretable", + "//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", ], ) java_library( - name = "eval_create_struct", - srcs = ["EvalCreateStruct.java"], + name = "eval_late_bound_call", + srcs = ["EvalLateBoundCall.java"], deps = [ - ":execution_frame", + ":eval_helpers", ":planned_interpretable", - "//common/types:type_providers", + "//common/ast", + "//common/exceptions:overload_not_found", "//common/values", - "//common/values:cel_value_provider", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", "//runtime:interpretable", + "//runtime:resolved_overload", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_optional_or", + srcs = ["EvalOptionalOr.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//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_create_list", - srcs = ["EvalCreateList.java"], + name = "eval_optional_or_value", + srcs = ["EvalOptionalOrValue.java"], deps = [ ":eval_helpers", - ":execution_frame", ":planned_interpretable", - "//runtime:evaluation_exception", + "//common/ast", + "//common/exceptions:overload_not_found", + "//runtime:accumulated_unknowns", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -323,14 +448,14 @@ java_library( ) java_library( - name = "eval_create_map", - srcs = ["EvalCreateMap.java"], + name = "eval_optional_select_field", + srcs = ["EvalOptionalSelectField.java"], deps = [ - ":execution_frame", - ":localized_evaluation_exception", + ":eval_helpers", ":planned_interpretable", - "//common/exceptions:duplicate_key", - "//runtime:evaluation_exception", + "//common/ast", + "//common/values", + "//runtime:accumulated_unknowns", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -338,51 +463,81 @@ java_library( ) java_library( - name = "eval_fold", - srcs = ["EvalFold.java"], + name = "eval_or", + srcs = ["EvalOr.java"], deps = [ - ":activation_wrapper", - ":execution_frame", + ":eval_helpers", ":planned_interpretable", - "//runtime:concatenated_list_view", + "//common/ast", + "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:interpretable", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "eval_test_only", + srcs = ["EvalTestOnly.java"], + deps = [ + ":attribute", + ":planned_interpretable", + "//common/ast", "//runtime:evaluation_exception", "//runtime:interpretable", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", - "@maven//:org_jspecify_jspecify", ], ) java_library( - name = "execution_frame", - srcs = ["ExecutionFrame.java"], + name = "eval_unary", + srcs = ["EvalUnary.java"], deps = [ - "//common:options", - "//common/exceptions:iteration_budget_exceeded", + ":eval_helpers", + ":planned_interpretable", + "//common/ast", + "//common/values", + "//runtime:accumulated_unknowns", "//runtime:evaluation_exception", - "//runtime:function_resolver", + "//runtime:interpretable", "//runtime:resolved_overload", ], ) java_library( - name = "eval_helpers", - srcs = ["EvalHelpers.java"], + name = "eval_var_args_call", + srcs = ["EvalVarArgsCall.java"], deps = [ - ":execution_frame", - ":localized_evaluation_exception", + ":eval_helpers", ":planned_interpretable", - "//common:error_codes", - "//common/exceptions:runtime_exception", + "//common/ast", + "//common/values", + "//runtime:accumulated_unknowns", + "//runtime:evaluation_exception", + "//runtime:interpretable", + "//runtime:resolved_overload", + ], +) + +java_library( + name = "eval_zero_arity", + srcs = ["EvalZeroArity.java"], + deps = [ + ":eval_helpers", + ":planned_interpretable", + "//common/ast", "//common/values", - "//common/values:cel_value", "//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"], @@ -393,22 +548,550 @@ java_library( ) java_library( - name = "error_metadata", + name = "planned_interpretable", + srcs = [ + "BlockMemoizer.java", + "ExecutionFrame.java", + "PlannedInterpretable.java", + ], + 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:interpreter_util", + "//runtime:partial_vars", + "//runtime:resolved_overload", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + ], +) + +cel_android_library( + name = "program_planner_android", + srcs = ["ProgramPlanner.java"], + tags = [ + ], + deps = [ + ":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_index_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", + "//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//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "planned_program_android", + srcs = ["PlannedProgram.java"], + deps = [ + ":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//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "eval_const_android", + srcs = ["EvalConstant.java"], + deps = [ + ":planned_interpretable_android", + "//common/ast:ast_android", + "//runtime:interpretable_android", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +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//:com_google_guava_guava", + "@maven_android//:com_google_guava_guava", ], ) -java_library( - name = "planned_interpretable", - srcs = ["PlannedInterpretable.java"], +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 = [ - ":execution_frame", + ":eval_helpers_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/values:values_android", + "//runtime:accumulated_unknowns_android", "//runtime:evaluation_exception", - "//runtime:interpretable", + "//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", "@maven//:com_google_errorprone_error_prone_annotations", ], ) + +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_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", + ], +) + +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_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", + ], +) + +cel_android_library( + name = "eval_fold_android", + srcs = ["EvalFold.java"], + deps = [ + ":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_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", + ], +) + +cel_android_library( + name = "eval_helpers_android", + srcs = ["EvalHelpers.java"], + deps = [ + ":localized_evaluation_exception_android", + ":planned_interpretable_android", + "//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", + ], +) + +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", + "//runtime/src/main/java/dev/cel/runtime:accumulated_unknowns_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/BlockMemoizer.java b/runtime/src/main/java/dev/cel/runtime/planner/BlockMemoizer.java new file mode 100644 index 000000000..80a0a5de0 --- /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].expr().id()); + slotVals[idx] = localizedException; + throw localizedException; + } catch (RuntimeException e) { + slotVals[idx] = e; + throw 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 b09191e9f..11da26a50 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java @@ -1,63 +1,77 @@ -// 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 - throw 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.ast.CelExpr; +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 + Object evalInternal(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) { + // 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 { + errorValue = + ErrorValue.create( + arg.expr().id(), + 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(CelExpr expr, PlannedInterpretable[] args) { + return new EvalAnd(expr, args); + } + + 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 fdd7ad2a3..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(resolver, frame); + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { + Object resolved = attr.resolve(expr().id(), resolver, frame); if (resolved instanceof MissingAttribute) { - ((MissingAttribute) resolved).resolve(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 new file mode 100644 index 000000000..1713195ab --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalBinary.java @@ -0,0 +1,78 @@ +// 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.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; + +final class EvalBinary extends PlannedInterpretable { + + private final String functionName; + private final CelResolvedOverload resolvedOverload; + private final PlannedInterpretable arg1; + private final PlannedInterpretable arg2; + private final CelValueConverter celValueConverter; + + @Override + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + boolean isStrict = resolvedOverload.isStrict(); + Object argVal1 = + isStrict ? evalStrictly(arg1, resolver, frame) : evalNonstrictly(arg1, resolver, frame); + Object argVal2 = + 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( + functionName, resolvedOverload, celValueConverter, argVal1, argVal2); + } + + static EvalBinary create( + CelExpr expr, + String functionName, + CelResolvedOverload resolvedOverload, + PlannedInterpretable arg1, + PlannedInterpretable arg2, + CelValueConverter celValueConverter) { + return new EvalBinary(expr, functionName, resolvedOverload, arg1, arg2, celValueConverter); + } + + private EvalBinary( + CelExpr expr, + String functionName, + CelResolvedOverload resolvedOverload, + PlannedInterpretable arg1, + PlannedInterpretable arg2, + CelValueConverter celValueConverter) { + super(expr); + this.functionName = functionName; + this.resolvedOverload = resolvedOverload; + this.arg1 = arg1; + this.arg2 = arg2; + this.celValueConverter = celValueConverter; + } +} 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..eed8791d4 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalBlock.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 com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelExpr; +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( + CelExpr expr, PlannedInterpretable[] slotExprs, PlannedInterpretable resultExpr) { + return new EvalBlock(expr, slotExprs, resultExpr); + } + + private EvalBlock( + CelExpr expr, PlannedInterpretable[] slotExprs, PlannedInterpretable resultExpr) { + super(expr); + this.slotExprs = slotExprs; + this.resultExpr = resultExpr; + } + + @Override + Object evalInternal(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(CelExpr expr, int slotIndex) { + return new EvalBlockSlot(expr, slotIndex); + } + + private EvalBlockSlot(CelExpr expr, int slotIndex) { + super(expr); + this.slotIndex = slotIndex; + } + + @Override + 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 74482d629..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,8 @@ 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; @@ -24,12 +26,14 @@ 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]; - // 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)); @@ -43,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 389a21a82..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,31 +16,63 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; -import dev.cel.runtime.CelEvaluationException; +import dev.cel.common.ast.CelExpr; +import dev.cel.runtime.AccumulatedUnknowns; 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 { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(values.length); - for (PlannedInterpretable value : values) { - builder.add(EvalHelpers.evalStrictly(value, resolver, frame)); + 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( + 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); + } + + if (unknowns != null) { + return unknowns; } + return builder.build(); } - static EvalCreateList create(long exprId, PlannedInterpretable[] values) { - return new EvalCreateList(exprId, values); + static EvalCreateList create(CelExpr expr, PlannedInterpretable[] values, boolean[] isOptional) { + return new EvalCreateList(expr, values, isOptional); } - private EvalCreateList(long exprId, PlannedInterpretable[] values) { - 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 4c5a1f0bf..8d34c10d0 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java @@ -17,11 +17,16 @@ 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.ast.CelExpr; 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; +import java.util.Optional; @Immutable final class EvalCreateMap extends PlannedInterpretable { @@ -34,34 +39,104 @@ 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 { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(keys.length); HashSet keysSeen = Sets.newHashSetWithExpectedSize(keys.length); + AccumulatedUnknowns unknowns = null; 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 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.expr().id()); + } - if (!keysSeen.add(key)) { - throw new LocalizedEvaluationException(CelDuplicateKeyException.of(key), keys[i].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()); + } + } + + if (isDuplicate) { + throw new LocalizedEvaluationException( + CelDuplicateKeyException.of(key), keyInterpretable.expr().id()); + } } - builder.put(key, 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)) { + 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(); + } + + builder.put(key, val); + } + + if (unknowns != null) { + return unknowns; } return builder.buildOrThrow(); } static EvalCreateMap create( - long exprId, PlannedInterpretable[] keys, PlannedInterpretable[] values) { - return new EvalCreateMap(exprId, keys, values); + CelExpr expr, + PlannedInterpretable[] keys, + PlannedInterpretable[] values, + boolean[] isOptional) { + return new EvalCreateMap(expr, keys, values, isOptional); } - private EvalCreateMap(long exprId, PlannedInterpretable[] keys, PlannedInterpretable[] values) { - super(exprId); + private EvalCreateMap( + CelExpr expr, + PlannedInterpretable[] keys, + PlannedInterpretable[] values, + boolean[] isOptional) { + super(expr); 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..a2e8a9da6 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java @@ -14,15 +14,17 @@ package dev.cel.runtime.planner; +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.CelEvaluationException; +import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.GlobalResolver; import java.util.Collections; -import java.util.HashMap; import java.util.Map; +import java.util.Optional; @Immutable final class EvalCreateStruct extends PlannedInterpretable { @@ -38,47 +40,82 @@ 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<>(); + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { + 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 '%s' from non-optional value" + " %s", + keys[i], 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); } + if (unknowns != null) { + return unknowns; + } + // Either a primitive (wrappers) or a struct is produced Object value = valueProvider .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; } static EvalCreateStruct create( - long exprId, + CelExpr expr, CelValueProvider valueProvider, CelType structType, String[] keys, - PlannedInterpretable[] values) { - return new EvalCreateStruct(exprId, valueProvider, structType, keys, values); + PlannedInterpretable[] values, + boolean[] isOptional) { + return new EvalCreateStruct(expr, valueProvider, structType, keys, values, isOptional); } private EvalCreateStruct( - long exprId, + CelExpr expr, CelValueProvider valueProvider, CelType structType, String[] keys, - PlannedInterpretable[] values) { - super(exprId); + PlannedInterpretable[] values, + boolean[] isOptional) { + super(expr); 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/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/EvalFold.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java index 3545ee4f7..1cbe807c2 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalFold.java @@ -15,7 +15,12 @@ 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.ast.CelExpr; +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; import dev.cel.runtime.GlobalResolver; @@ -36,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, @@ -46,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, @@ -59,7 +64,7 @@ private EvalFold( PlannedInterpretable condition, PlannedInterpretable loopStep, PlannedInterpretable result) { - super(exprId); + super(expr); this.accuVar = accuVar; this.accuInit = accuInit; this.iterVar = iterVar; @@ -71,10 +76,12 @@ 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); - Folder folder = new Folder(resolver, accuVar, iterVar, iterVar2); - folder.accuVal = maybeWrapAccumulator(accuInit.eval(folder, frame)); + if (iterRangeRaw instanceof AccumulatedUnknowns) { + return iterRangeRaw; + } + Folder folder = new Folder(resolver, frame, accuInit, accuVar, iterVar, iterVar2); Object result; if (iterRangeRaw instanceof Map) { @@ -98,13 +105,24 @@ 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); } folder.accuVal = loopStep.eval(folder, frame); + folder.initialized = true; } + folder.computeResult = true; return result.eval(folder, frame); } @@ -121,22 +139,35 @@ 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) { - return result.eval(folder, frame); + folder.computeResult = true; + return maybeUnwrapAccumulator(result.eval(folder, frame)); } folder.accuVal = loopStep.eval(folder, frame); + folder.initialized = true; index++; } - return result.eval(folder, frame); + folder.computeResult = true; + 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; } @@ -144,13 +175,16 @@ 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; } 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; @@ -158,9 +192,19 @@ private static class Folder implements ActivationWrapper { private Object iterVarVal; private Object iterVar2Val; private Object accuVal; + private boolean initialized = false; + private boolean computeResult = false; - private Folder(GlobalResolver resolver, String accuVar, String iterVar, String iterVar2) { + 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; @@ -171,21 +215,42 @@ 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)) { + 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()); + } + } } 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..1b8d61234 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java @@ -1,81 +1,129 @@ -// 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.CelValue; -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); - Object runtimeValue = valueConverter.toRuntimeValue(result); - if (runtimeValue instanceof CelValue) { - return valueConverter.unwrap((CelValue) runtimeValue); - } - - return runtimeValue; - } 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.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 { + + 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.expr().id(), 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.expr().id()); + } catch (Exception e) { + // Wrap generic exceptions with location + throw new LocalizedEvaluationException( + e, CelErrorCode.INTERNAL_ERROR, interpretable.expr().id()); + } + } + + static Object dispatch( + String functionName, + CelResolvedOverload overload, + CelValueConverter valueConverter, + Object[] args) + throws CelEvaluationException { + try { + Object result = overload.invoke(args); + return convertAndAdaptResult(valueConverter, result); + } catch (RuntimeException e) { + throw handleDispatchException(e, overload, args); + } + } + + static Object dispatch( + String functionName, + CelResolvedOverload overload, + CelValueConverter valueConverter, + Object arg) + throws CelEvaluationException { + try { + Object result = overload.invoke(arg); + return convertAndAdaptResult(valueConverter, result); + } catch (RuntimeException e) { + throw handleDispatchException(e, overload, arg); + } + } + + static Object dispatch( + String functionName, + CelResolvedOverload overload, + CelValueConverter valueConverter, + Object arg1, + Object arg2) + throws CelEvaluationException { + try { + Object result = overload.invoke(arg1, arg2); + 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) { + // 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.getFunctionName(), Joiner.on(", ").join(args)), + e); + } + + private EvalHelpers() {} +} 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/EvalLateBoundCall.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalLateBoundCall.java index a22ba8e94..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,8 +17,10 @@ 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; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.GlobalResolver; @@ -34,12 +36,19 @@ 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++) { 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 = @@ -47,25 +56,25 @@ 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( - 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 new file mode 100644 index 000000000..37e3a8ccb --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOr.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.runtime.planner; + +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; +import java.util.Optional; + +@Immutable +final class EvalOptionalOr extends PlannedInterpretable { + private final PlannedInterpretable lhs; + private final PlannedInterpretable rhs; + + @Override + Object evalInternal(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"); + } + + Optional optionalLhs = (Optional) lhsValue; + if (optionalLhs.isPresent()) { + return optionalLhs; + } + + return EvalHelpers.evalStrictly(rhs, resolver, frame); + } + + static EvalOptionalOr create(CelExpr expr, PlannedInterpretable lhs, PlannedInterpretable rhs) { + return new EvalOptionalOr(expr, lhs, rhs); + } + + 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 new file mode 100644 index 000000000..b64c6d433 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalOrValue.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.runtime.planner; + +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; +import java.util.Optional; + +@Immutable +final class EvalOptionalOrValue extends PlannedInterpretable { + private final PlannedInterpretable lhs; + private final PlannedInterpretable rhs; + + @Override + Object evalInternal(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"); + } + + Optional optionalLhs = (Optional) lhsValue; + if (optionalLhs.isPresent()) { + return optionalLhs.get(); + } + + return EvalHelpers.evalStrictly(rhs, resolver, frame); + } + + static EvalOptionalOrValue create( + CelExpr expr, PlannedInterpretable lhs, PlannedInterpretable rhs) { + return new EvalOptionalOrValue(expr, lhs, rhs); + } + + 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 new file mode 100644 index 000000000..4122a6e8e --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java @@ -0,0 +1,99 @@ +// 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.ast.CelExpr; +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; + +@Immutable +final class EvalOptionalSelectField extends PlannedInterpretable { + private final PlannedInterpretable operand; + private final PlannedInterpretable selectAttribute; + private final String field; + private final CelValueConverter celValueConverter; + + @Override + Object evalInternal(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); + if (runtimeOperandValue instanceof AccumulatedUnknowns) { + return runtimeOperandValue; + } + + 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; + } + + if (resultValue instanceof AccumulatedUnknowns) { + return resultValue; + } + + return Optional.of(resultValue); + } + + static EvalOptionalSelectField create( + CelExpr expr, + PlannedInterpretable operand, + String field, + PlannedInterpretable selectAttribute, + CelValueConverter celValueConverter) { + return new EvalOptionalSelectField(expr, operand, field, selectAttribute, celValueConverter); + } + + private EvalOptionalSelectField( + CelExpr expr, + PlannedInterpretable operand, + String field, + PlannedInterpretable selectAttribute, + CelValueConverter celValueConverter) { + super(expr); + 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/EvalOr.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java index 8c8f5954d..849b6e7b4 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java @@ -1,63 +1,77 @@ -// 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 - throw 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.ast.CelExpr; +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 + Object evalInternal(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) { + // 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 { + errorValue = + ErrorValue.create( + arg.expr().id(), + 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(CelExpr expr, PlannedInterpretable[] args) { + return new EvalOr(expr, args); + } + + 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 c715ff032..a612da9e5 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java @@ -17,42 +17,48 @@ 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; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.GlobalResolver; final class EvalUnary extends PlannedInterpretable { + private final String functionName; private final CelResolvedOverload resolvedOverload; private final PlannedInterpretable arg; private final CelValueConverter celValueConverter; @Override - public Object eval(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + boolean isStrict = resolvedOverload.isStrict(); Object argVal = - resolvedOverload.isStrict() - ? evalStrictly(arg, resolver, frame) - : evalNonstrictly(arg, resolver, frame); - Object[] arguments = new Object[] {argVal}; - - return EvalHelpers.dispatch(resolvedOverload, celValueConverter, arguments); + isStrict ? evalStrictly(arg, resolver, frame) : evalNonstrictly(arg, resolver, frame); + if (isStrict && argVal instanceof AccumulatedUnknowns) { + return argVal; + } + return EvalHelpers.dispatch(functionName, resolvedOverload, celValueConverter, argVal); } static EvalUnary create( - long exprId, + CelExpr expr, + String functionName, CelResolvedOverload resolvedOverload, PlannedInterpretable arg, CelValueConverter celValueConverter) { - return new EvalUnary(exprId, 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; 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 9f14f8bf9..8046710e9 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java @@ -17,13 +17,16 @@ 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; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.GlobalResolver; final class EvalVarArgsCall extends PlannedInterpretable { + private final String functionName; private final CelResolvedOverload resolvedOverload; @SuppressWarnings("Immutable") @@ -32,33 +35,42 @@ 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 { + 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); + isStrict ? evalStrictly(arg, resolver, frame) : evalNonstrictly(arg, resolver, frame); + if (isStrict) { + unknowns = AccumulatedUnknowns.maybeMerge(unknowns, argVals[i]); + } + } + if (unknowns != null) { + return unknowns; } - return EvalHelpers.dispatch(resolvedOverload, celValueConverter, argVals); + return EvalHelpers.dispatch(functionName, resolvedOverload, celValueConverter, argVals); } static EvalVarArgsCall create( - long exprId, + CelExpr expr, + String functionName, CelResolvedOverload resolvedOverload, PlannedInterpretable[] args, CelValueConverter celValueConverter) { - return new EvalVarArgsCall(exprId, 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; 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..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; @@ -22,22 +23,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); + Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) throws CelEvaluationException { + return EvalHelpers.dispatch(functionName, resolvedOverload, celValueConverter, EMPTY_ARRAY); } static EvalZeroArity create( - long exprId, CelResolvedOverload resolvedOverload, CelValueConverter celValueConverter) { - return new EvalZeroArity(exprId, resolvedOverload, celValueConverter); + CelExpr expr, + String functionName, + CelResolvedOverload resolvedOverload, + CelValueConverter celValueConverter) { + return new EvalZeroArity(expr, functionName, resolvedOverload, celValueConverter); } private EvalZeroArity( - long exprId, CelResolvedOverload resolvedOverload, CelValueConverter celValueConverter) { - super(exprId); + CelExpr expr, + String functionName, + CelResolvedOverload resolvedOverload, + CelValueConverter celValueConverter) { + 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 80ee4b318..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,17 +17,23 @@ 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 { private final int comprehensionIterationLimit; private final CelFunctionResolver functionResolver; + private final PartialVars partialVars; + private final @Nullable CelEvaluationListener listener; private int iterationCount; + private BlockMemoizer blockMemoizer; Optional findOverload( String functionName, Collection overloadIds, Object[] args) @@ -47,12 +53,42 @@ void incrementIterations() { } } - static ExecutionFrame create(CelFunctionResolver functionResolver, CelOptions celOptions) { - return new ExecutionFrame(functionResolver, celOptions.comprehensionMaxIterations()); + 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, + CelOptions celOptions, + @Nullable PartialVars partialVars, + @Nullable CelEvaluationListener listener) { + return new ExecutionFrame( + functionResolver, celOptions.comprehensionMaxIterations(), partialVars, listener); + } + + Optional partialVars() { + return Optional.ofNullable(partialVars); + } + + @Nullable CelEvaluationListener getListener() { + return listener; } - private ExecutionFrame(CelFunctionResolver functionResolver, int limit) { + 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/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..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,17 +15,19 @@ 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; 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 6bdf0c072..01673923d 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; @@ -22,16 +23,22 @@ 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.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; @@ -41,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 @@ -54,21 +61,38 @@ 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); - if (value != null) { - if (!qualifiers.isEmpty()) { - return applyQualifiers(value, celValueConverter, qualifiers); - } else { - return value; + value = InterpreterUtil.maybeAdaptToAccumulatedUnknowns(value); + + PartialVars partialVars = frame.partialVars().orElse(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++) { + attr = attr.qualify(CelAttribute.Qualifier.fromGeneric(qualifiers.get(i).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); + } + // Attempt to resolve the qualify type name if the name is not a variable identifier value = findIdent(name); if (value != null) { @@ -76,7 +100,7 @@ public Object resolve(GlobalResolver ctx, ExecutionFrame frame) { } } - return MissingAttribute.newMissingAttribute(namespacedNames); + return MissingAttribute.newMissingAttribute(candidateAttributes.keySet()); } private @Nullable Object findIdent(String name) { @@ -127,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(); @@ -139,57 +174,74 @@ public NamespacedAttribute addQualifier(Qualifier qualifier) { return new NamespacedAttribute( typeProvider, celValueConverter, - namespacedNames, + candidateAttributes, disambiguateNames, - ImmutableList.builder().addAll(qualifiers).add(qualifier).build()); + ImmutableList.builderWithExpectedSize(qualifiers.size() + 1) + .addAll(qualifiers) + .add(qualifier) + .build()); } private static Object applyQualifiers( Object value, CelValueConverter celValueConverter, ImmutableList qualifiers) { + if (value instanceof AccumulatedUnknowns) { + return value; + } 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++) { + Qualifier element = qualifiers.get(i); + obj = element.qualify(obj); + obj = celValueConverter.toRuntimeValue(obj); } - if (obj instanceof CelValue) { - obj = celValueConverter.unwrap((CelValue) obj); - } + return celValueConverter.maybeUnwrap(obj); + } - return 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/PlannedInterpretable.java b/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java index 6f3a9d7ff..6bdeaf1df 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,34 @@ 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; +import dev.cel.runtime.InterpreterUtil; @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, InterpreterUtil.maybeAdaptToCelUnknownSet(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 8b419cab2..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,25 +15,37 @@ 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; 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; 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; 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() { @@ -50,60 +62,137 @@ 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); + 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); + 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); + return evalOrThrow( + 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); + interpretable(), + (name) -> resolver.find(name).orElse(null), + EMPTY_FUNCTION_RESOLVER, + /* partialVars= */ null, + /* listener= */ 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, + /* partialVars= */ null, + /* listener= */ null); + } + + @Override + public Object eval(PartialVars partialVars) throws CelEvaluationException { + return evalOrThrow( + interpretable(), + (name) -> partialVars.resolver().find(name).orElse(null), + EMPTY_FUNCTION_RESOLVER, + partialVars, + /* 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."); } - private Object evalOrThrow( + @Override + public ListenableFuture evalAsync(PartialVars partialVars) { + throw new UnsupportedOperationException("evalAsync is not supported by PlannedProgram."); + } + + public Object evalOrThrow( PlannedInterpretable interpretable, GlobalResolver resolver, - CelFunctionResolver functionResolver) + CelFunctionResolver functionResolver, + @Nullable PartialVars partialVars, + @Nullable CelEvaluationListener listener) throws CelEvaluationException { try { - ExecutionFrame frame = ExecutionFrame.create(functionResolver, options()); + ExecutionFrame frame = + ExecutionFrame.create(functionResolver, options(), partialVars, listener); 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); + 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/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/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index fc22d4f10..23a6e5dec 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; @@ -26,6 +28,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; @@ -52,6 +55,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 @@ -78,7 +82,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()) @@ -92,7 +100,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: @@ -122,35 +130,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()); } @@ -159,24 +166,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, 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 @@ -192,14 +204,40 @@ private PlannedInterpretable planCheckedIdent( () -> new NoSuchElementException( "Reference to an undefined type: " + identRef.name())); - return EvalConstant.create(id, identType); + return EvalConstant.create(expr, identType); } - return EvalAttribute.create(id, attributeFactory.newAbsoluteAttribute(identRef.name())); + String identName = identRef.name(); + PlannedInterpretable blockSlot = maybeInterceptBlockSlot(expr, identName).orElse(null); + if (blockSlot != null) { + return blockSlot; + } + + return EvalAttribute.create(expr, attributeFactory.newAbsoluteAttribute(identRef.name())); + } + + private Optional maybeInterceptBlockSlot(CelExpr expr, 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(expr, 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(); + CelExpr target = resolvedFunction.target().orElse(null); int argCount = expr.call().args().size(); if (target != null) { @@ -219,16 +257,21 @@ 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) { case LOGICAL_OR: - return EvalOr.create(expr.id(), evaluatedArgs); + return options.enableShortCircuiting() + ? EvalOr.create(expr, evaluatedArgs) + : EvalExhaustiveOr.create(expr, evaluatedArgs); case LOGICAL_AND: - return EvalAnd.create(expr.id(), evaluatedArgs); + return options.enableShortCircuiting() + ? EvalAnd.create(expr, evaluatedArgs) + : EvalExhaustiveAnd.create(expr, evaluatedArgs); case CONDITIONAL: - return EvalConditional.create(expr.id(), evaluatedArgs); + return options.enableShortCircuiting() + ? EvalConditional.create(expr, evaluatedArgs) + : EvalExhaustiveConditional.create(expr, evaluatedArgs); default: // fall-through } @@ -244,10 +287,21 @@ 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)) { + 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); @@ -255,45 +309,132 @@ 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()); } return EvalLateBoundCall.create( - expr.id(), functionName, overloadIds, evaluatedArgs, celValueConverter); + expr, functionName, overloadIds, evaluatedArgs, celValueConverter); } switch (argCount) { case 0: - return EvalZeroArity.create(expr.id(), resolvedOverload, celValueConverter); + return EvalZeroArity.create(expr, functionName, resolvedOverload, celValueConverter); case 1: - return EvalUnary.create(expr.id(), resolvedOverload, evaluatedArgs[0], celValueConverter); + 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, + resolvedOverload, + evaluatedArgs[0], + evaluatedArgs[1], + celValueConverter); default: return EvalVarArgsCall.create( - expr.id(), resolvedOverload, evaluatedArgs, celValueConverter); + expr, functionName, resolvedOverload, evaluatedArgs, celValueConverter); + } + } + + private PlannedInterpretable planBlock(CelBlock celBlock, PlannerContext ctx) { + ImmutableList indices = celBlock.indices(); + + PlannedInterpretable[] slotExprs = new PlannedInterpretable[indices.size()]; + for (int i = 0; i < slotExprs.length; i++) { + slotExprs[i] = plan(indices.get(i), ctx); + } + PlannedInterpretable resultExpr = plan(celBlock.result(), ctx); + return EvalBlock.create(celBlock.expr(), slotExprs, resultExpr); + } + + /** + * 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, evaluatedArgs[0], evaluatedArgs[1])); + } + + return Optional.empty(); + case "orValue": + if (overloadId.isEmpty() || overloadId.equals("optional_orValue_value")) { + return Optional.of(EvalOptionalOrValue.create(expr, evaluatedArgs[0], evaluatedArgs[1])); + } + + return Optional.empty(); + default: + break; + } + + if (functionName.equals(Operator.OPTIONAL_SELECT.getFunction())) { + 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, attributeFactory.newRelativeAttribute(evaluatedArgs[0])); + } + Qualifier qualifier = StringQualifier.create(field); + PlannedInterpretable selectAttribute = attribute.addQualifier(expr, qualifier); + + return Optional.of( + EvalOptionalSelectField.create( + expr, evaluatedArgs[0], field, selectAttribute, celValueConverter)); + } + + return Optional.empty(); } 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()]; 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, 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 +442,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, values, isOptional); } private PlannedInterpretable planCreateMap(CelExpr celExpr, PlannerContext ctx) { @@ -310,14 +456,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, keys, values, isOptional); } private PlannedInterpretable planComprehension(CelExpr expr, PlannerContext ctx) { @@ -338,7 +486,7 @@ private PlannedInterpretable planComprehension(CelExpr expr, PlannerContext ctx) ctx.popLocalVars(comprehension.accuVar()); return EvalFold.create( - expr.id(), + expr, comprehension.accuVar(), accuInit, comprehension.iterVar(), @@ -413,7 +561,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); @@ -423,9 +581,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())); @@ -437,6 +593,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()) { @@ -484,16 +646,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) { @@ -526,14 +695,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); } } 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..38f733c79 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java @@ -16,8 +16,8 @@ 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.AccumulatedUnknowns; import dev.cel.runtime.GlobalResolver; /** @@ -32,19 +32,22 @@ 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); + // Avoid enhanced for loop to prevent UnmodifiableIterator from being allocated + for (int i = 0; i < qualifiers.size(); i++) { + Qualifier element = qualifiers.get(i); + obj = element.qualify(obj); + obj = celValueConverter.toRuntimeValue(obj); } - // TODO: Handle unknowns - if (obj instanceof CelValue) { - obj = celValueConverter.unwrap((CelValue) obj); - } - return obj; + return celValueConverter.maybeUnwrap(obj); } @Override 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..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,11 +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; @@ -31,21 +34,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); 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( 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/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/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel index 569d7372d..f898b66fe 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, @@ -18,6 +19,7 @@ java_library( ["*.java"], # keep sorted exclude = [ + "AbstractPlannerInterpreterTest.java", "CelLiteInterpreterTest.java", "InterpreterTest.java", "PlannerInterpreterTest.java", @@ -41,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", @@ -54,7 +57,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", @@ -68,12 +70,13 @@ 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", + "//runtime:partial_vars", "//runtime:proto_message_activation_factory", "//runtime:proto_message_runtime_equality", "//runtime:proto_message_runtime_helpers", @@ -88,6 +91,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", @@ -124,13 +128,35 @@ 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, srcs = [ "PlannerInterpreterTest.java", ], + resources = [ + "//runtime/testdata", + ], deps = [ + ":abstract_planner_interpreter_test", "//common:cel_ast", "//common:compiler_common", "//common:container", @@ -138,8 +164,7 @@ java_library( "//common/types:type_providers", "//extensions", "//runtime", - "//runtime:runtime_planner_impl", - "//testing:base_interpreter_test", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", ], @@ -165,6 +190,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", @@ -188,7 +214,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/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/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/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/CelLiteInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/CelLiteInterpreterTest.java index b3a1f2efa..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()); } @@ -54,6 +56,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/java/dev/cel/runtime/CelLiteRuntimeAndroidTest.java b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeAndroidTest.java index 54ce24417..6c54ce486 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 @@ -191,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); @@ -204,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); @@ -285,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() @@ -306,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); } @@ -515,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( @@ -523,7 +539,8 @@ public void eval_protoMessage_deepTraversalReturnsRepeatedStrings(String checked .setPayload( TestAllTypes.newBuilder() .addAllRepeatedString(data) - .build())))); + .build())) + .build())); assertThat(result).isEqualTo(data); } @@ -712,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 4ffe0941c..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; @@ -59,8 +61,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; @@ -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/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/CelRuntimeLegacyImplTest.java b/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java index fa3b5f4ae..5bef0c61e 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java @@ -15,18 +15,20 @@ 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; -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; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -39,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); } @@ -108,13 +110,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 +123,45 @@ public void toRuntimeBuilder_optionalProperties() { assertThat(newRuntimeBuilder.customTypeFactory).isEqualTo(customTypeFactory); assertThat(newRuntimeBuilder.overriddenStandardFunctions) .isEqualTo(overriddenStandardFunctions); - assertThat(newRuntimeBuilder.celValueProvider).isEqualTo(noOpValueProvider); + } + + @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)); } } diff --git a/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java b/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java index c7f142602..d7247f8a1 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelRuntimeTest.java @@ -17,6 +17,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +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; @@ -35,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; @@ -51,6 +55,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 +66,8 @@ @RunWith(TestParameterInjector.class) public class CelRuntimeTest { + @TestParameter private CelRuntimeFlavor runtimeFlavor; + @Test public void evaluate_anyPackedEqualityUsingProtoDifferencer_success() throws Exception { Cel cel = @@ -100,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) @@ -273,7 +280,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 +305,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 +320,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 +338,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 +359,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 +381,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 +399,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 +415,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 +431,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 +454,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(); @@ -470,12 +481,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(); @@ -501,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); @@ -509,13 +530,19 @@ 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(); - 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"); @@ -536,15 +563,21 @@ 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(); - 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(unknownResult).isInstanceOf(CelUnknownSet.class); assertThat(branchResults.build()).containsExactly(true, true, unknownResult); } @@ -561,12 +594,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(); @@ -596,15 +639,21 @@ 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(); - 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(unknownResult).isInstanceOf(CelUnknownSet.class); assertThat(branchResults.build()).containsExactly(false, false, unknownResult); } @@ -619,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); @@ -627,13 +676,19 @@ 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(); - 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"); @@ -649,8 +704,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(); @@ -674,15 +734,21 @@ 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(); - 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(unknownResult).isInstanceOf(CelUnknownSet.class); assertThat(branchResults.build()).containsExactly(false, unknownResult, true); } @@ -705,12 +771,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(); @@ -725,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 = @@ -732,11 +857,238 @@ 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"); + } + + @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/CelStandardFunctionsTest.java b/runtime/src/test/java/dev/cel/runtime/CelStandardFunctionsTest.java index d85ef7424..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(); @@ -224,7 +229,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/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..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; @@ -77,22 +78,31 @@ 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(), 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/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/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(); + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java index 7518951c7..4d93c6e07 100644 --- a/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java +++ b/runtime/src/test/java/dev/cel/runtime/PlannerInterpreterTest.java @@ -21,24 +21,24 @@ import dev.cel.common.CelOptions; import dev.cel.common.CelValidationException; import dev.cel.common.types.CelTypeProvider; +import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.extensions.CelExtensions; -import dev.cel.testing.BaseInterpreterTest; 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; @Override protected CelRuntimeBuilder newBaseRuntimeBuilder(CelOptions celOptions) { - return CelRuntimeImpl.newBuilder() + return CelRuntimeFactory.plannerRuntimeBuilder() .addLateBoundFunctions("record") - // CEL-Internal-2 .setOptions(celOptions) .addLibraries(CelExtensions.optional()) - .addFileTypes(TEST_FILE_DESCRIPTORS); + .addFileTypes(TEST_FILE_DESCRIPTORS) + .addMessageTypes(TestAllTypes.getDescriptor()); } @Override @@ -70,27 +70,4 @@ protected CelAbstractSyntaxTree prepareTest(CelTypeProvider typeProvider) { return null; } } - - @Override - public void unknownField() { - // TODO: Unknown support not implemented yet - skipBaselineVerification(); - } - - @Override - public void unknownResultSet() { - // TODO: Unknown support not implemented yet - 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(); - } - } } 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/async/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel index 99e9fd59c..29f08eb74 100644 --- a/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/async/BUILD.bazel @@ -1,22 +1,24 @@ 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", 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", 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/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 fb05b0b31..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,13 +40,20 @@ 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", + "//runtime:partial_vars", "//runtime:program", "//runtime:runtime_equality", "//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", "@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 33500f217..a3b1e3596 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; @@ -65,13 +66,18 @@ 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.InternalCelFunctionBinding; +import dev.cel.runtime.PartialVars; import dev.cel.runtime.Program; import dev.cel.runtime.RuntimeEquality; import dev.cel.runtime.RuntimeHelpers; @@ -175,7 +181,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)); @@ -203,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( @@ -240,6 +262,7 @@ private static void addBindingsToDispatcher( overloadBindings.forEach( overload -> builder.addOverload( + ((InternalCelFunctionBinding) overload).getFunctionName(), overload.getOverloadId(), overload.getArgTypes(), overload.isStrict(), @@ -316,6 +339,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); @@ -390,6 +442,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{}"); @@ -450,15 +513,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"); } @@ -514,17 +573,108 @@ 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)"); 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 = @@ -906,6 +1056,199 @@ 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 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 = @@ -1003,6 +1346,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); } @@ -1055,7 +1515,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), 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/runtime/src/test/resources/nullAssignability.baseline b/runtime/src/test/resources/nullAssignability.baseline new file mode 100644 index 000000000..b60f434ea --- /dev/null +++ b/runtime/src/test/resources/nullAssignability.baseline @@ -0,0 +1,64 @@ +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 + +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/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 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..7885e9da1 --- /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: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") +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:31: Text '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..c5e8867db --- /dev/null +++ b/runtime/src/test/resources/planner_unknownResultSet_success.baseline @@ -0,0 +1,473 @@ +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]} + +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/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/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( 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: diff --git a/testing/BUILD.bazel b/testing/BUILD.bazel index c1b2a92b4..cc389fed1 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"], @@ -40,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/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/main/java/dev/cel/testing/BUILD.bazel b/testing/src/main/java/dev/cel/testing/BUILD.bazel index f2480a034..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", ], ) @@ -87,10 +91,11 @@ java_library( "//common/types:message_type_provider", "//common/types:type_providers", "//common/values:cel_byte_string", + "//extensions", "//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", @@ -101,6 +106,15 @@ 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", + ], +) + +java_library( + name = "cel_runtime_flavor", + srcs = ["CelRuntimeFlavor.java"], + tags = [ + ], + deps = [ + "//bundle:cel", ], ) diff --git a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java index bc67e8218..bda56a19e 100644 --- a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java +++ b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java @@ -74,7 +74,9 @@ 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; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelLateFunctionBindings; @@ -83,6 +85,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; @@ -112,7 +115,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) @@ -152,7 +154,7 @@ protected void prepareCompiler(CelTypeProvider typeProvider) { this.celCompiler = celCompiler .toCompilerBuilder() - .addLibraries(CelOptionalLibrary.INSTANCE) + .addLibraries(CelOptionalLibrary.INSTANCE, CelExtensions.bindings()) .setOptions(celOptions) .build(); } @@ -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)); @@ -1850,6 +1871,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() @@ -2122,6 +2160,58 @@ 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(); + + 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 public void longComprehension() { ImmutableList l = LongStream.range(0L, 1000L).boxed().collect(toImmutableList()); @@ -2463,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(", "); } @@ -2487,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); + } } } 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/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/main/java/dev/cel/testing/CelRuntimeFlavor.java b/testing/src/main/java/dev/cel/testing/CelRuntimeFlavor.java new file mode 100644 index 000000000..66ce8d802 --- /dev/null +++ b/testing/src/main/java/dev/cel/testing/CelRuntimeFlavor.java @@ -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. + +package dev.cel.testing; + +import dev.cel.bundle.CelBuilder; +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 CelFactory.plannerCelBuilder(); + } + }; + + /** Returns a new {@link CelBuilder} instance for this runtime flavor. */ + public abstract CelBuilder builder(); +} 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..677884a8a 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel +++ b/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel @@ -92,10 +92,11 @@ java_library( "//bundle:environment", "//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", @@ -103,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", @@ -134,6 +134,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", @@ -160,13 +161,19 @@ java_library( deps = [ ":cel_expression_source", ":default_result_matcher", + ":registry_utils", ":result_matcher", "//:auto_value", "//bundle:cel", + "//common:cel_descriptor_util", + "//common:cel_descriptors", "//common:options", "//policy:parser", "//runtime", + "//testing:proto_descriptor_utils", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", ], ) @@ -176,8 +183,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", ], ) @@ -206,8 +212,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", @@ -223,6 +231,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 aa0d4b34f..6ef988a44 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,24 @@ 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.CelDescriptors; 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 +75,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 +124,41 @@ public abstract class CelTestContext { */ public abstract Optional fileDescriptorSetPath(); + 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(); + } + + /** Returns a unified set of {@link CelDescriptors} combined from all descriptor sources. */ + @Memoized + public Optional mergedDescriptors() { + if (fileTypes().isEmpty() && !fileDescriptorSetPath().isPresent()) { + return Optional.empty(); + } + 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(); + /** Returns a builder for {@link CelTestContext} with the current instance's values. */ public abstract Builder toBuilder(); @@ -123,6 +183,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 +195,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/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/CelTestSuiteTextProtoParser.java b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteTextProtoParser.java index 3819e38d2..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,8 @@ 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; import dev.cel.expr.conformance.test.TestSection; @@ -29,32 +31,52 @@ 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; /** * 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); + 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/CelTestSuiteYamlParser.java b/testing/src/main/java/dev/cel/testing/testrunner/CelTestSuiteYamlParser.java index d1a3d6615..2340bf229 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, ""); } @@ -86,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()); @@ -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/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 a5e912ccb..1d3e49fbe 100644 --- a/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java +++ b/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java @@ -28,18 +28,21 @@ 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; +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.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; @@ -50,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; @@ -104,6 +105,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 { @@ -192,16 +200,23 @@ 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(descriptors.messageTypeDescriptors()) + .setExtensionRegistry(RegistryUtils.getExtensionRegistry(descriptors)) + .build(); + } + + if (!celTestContext.fileTypes().isEmpty()) { extendedCel = extendedCel .toCelBuilder() .addMessageTypes( - ProtoDescriptorUtils.getAllDescriptorsFromJvm( - celTestContext.fileDescriptorSetPath().get()) + CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(celTestContext.fileTypes()) .messageTypeDescriptors()) - .setExtensionRegistry( - RegistryUtils.getExtensionRegistry(celTestContext.fileDescriptorSetPath().get())) .build(); } @@ -302,8 +317,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()) { @@ -338,27 +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.fileDescriptorSetPath().get()) - .getDescriptorForTypeUrl(any.getTypeUrl()); - return 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())); + ExtensionRegistry extensionRegistry = + celTestContext + .extensionRegistry() + .orElseGet( + () -> + celTestContext + .mergedDescriptors() + .map(RegistryUtils::getExtensionRegistry) + .orElseGet(ExtensionRegistry::getEmptyRegistry)); + + return DynamicMessage.getDefaultInstance(descriptor) + .getParserForType() + .parseFrom(any.getValue(), extensionRegistry); } private static Message getEvaluatedContextExpr( @@ -396,10 +424,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/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 041c0f52d..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,13 +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.CelDescriptorUtil; import dev.cel.common.CelDescriptors; -import dev.cel.common.internal.DefaultInstanceMessageFactory; import dev.cel.common.internal.ProtoTimeUtils; import dev.cel.common.types.CelType; import dev.cel.common.types.ListType; @@ -46,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. */ @@ -55,45 +53,55 @@ 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 * `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, CelDescriptors descriptors) throws IOException { + TypeRegistry typeRegistry = RegistryUtils.getTypeRegistry(descriptors); + ExtensionRegistry extensionRegistry = RegistryUtils.getExtensionRegistry(descriptors); + return fromValue(value, typeRegistry, extensionRegistry); + } + 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); + return fromValue(value, ProtoDescriptorUtils.getDescriptorsFromFile(fileDescriptorSetPath)); } /** - * Converts a {@link Value} to a Java native object. + * 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) throws IOException { + 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()); - Message prototype = getDefaultInstance(descriptor); + typeRegistry.getDescriptorForTypeUrl(value.getObjectValue().getTypeUrl()); + if (descriptor == null) { + throw new IOException( + "Unknown type, descriptor was not found in registry: " + + value.getObjectValue().getTypeUrl()); + } + Message prototype = DynamicMessage.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 +126,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 +138,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 +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(), DEFAULT_EXTENSION_REGISTRY); + ((dev.cel.expr.Value) object).toByteArray(), + ExtensionRegistry.getEmptyRegistry()); } if (object instanceof Value) { return (Value) object; @@ -286,43 +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 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) - .orElseThrow( - () -> - new NoSuchElementException( - "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/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/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]#"); + } +} 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..9141832cb 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", @@ -154,42 +155,10 @@ java_test( ], ) -cel_java_test( - name = "test_runner_sample_yaml", - cel_expr = "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", - 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 = "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", 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", @@ -212,56 +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 = "context_pb/policy.yaml", - config = "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", - 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 = "nested_rule/policy.yaml", - config = "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", - 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 = "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", - 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", @@ -287,40 +206,6 @@ 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", - 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 = "context_pb/policy.yaml", - config = "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", - 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/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(); } } 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..112ef1f82 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; @@ -261,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 @@ -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"); + } } diff --git a/testing/src/test/resources/environment/dump_env.yaml b/testing/src/test/resources/environment/dump_env.yaml index 6a885ea51..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 @@ -82,3 +117,15 @@ 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 +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 fbed2b9d5..f380f4ed2 100644 --- a/testing/src/test/resources/environment/extended_env.yaml +++ b/testing/src/test/resources/environment/extended_env.yaml @@ -15,29 +15,74 @@ 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" +- 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 +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 @@ -46,7 +91,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 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/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 3e2624b64..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: conflicting output types found. - | output: "false" - | .............^ -ERROR: compose_errors_conflicting_output/policy.yaml:23:14: conflicting output types found. - | - output: "{'banned': true}" - | .............^ \ No newline at end of file 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 559d62e1d..000000000 --- a/testing/src/test/resources/policy/compose_errors_conflicting_subrule/expected_errors.baseline +++ /dev/null @@ -1,3 +0,0 @@ -ERROR: compose_errors_conflicting_subrule/policy.yaml:36:14: failed composing the subrule 'banned regions' due to conflicting 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..7510dc02d --- /dev/null +++ b/testing/src/test/resources/policy/incompatible_outputs/expected_errors.baseline @@ -0,0 +1,7 @@ +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 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 8585c5efb..000000000 --- a/testing/src/test/resources/policy/k8s/tests.yaml +++ /dev/null @@ -1,31 +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: "'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 fe6daa61d..000000000 --- a/testing/src/test/resources/policy/limits/tests.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. - -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 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 a9807c376..000000000 --- a/testing/src/test/resources/policy/nested_rule/tests.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. - -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: "permitted" - tests: - - name: "valid_origin" - input: - resource: - value: - origin: "uk" - output: "{'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 b5fbba745..000000000 --- a/testing/src/test/resources/policy/nested_rule2/tests.yaml +++ /dev/null @@ -1,48 +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: "{'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 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 b10785d0c..000000000 --- a/testing/src/test/resources/policy/nested_rule3/tests.yaml +++ /dev/null @@ -1,48 +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: "{'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 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 82dd6b11b..000000000 --- a/testing/src/test/resources/policy/pb/tests.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. - -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" 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 67681ef46..000000000 --- a/testing/src/test/resources/policy/required_labels/tests.yaml +++ /dev/null @@ -1,79 +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: "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: "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\"]" 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 c0feeb202..000000000 --- a/testing/src/test/resources/policy/restricted_destinations/tests.yaml +++ /dev/null @@ -1,118 +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: "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: "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" 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..875e00392 --- /dev/null +++ b/testing/src/test/resources/policy/unreachable/expected_errors.baseline @@ -0,0 +1,9 @@ +ERROR: unreachable/policy.yaml:38:9: Condition is always false + | - condition: "false" + | ........^ +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/src/test/resources/policy/compose_errors_conflicting_output/policy.yaml b/testing/src/test/resources/policy/verification/flawed_policy.yaml similarity index 54% rename from testing/src/test/resources/policy/compose_errors_conflicting_output/policy.yaml rename to testing/src/test/resources/policy/verification/flawed_policy.yaml index a5ed5c09c..1e276c899 100644 --- a/testing/src/test/resources/policy/compose_errors_conflicting_output/policy.yaml +++ b/testing/src/test/resources/policy/verification/flawed_policy.yaml @@ -1,4 +1,4 @@ -# Copyright 2024 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. @@ -12,12 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -name: nested_rule +name: flawed_policy +description: Tests detecting an invariant violation when a policy allows insecure output (port == 80), checking accurate counterexample generation. rule: - variables: - - name: "permitted_regions" - expression: "['us', 'uk', 'es']" match: - - condition: resource.origin in variables.permitted_regions - output: "false" - - output: "{'banned': true}" + - 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/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; +} 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"], ) diff --git a/testing/testrunner/cel_java_test.bzl b/testing/testrunner/cel_java_test.bzl index b3457f6f0..d2dd796c0 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(":") + 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", - ] 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", 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 = diff --git a/verifier/BUILD.bazel b/verifier/BUILD.bazel new file mode 100644 index 000000000..ef1316ca2 --- /dev/null +++ b/verifier/BUILD.bazel @@ -0,0 +1,70 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + 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", + 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", + compatible_with = [], + 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 = "numeric_bounds", + compatible_with = [], + visibility = [":verifier_internal"], + exports = ["//verifier/src/main/java/dev/cel/verifier:numeric_bounds"], +) + +java_library( + name = "type_system", + compatible_with = [], + visibility = [":verifier_internal"], + exports = ["//verifier/src/main/java/dev/cel/verifier:type_system"], +) + +java_library( + name = "z3_impl", + compatible_with = [], + 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/README.md b/verifier/README.md new file mode 100644 index 000000000..bd9979390 --- /dev/null +++ b/verifier/README.md @@ -0,0 +1,439 @@ +# 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). 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 & 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. +* **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 unknown fields from failure paths + .build(); +``` + +--- + +## Upcoming Capabilities + +The following features are planned for future releases: + +* **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; + } + + 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; + } + } +} +``` + +### 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.plannerCelBuilder() + .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; + } + + 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: + // Invariant 'always_secure' violation detected. Counterexample input: + // port = 80 + break; + case INCONCLUSIVE: + System.out.println("Verification was inconclusive: " + result.message()); + break; + } + } +} +``` + +--- + +## Limitations & Best Practices + +### Limitations + +* **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-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 +`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. + +--- + +## Tools & CLI + +For command-line verification and interactive execution, see the [CLI Tool documentation](tools/README.md). 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..3396b6df4 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/BUILD.bazel @@ -0,0 +1,192 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = [ + "//publish:__pkg__", + "//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", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "verifier_factory", + srcs = ["CelVerifierFactory.java"], + compatible_with = [], + tags = [ + ], + deps = [ + ":verifier", + ":z3_impl", + "//bundle:cel", + "//checker:checker_builder", + "//compiler", + "//compiler:compiler_builder", + "//parser:parser_builder", + "//runtime", + ], +) + +java_library( + name = "policy_verifier", + srcs = [ + "CelPolicyVerifier.java", + "CelPolicyVerifierBuilder.java", + ], + tags = [ + ], + deps = [ + ":verifier", + "//policy", + "//policy:validation_exception", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "policy_verifier_factory", + srcs = ["CelPolicyVerifierFactory.java"], + compatible_with = [], + tags = [ + ], + deps = [ + ":policy_verifier", + ":policy_verifier_impl", + ":verifier", + "//policy:compiler", + ], +) + +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", + ], +) + +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"], + compatible_with = [], + tags = [ + ], + deps = [ + ":numeric_bounds", + "//common/internal:proto_time_utils", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:tools_aqua_z3_turnkey", + ], +) + +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:expr_util", + "//common/navigation:mutable_navigation", + "//common/values:cel_byte_string", + "//optimizer:ast_optimizer", + "//optimizer:mutable_ast", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + +java_library( + name = "z3_impl", + srcs = [ + "CelAstAlphaHasher.java", + "CelAstToZ3Translator.java", + "CelVerifierZ3Impl.java", + "CelZ3CounterexampleGenerator.java", + "CelZ3ExtensionalityAxioms.java", + "CelZ3FunctionRegistry.java", + "CelZ3OperatorTranslator.java", + "TranslatedValue.java", + ], + compatible_with = [], + tags = [ + ], + deps = [ + ":canonicalization_optimizer", + ":numeric_bounds", + ":type_system", + ":verifier", + "//:auto_value", + "//bundle:cel", + "//common:cel_ast", + "//common:compiler_common", + "//common:operator", + "//common/ast", + "//common/ast:cel_block", + "//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", + "@maven//:org_jspecify_jspecify", + "@maven//:tools_aqua_z3_turnkey", + ], +) 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..5e2a0a8ec --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CanonicalizationOptimizer.java @@ -0,0 +1,869 @@ +// 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.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; +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.CelNavigableExprUtil; +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; +import org.jspecify.annotations.Nullable; + +/** + * 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; + + /** + * 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, CanonicalizationScope.EMPTY); + canonicalizeMacroCalls(mutableAst); + CelAbstractSyntaxTree optimizedAst = + AstMutator.newInstance(canonicalizationOptions.maxIterationLimit()) + .renumberIdsConsecutively(mutableAst) + .toParsedAst(); + 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, CanonicalizationScope baseScope) { + CelMutableAst mutableAst = CelMutableAst.of(root, CelMutableSource.newInstance()); + mutableAst = runCanonicalizationLoop(mutableAst, baseScope); + return mutableAst.expr(); + } + + private CelMutableAst runCanonicalizationLoop( + CelMutableAst mutableAst, CanonicalizationScope baseScope) { + 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, baseScope); + 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 Optional maybeCanonicalize( + CelMutableAst mutableAst, + CelNavigableMutableExpr navigableExpr, + CanonicalizationScope baseScope) { + 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) { + 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, baseScope); + } + + if (functionName.equals(Operator.LOGICAL_NOT.getFunction()) && args.size() == 1) { + return maybeCanonicalizeLogicalNot(mutableAst, args.get(0)); + } + + return Optional.empty(); + } + + private static Optional maybeCanonicalizeCommutativeCall( + CelNavigableMutableExpr navigableExpr, String functionName, CanonicalizationScope baseScope) { + // 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()); + } + 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() + || scopedComparator.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( + 0, CelMutableCall.create(functionName, rebuilt, uniqueSorted.get(i))); + } + if (scopedComparator.compare(rebuilt, navigableExpr.expr()) == 0) { + return Optional.empty(); + } + return Optional.of(rebuilt); + } + + private static Optional maybeCanonicalizeSymmetricCall( + CelNavigableMutableExpr navigableExpr, + String functionName, + List args, + CanonicalizationScope baseScope) { + CelMutableExpr arg0 = args.get(0); + CelMutableExpr arg1 = args.get(1); + 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, 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( + 0, + 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( + 0, + 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( + 0, + 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( + 0, + CelMutableCall.create( + Operator.EQUALS.getFunction(), subArgs.get(0), subArgs.get(1)))); + } + return QuantifierDeMorganRewriter.maybeRewrite(mutableAst, target); + } + + private static CelMutableExpr negate(CelMutableExpr expr) { + return CelMutableExpr.ofCall( + 0, CelMutableCall.create(Operator.LOGICAL_NOT.getFunction(), expr)); + } + + 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 boolean isCallWithArgCount( + CelMutableExpr expr, String functionName, int argCount) { + return expr.getKind() == Kind.CALL + && expr.call().function().equals(functionName) + && 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 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) { + 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 compareIdent(e1.ident().name(), e2.ident().name(), scope); + 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 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 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) + .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) { + 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) { + 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 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; + } + } + } + + /** + * 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 enclosingComprehensions = + collectEnclosingComprehensions(contextExpr); + if (enclosingComprehensions.isEmpty()) { + return false; + } + return operand + .allNodes() + .filter(node -> node.getKind() == Kind.IDENT) + .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(); + CelMutableExpr currExpr = curr.expr(); + if ((currExpr == comp.loopCondition() || currExpr == comp.loopStep()) + && !comp.accuVar().isEmpty()) { + comps.add(parent); + } + } + curr = parent; + maybeParent = parent.parent(); + } + return comps; + } + } + + /** + * Rewriter for De Morgan quantifier dualities over single-variable and two-variable + * comprehensions. + */ + private static final class QuantifierDeMorganRewriter { + + 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. */ + @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/CelAstAlphaHasher.java b/verifier/src/main/java/dev/cel/verifier/CelAstAlphaHasher.java new file mode 100644 index 000000000..31a7e3b2a --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelAstAlphaHasher.java @@ -0,0 +1,240 @@ +// 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.HashMap; +import java.util.List; +import java.util.Map; +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 { + 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); + } + break; + case SELECT: + hashAst(expr.select().operand(), scope, context); + context.hasher.putString(expr.select().field(), UTF_8); + context.hasher.putBoolean(expr.select().testOnly()); + break; + case CALL: + 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 (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; + 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 Map freeVarIndices = new HashMap<>(); + final List freeVars = new ArrayList<>(); + + HasherContext(HashFunction hashFunction) { + this.hasher = hashFunction.newHasher(); + } + } + + private static final class Scope { + final String varName; + final @Nullable Scope parent; + + Scope(String varName, @Nullable 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 new file mode 100644 index 000000000..ba63e9693 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelAstToZ3Translator.java @@ -0,0 +1,1456 @@ +// 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.google.common.collect.Iterables; +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.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.CelTypes; +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; +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 NULL_VALUE_FIELD = "null_value"; + 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)); + } + + /** + * 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); + 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, tv.isApproximate()); + } + + 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())); + 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); + + 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) + ctx.mkITE( + typeSystem.optHasValue(optRef), + typeSystem.mkConcatSafe(seq, ctx.mkUnit(typeSystem.getOptionalValue(optRef))), + seq); + } 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)); + 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(); + + Expr finalValue = value; + BoolExpr finalPresence = ctx.mkTrue(); + if (entryAst.optionalEntry()) { + 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); + keysSeq = + ctx.mkITE(shouldInsertKey, typeSystem.mkConcatSafe(keysSeq, ctx.mkUnit(key)), keysSeq); + + 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)); + 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(); + 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( + 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(); + + 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); + + Expr finalValue = value; + BoolExpr optionalHasValue = ctx.mkTrue(); + if (entryAst.optionalEntry()) { + 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: + // + // 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 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, finalValue)); + + 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 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(); + } + if (type instanceof OptionalType) { + return typeSystem.mkOptionalNone(); + } + 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.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); + 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 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(typeSystem.isUnknown(value)); + 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(typeSystem.isUnknown(value)); + 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(); + 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) { + 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) { + FieldAccess msgAcc = getMsgAccess(operand, field, ctx.mkTrue()); + presenceResult = msgAcc.presence; + Expr defaultVal = getDefaultValueForType(extractAstTypeOrDefault(ast, exprId)); + valueResult = ctx.mkITE((BoolExpr) msgAcc.presence, msgAcc.value, defaultVal); + } else { + // Dynamic type: generate the full SMT decision tree + BoolExpr isMap = typeSystem.isMap(operand); + BoolExpr isMessage = typeSystem.isMessage(operand); + + FieldAccess mapAcc = getMapAccess(operand, field, isMap); + FieldAccess msgAcc = getMsgAccess(operand, field, isMessage); + + presenceResult = + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(isMessage, msgAcc.presence) + .addCase(isMap, mapAcc.presence) + .build(ctx.mkFalse()); + + Expr defaultVal = getDefaultValueForType(extractAstTypeOrDefault(ast, exprId)); + 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) + .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, ImmutableList.of(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))); + + boolean isDynamic = ast.getTypeOrThrow(exprId).equals(SimpleType.DYN); + BoolExpr isApprox = ctx.mkBool(!isDynamic); + return TranslatedValue.propagateStrict( + ctx, typeSystem, callRes, Optional.of(expr), isApprox, 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(); + 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<>(); + + // For statically known list/map literals, unroll them exactly. + 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); + 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 + && 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(); + 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; + + 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 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)))); + constraints.add(ctx.mkImplies(validPair, notEqual)); + } + } + + BoolExpr isNotTruncated = ctx.mkLe(lengthExpr, ctx.mkInt(comprehensionUnrollLimit)); + + ArrayExpr seqMap = ctx.mkConstArray(typeSystem.celValueSort(), ctx.mkFalse()); + for (int i = 0; i < comprehensionUnrollLimit; i++) { + seqMap = + (ArrayExpr) + ctx.mkITE( + ctx.mkLt(ctx.mkInt(i), lengthExpr), + ctx.mkStore(seqMap, ctx.mkNth(seq, ctx.mkInt(i)), ctx.mkTrue()), + seqMap); + } + constraints.add(ctx.mkImplies(isNotTruncated, ctx.mkEq(mapPresence, seqMap))); + return CelZ3TypeSystem.mkAndFlattened(ctx, constraints); + } + + 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), celExpr, ast); + 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()); + + 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)); + // 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( + 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()); + + BoolExpr isNotError = ctx.mkNot(typeSystem.isError(resultTv.z3Expr())); + BoolExpr shouldYieldUnknown = ctx.mkAnd(isTruncated, isNotError); + taints.add(shouldYieldUnknown); + + return TranslatedValue.create( + typeSystem.propagateErrorAndUnknown( + ctx.mkITE(shouldYieldUnknown, mkParameterizedUnknown(celExpr, ast), 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, + CelExpr compExpr, + CelAbstractSyntaxTree ast) { + 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, isActive, isMatch)); + hasErrorList.add(CelZ3TypeSystem.mkAndFlattened(ctx, isActive, isE)); + hasUnknownList.add(CelZ3TypeSystem.mkAndFlattened(ctx, isActive, isU)); + + hasSafeMatchList.add( + CelZ3TypeSystem.mkAndFlattened( + ctx, isActive, isMatch, CelZ3TypeSystem.mkNotFlattened(ctx, stepTv.isApproximate()))); + hasSafeErrorList.add( + CelZ3TypeSystem.mkAndFlattened( + ctx, isActive, isE, CelZ3TypeSystem.mkNotFlattened(ctx, stepTv.isApproximate()))); + hasSafeUnknownList.add( + CelZ3TypeSystem.mkAndFlattened( + ctx, isActive, isU, CelZ3TypeSystem.mkNotFlattened(ctx, stepTv.isApproximate()))); + activeTaints.add(CelZ3TypeSystem.mkAndFlattened(ctx, 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), mkParameterizedUnknown(compExpr, ast)) + .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, /* expectNot= */ false); + } + + private static boolean isExistsMacro(CelComprehension comp) { + return isBooleanAccuInit(comp, false) + && isNotStrictlyFalseLoopCondition(comp, /* expectNot= */ true); + } + + 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, boolean expectNot) { + CelExpr.CelCall call = comp.loopCondition().callOrDefault(); + 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) { + CelType type = + ast.getType(exprId) + .orElseThrow( + () -> new IllegalArgumentException("Type not found for expr ID: " + exprId)); + BoolExpr typeConstraint = createTypeConstraintForType(val, type); + return ctx.mkOr(typeSystem.isErrorOrUnknown(val), typeConstraint); + } + + 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); + 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); + } + 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(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(CelNumericBounds.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.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) { + // 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(); + + 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 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)); + } + + return CelZ3TypeSystem.mkAndFlattened(ctx, boundsAndTypes); + } + if (type instanceof MapType) { + // 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); + boundsAndTypes.add(getBoundedMapBijection(mapPresence, seq, (ArithExpr) length)); + + 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 = + 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))); + } + + return CelZ3TypeSystem.mkAndFlattened(ctx, boundsAndTypes); + } + 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: + 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); + if (!elemKey.isPresent()) { + return Optional.empty(); // Contains non-constants + } + builder.add(elemKey.get()); + } + return Optional.of(builder.build()); + default: + 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/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/CelPolicyVerifier.java b/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifier.java new file mode 100644 index 000000000..70e1494f1 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifier.java @@ -0,0 +1,42 @@ +// 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 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; + + /** + * 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/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..96473c16f --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelPolicyVerifierImpl.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.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; + +/** 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; + + 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); + } + + @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."); + } + String invariantId = invariant.invariantId().value(); + CelVerificationResult result = + ((CelVerifierZ3Impl) astVerifier) + .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/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..5a4c7ada6 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelVerificationResult.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.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 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 String message() { + return reason() + counterexample(); + } + + static CelVerificationResult 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 failed(String reason, String counterexample) { + return new AutoValue_CelVerificationResult( + VerificationStatus.VIOLATED, reason, counterexample); + } + + static CelVerificationResult inconclusive(String reason) { + return new AutoValue_CelVerificationResult(VerificationStatus.INCONCLUSIVE, reason, ""); + } + + 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/CelVerifier.java b/verifier/src/main/java/dev/cel/verifier/CelVerifier.java new file mode 100644 index 000000000..0e80424f2 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelVerifier.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 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. 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. + */ + 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..d761428d6 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelVerifierFactory.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; + +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}. + * + * @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 new file mode 100644 index 000000000..04f3d3476 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelVerifierZ3Impl.java @@ -0,0 +1,570 @@ +// 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.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; +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; + +/** 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; + + @SuppressWarnings("Immutable") // Cel environment is immutable, just not marked as such + private final Cel cel; + + static Builder newBuilder() { + return new Builder(CelFactory.plannerCelBuilder().build()); + } + + static Builder newBuilder(Cel cel) { + return new Builder(Preconditions.checkNotNull(cel)); + } + + static final class Builder implements CelVerifierBuilder { + private Duration timeout; + private int comprehensionUnrollLimit; + private final ImmutableSet.Builder unknownIdentifiers; + private final ImmutableList.Builder functionAxioms; + private final Cel cel; + private CelTypeProvider typeProvider; + + 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 + @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, + cel); + } + } + + @Override + public CelVerificationResult isSatisfiable(CelAbstractSyntaxTree ast) + throws CelVerificationException { + Preconditions.checkArgument(ast.isChecked(), "AST must be type-checked."); + 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, /* searchForCounterexample= */ 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."); + 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( + 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); + } + + // 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, + /* checkTruncation= */ false); + + switch (result.outcome) { + case EXACT_MATCH: + return CelVerificationResult.failed( + "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)); + 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); + } + } + + CelVerificationResult verifyImplication( + CelAbstractSyntaxTree assumeAst, + CelAbstractSyntaxTree assertAst, + Map boundSymbols, + String subjectName) + 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( + 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)); + 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))); + 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, + /* checkTruncation= */ true); + + switch (result.outcome) { + case EXACT_MATCH: + return searchForCounterexample + ? CelVerificationResult.failed( + "Condition is not always true.", + getCounterexampleString( + ctx, + translator.getTypeSystem(), + result.model, + /* isApproximate= */ false, + /* isCounterexample= */ true)) + : CelVerificationResult.verified( + "Condition is satisfiable." + + getCounterexampleString( + ctx, + translator.getTypeSystem(), + result.model, + /* isApproximate= */ false, + /* isCounterexample= */ false)); + + 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, + /* isApproximate= */ true, + /* isCounterexample= */ searchForCounterexample)); + + 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, + boolean checkTruncation) + 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)); + } + + // 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); + 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, + boolean isApproximate, + boolean isCounterexample) { + return CelZ3CounterexampleGenerator.generate( + ctx, typeSystem, model, isApproximate, isCounterexample); + } + + CelVerifierZ3Impl( + Duration timeout, + int comprehensionUnrollLimit, + ImmutableSet unknownIdentifiers, + CelZ3FunctionRegistry functionRegistry, + 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 { + 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..cef976608 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java @@ -0,0 +1,309 @@ +// 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.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_ELEMENTS_TO_PRINT = 15; + + private CelZ3CounterexampleGenerator() {} + + static String generate( + Context ctx, + CelZ3TypeSystem typeSystem, + Model model, + boolean isApproximate, + boolean isCounterexample) { + 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 isCounterexample + ? " (The expression fails unconditionally, regardless of input state)" + : " (The expression is satisfiable unconditionally, regardless of input state)"; + } + + String prefix; + if (isCounterexample) { + prefix = isApproximate ? " Potential counterexample input:" : " Counterexample input:"; + } else { + prefix = isApproximate ? " Potential satisfying input:" : " Satisfying input:"; + } + return prefix + 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.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]) + "s')"; + } 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"; + } else if (decl.equals(typeSystem.nullCons().ConstructorDecl())) { + return "null"; + } 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(); + } + + 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)); + 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 = + 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 lenExpr = + evaluateStrict( + model, + ctx.mkLength(typeSystem.getMapKeys(mapRef)), + String.format("Z3 failed to evaluate length for map %s", mapRef)); + 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<>(); + 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( + 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)); + } + } + if (length > printLimit) { + entries.add("... (" + (length - printLimit) + " more entries)"); + } + + return "{" + String.join(", ", entries) + "}"; + } + + private static String reconstructMessage( + Context ctx, CelZ3TypeSystem typeSystem, Model model, Expr msgRef) { + Expr presenceArray = + evaluateStrict( + model, + typeSystem.getMsgPresence(msgRef), + String.format("Z3 failed to evaluate presence 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("\"", ""); + + Set> keys = new LinkedHashSet<>(); + extractKeys(presenceArray, 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, Set> keys) { + int iterations = 0; + while (true) { + 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(); + + if (declName.equals("store")) { + 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]; + continue; + } + break; + } + } + + 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..be1ec1475 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3ExtensionalityAxioms.java @@ -0,0 +1,189 @@ +// 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 = + typeSystem.internFuncDecl(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 (isAppOf(ref, 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 = + typeSystem.internFuncDecl( + 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 (isAppOf(ref, 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 = + typeSystem.internFuncDecl( + 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, + 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 (isAppOf(ref, typeSystem.messageCons().getAccessorDecls()[0])) { + Expr inner = ref.getArgs()[0]; + axiom = ctx.mkImplies(typeSystem.isMessage(inner), axiom); + } + axioms.add(axiom); + } + } + + /** + * 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/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..bd5c8874e --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3OperatorTranslator.java @@ -0,0 +1,993 @@ +// 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.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; +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.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: + return typeSystem.isInt(arg); + case TIMESTAMP: + return typeSystem.isTimestamp(arg); + case DURATION: + return typeSystem.isDuration(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 NEGATE: + return translateNegate(args.get(0), ast); + case EQUALS: + return translateEquality(args.get(0), args.get(1), ast, /* isEquals= */ true); + case NOT_EQUALS: + return translateEquality(args.get(0), args.get(1), ast, /* isEquals= */ false); + case LESS: + case GREATER: + case LESS_EQUALS: + case GREATER_EQUALS: + case ADD: + case SUBTRACT: + case MULTIPLY: + case DIVIDE: + case MODULO: + 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, false); + case OPTIONAL_INDEX: + return translateIndex(args, ast, true); + 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 unknownResult = ctx.mkITE(a.isZ3Unknown(), a.z3Expr(), b.z3Expr()); + + Expr resultZ3 = + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(hasMatch, typeSystem.mkBool(!isAnd)) + .addCase(hasUnknown, unknownResult) + .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 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) { + Optional intRange = Optional.empty(); + Optional uintRange = Optional.empty(); + double doubleVal; + + switch (constant.getKind()) { + case INT64_VALUE: + long vInt = constant.int64Value(); + intRange = Optional.of(CelNumericBounds.IntRange.of(vInt, vInt)); + if (vInt >= 0) { + 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) { + intRange = Optional.of(CelNumericBounds.IntRange.of(vUint, vUint)); + } + 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; + 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 buildIntRangeExpr(intRange, typeSystem.getInt(symVal)); + } else if (symType.kind() == CelKind.UINT) { + return buildUintRangeExpr(uintRange, typeSystem.getUint(symVal)); + } else if (symType.kind() == CelKind.DOUBLE) { + return ctx.mkFPEq(typeSystem.getDouble(symVal), typeSystem.mkFpDouble(doubleVal)); + } + } + + 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) + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(typeSystem.isInt(symVal), intEq) + .addCase(typeSystem.isUint(symVal), uintEq) + .addCase(typeSystem.isDouble(symVal), doubleEq) + .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()) { + 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) && type0.kind() == type1.kind()) { + 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(typeSystem.getDouble(z3Expr0), typeSystem.getDouble(z3Expr1)); + default: + return ctx.mkFalse(); + } + } + + private BoolExpr mkIsFiniteDouble(Expr z3Expr) { + FPExpr fpVal = typeSystem.getDouble(z3Expr); + return ctx.mkAnd( + typeSystem.isDouble(z3Expr), + ctx.mkNot(ctx.mkOr(ctx.mkFPIsNaN(fpVal), ctx.mkFPIsInfinite(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), + ctx.mkITE(typeSystem.isUint(z3Expr0), typeSystem.getUint(z3Expr0), ctx.mkInt(0))); + IntExpr val1 = + (IntExpr) + ctx.mkITE( + 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); + + FPExpr fpVal1 = typeSystem.getDouble(z3Expr1); + ArithExpr realVal0 = ctx.mkInt2Real(val0); + BoolExpr intDoubleEq = + ctx.mkAnd( + mkIsFiniteDouble(z3Expr1), + ctx.mkLe(realVal0, ctx.mkFPToReal(fpVal1)), + ctx.mkLe(ctx.mkFPToReal(fpVal1), realVal0)); + + FPExpr fpVal0 = typeSystem.getDouble(z3Expr0); + ArithExpr realVal1 = ctx.mkInt2Real(val1); + BoolExpr doubleIntEq = + ctx.mkAnd( + mkIsFiniteDouble(z3Expr0), + 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(typeSystem.getDouble(z3Expr0), typeSystem.getDouble(z3Expr1))) + .addCase(isIntOrUintAndDouble, intDoubleEq) + .addCase(isDoubleAndIntOrUint, doubleIntEq) + .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 = + 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 = translateEquality(elemA, elemB, ast, /* isEquals= */ true); + 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 isEquals) { + Expr z3Arg0 = arg0.z3Expr(); + Expr z3Arg1 = arg1.z3Expr(); + + CelType type0 = extractAstTypeOrDefault(arg0, ast); + CelType type1 = extractAstTypeOrDefault(arg1, ast); + + 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)) + && !hasOptionalElements(arg0) + && !hasOptionalElements(arg1)) { + equality = unrollListEquality(arg0, arg1, ast); + } else if (isStaticallyKnown(type0) && isStaticallyKnown(type1)) { + equality = typeSystem.getStructuralEquality(z3Arg0, z3Arg1); + } else { + 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); + if ((arg0.isLiteral(ExprKind.Kind.LIST) || arg1.isLiteral(ExprKind.Kind.LIST)) + && !hasOptionalElements(arg0) + && !hasOptionalElements(arg1)) { + structuralEq = + (BoolExpr) + ctx.mkITE( + ctx.mkAnd(typeSystem.isList(z3Arg0), typeSystem.isList(z3Arg1)), + unrollListEquality(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 (!isEquals) { + 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.mkOr(arg0.isApproximate(), arg1.isApproximate())); + } + + return TranslatedValue.propagateStrict(ctx, typeSystem, equalityExpr, arg0, arg1) + // 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, BoolExpr typeGuard, boolean isOptional) { + Expr listRef = typeSystem.getListRef(lhsTrans); + SeqExpr seq = typeSystem.getSeq(listRef); + Expr index = typeSystem.getInt(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(ctx.mkAnd(typeGuard, inBounds), valNotError)); + if (!allowUnknowns) { + BoolExpr valNotUnknown = ctx.mkNot(typeSystem.isUnknown(val)); + 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()); + } + + 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 condMap1 = CelZ3TypeSystem.mkAndFlattened(ctx, cond1, inMap1); + BoolExpr condMap2 = CelZ3TypeSystem.mkAndFlattened(ctx, cond2, inMap2); + + BoolExpr altInMap = CelZ3TypeSystem.mkOrFlattened(ctx, inMapOrig, condMap1, condMap2); + Expr altVal = + CelZ3TypeSystem.SwitchBuilder.newBuilder(ctx) + .addCase(inMapOrig, valOrig) + .addCase(condMap1, val1) + .addCase(condMap2, val2) + .build(valOrig); + + return new ProbeResult(altInMap, altVal); + } + + 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); + + 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(CelNumericBounds.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(finalVal)); + 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, boolean isOptional) { + TranslatedValue lhs = args.get(0); + TranslatedValue rhs = args.get(1); + CelType lhsType = extractAstTypeOrDefault(lhs, ast); + CelType rhsType = extractAstTypeOrDefault(rhs, ast); + + 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) { + expectedElemType = ((ListType) lhsType).elemType(); + } else if (lhsType.kind() == CelKind.MAP) { + 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.mkAnd(shouldEvaluate, ctx.mkNot(typeSystem.isError(actualValue))), + typeConstraintGenerator.apply(actualValue, finalType))); + + return actualValue; + } + + 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( + 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, cond.z3Expr()) + .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.create( + typeSystem.wrapBool(ctx.mkNot(isFalse)), typeSystem, arg.isApproximate()); + } + + 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..dc19a8d3a --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java @@ -0,0 +1,1110 @@ +// 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.common.primitives.UnsignedLongs; +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.FPNum; +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 dev.cel.common.internal.ProtoTimeUtils; +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 { + + 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_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"; + + 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"; + + 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 timestampCons; + private final Constructor durationCons; + private final Constructor errorCons; + private final Constructor unknownCons; + private final Constructor nullCons; + private final Constructor optionalCons; + + private final Sort unknownIdSort; + 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; + } + + public Constructor boolCons() { + return boolCons; + } + + public Constructor intCons() { + return intCons; + } + + public Constructor uintCons() { + return uintCons; + } + + public Constructor doubleCons() { + return doubleCons; + } + + public Constructor stringCons() { + return stringCons; + } + + public Constructor bytesCons() { + return bytesCons; + } + + public Constructor timestampCons() { + return timestampCons; + } + + public Constructor durationCons() { + return durationCons; + } + + 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)); + } + + /** 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); + } + + /** 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(Long.toString(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 mkUint(UnsignedLongs.toString(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 FPExpr getDouble(Expr val) { + return (FPExpr) 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(getDouble(arg0), 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()); + } + + 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)); + } + + /** 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; + } + + /** + * 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; + } + 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); + 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, unknownResult) + .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); + } + + public 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 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); + } + + /** 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 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); + } + + /** 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 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); + } + + /** 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(CelNumericBounds.MAX_INT64)), + ctx.mkLt(result, ctx.mkInt(CelNumericBounds.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(CelNumericBounds.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. 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 { + + 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) { + // Skip branches that can never be hit (e.g. `isFalse()` probes). + if (condition.isFalse()) { + return this; + } + cases.add(new SwitchCase(condition, value)); + return this; + } + + 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; + } + + 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, BoolExpr... args) { + return mkOrFlattened(ctx, Arrays.asList(args)); + } + + /** + * 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, 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. + 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); + // 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); + 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.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"); + 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 = + 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.timestampCons, + this.durationCons, + 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..506f0bbc7 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/TranslatedValue.java @@ -0,0 +1,216 @@ +// 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 unknowns = new ArrayList<>(); + List taints = new ArrayList<>(); + taints.add(baseTaint); + + boolean hasNonConstantArgs = false; + 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; + } + hasNonConstantArgs = true; + + Expr z3Expr = arg.z3Expr(); + BoolExpr isApprox = arg.isApproximate(); + BoolExpr isError = ts.isError(z3Expr); + BoolExpr isUnknown = ts.isUnknown(z3Expr); + + 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); + } + + 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 hasUnknown = CelZ3TypeSystem.mkOrFlattened(ctx, unknowns); + + BoolExpr isSafe = + CelZ3TypeSystem.mkOrFlattened( + ctx, + hasExactUnknown, + CelZ3TypeSystem.mkAndFlattened( + ctx, 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..bff6b1edb --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/AddAxiom.java @@ -0,0 +1,135 @@ +// 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.BoolExpr; +import com.microsoft.z3.Expr; +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(), + 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) -> { + 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(), ts.getDouble(l), 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..c7b101e21 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/AxiomHelpers.java @@ -0,0 +1,99 @@ +// 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.FPExpr; +import com.microsoft.z3.IntExpr; +import com.microsoft.z3.RealExpr; + +/** 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)); + } + + /** + * 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/BUILD.bazel b/verifier/src/main/java/dev/cel/verifier/axioms/BUILD.bazel new file mode 100644 index 000000000..c397f1b45 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/BUILD.bazel @@ -0,0 +1,31 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = [ + "//publish:__pkg__", + "//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:numeric_bounds", + "//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..06d797d4d --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/CelZ3FunctionAxiom.java @@ -0,0 +1,189 @@ +// 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) { + approx = (BoolExpr) ctx.mkITE(ts.isUnknown(val), 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..2ccb1543a --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/GreaterAxiom.java @@ -0,0 +1,130 @@ +// 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.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(typeSystem.getInt(lhs), typeSystem.getInt(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_TIMESTAMP.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGt(typeSystem.getTimestamp(lhs), typeSystem.getTimestamp(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_DURATION.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGt(typeSystem.getDuration(lhs), typeSystem.getDuration(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGt(typeSystem.getUint(lhs), typeSystem.getUint(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkFPGt(typeSystem.getDouble(lhs), 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( + AxiomHelpers.mkFpLtReal( + ctx, + typeSystem.getDouble(rhs), + ctx.mkInt2Real(typeSystem.getInt(lhs)))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_UINT64_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + AxiomHelpers.mkFpLtReal( + ctx, + typeSystem.getDouble(rhs), + ctx.mkInt2Real(typeSystem.getUint(lhs)))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_DOUBLE_INT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + AxiomHelpers.mkRealLtFp( + ctx, + ctx.mkInt2Real(typeSystem.getInt(rhs)), + typeSystem.getDouble(lhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_DOUBLE_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + AxiomHelpers.mkRealLtFp( + ctx, + ctx.mkInt2Real(typeSystem.getUint(rhs)), + typeSystem.getDouble(lhs))))) + .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..d71f0f248 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/GreaterEqualsAxiom.java @@ -0,0 +1,130 @@ +// 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.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(typeSystem.getInt(lhs), typeSystem.getInt(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_TIMESTAMP.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + 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(typeSystem.getDuration(lhs), typeSystem.getDuration(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkGe(typeSystem.getUint(lhs), typeSystem.getUint(rhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkFPGEq(typeSystem.getDouble(lhs), 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( + AxiomHelpers.mkFpLeReal( + ctx, + typeSystem.getDouble(rhs), + ctx.mkInt2Real(typeSystem.getInt(lhs)))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_UINT64_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + AxiomHelpers.mkFpLeReal( + ctx, + typeSystem.getDouble(rhs), + ctx.mkInt2Real(typeSystem.getUint(lhs)))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_DOUBLE_INT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + AxiomHelpers.mkRealLeFp( + ctx, + ctx.mkInt2Real(typeSystem.getInt(rhs)), + typeSystem.getDouble(lhs))))) + .addBinaryOverloadTranslator( + Comparison.GREATER_EQUALS_DOUBLE_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + AxiomHelpers.mkRealLeFp( + ctx, + ctx.mkInt2Real(typeSystem.getUint(rhs)), + typeSystem.getDouble(lhs))))) + .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..31b1d3a21 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/LessAxiom.java @@ -0,0 +1,130 @@ +// 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.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(typeSystem.getInt(lhs), typeSystem.getInt(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_TIMESTAMP.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLt(typeSystem.getTimestamp(lhs), typeSystem.getTimestamp(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_DURATION.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLt(typeSystem.getDuration(lhs), typeSystem.getDuration(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLt(typeSystem.getUint(lhs), typeSystem.getUint(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkFPLt(typeSystem.getDouble(lhs), 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( + AxiomHelpers.mkRealLtFp( + ctx, + ctx.mkInt2Real(typeSystem.getInt(lhs)), + typeSystem.getDouble(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_UINT64_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + AxiomHelpers.mkRealLtFp( + ctx, + ctx.mkInt2Real(typeSystem.getUint(lhs)), + typeSystem.getDouble(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_DOUBLE_INT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + AxiomHelpers.mkFpLtReal( + ctx, + typeSystem.getDouble(lhs), + ctx.mkInt2Real(typeSystem.getInt(rhs)))))) + .addBinaryOverloadTranslator( + Comparison.LESS_DOUBLE_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + AxiomHelpers.mkFpLtReal( + ctx, + 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..c2466cf1b --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/LessEqualsAxiom.java @@ -0,0 +1,130 @@ +// 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.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(typeSystem.getInt(lhs), typeSystem.getInt(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_TIMESTAMP.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + 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(typeSystem.getDuration(lhs), typeSystem.getDuration(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkLe(typeSystem.getUint(lhs), typeSystem.getUint(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + ctx.mkFPLEq(typeSystem.getDouble(lhs), 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( + AxiomHelpers.mkRealLeFp( + ctx, + ctx.mkInt2Real(typeSystem.getInt(lhs)), + typeSystem.getDouble(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_UINT64_DOUBLE.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + AxiomHelpers.mkRealLeFp( + ctx, + ctx.mkInt2Real(typeSystem.getUint(lhs)), + typeSystem.getDouble(rhs))))) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_DOUBLE_INT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + AxiomHelpers.mkFpLeReal( + ctx, + typeSystem.getDouble(lhs), + ctx.mkInt2Real(typeSystem.getInt(rhs)))))) + .addBinaryOverloadTranslator( + Comparison.LESS_EQUALS_DOUBLE_UINT64.celOverloadDecl(), + (ctx, typeSystem, constraintSink, lhs, rhs) -> + Optional.of( + typeSystem.wrapBool( + AxiomHelpers.mkFpLeReal( + ctx, + 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..49f2d7450 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/OptionalAxioms.java @@ -0,0 +1,204 @@ +// 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.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.common.CelOverloadDecl; +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 = + ImmutableList.of( + createAxiom( + OPTIONAL_NONE, + (ctx, ts, sink, args, argApproximations) -> + Optional.of(CelZ3OverloadResult.create(ts.mkOptionalNone(), ctx.mkFalse()))), + createUnaryAxiom( + 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( + OPTIONAL_OF_NON_ZERO_VALUE, + (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( + OPTIONAL_HAS_VALUE, + (ctx, ts, sink, val) -> + Optional.of(ts.wrapBool(ts.optHasValue(ts.getOptionalRef(val))))), + createUnaryAxiom( + 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( + 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( + 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) { + 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 CelZ3FunctionAxiom createAxiom( + 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( + 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( + 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/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..3bedacf8c --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/SubtractAxiom.java @@ -0,0 +1,94 @@ +// 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.BoolExpr; +import com.microsoft.z3.Expr; +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(), + 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) -> { + 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(), 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 new file mode 100644 index 000000000..61e941258 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/TypeAxiom.java @@ -0,0 +1,78 @@ +// 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.Optional; + +/** 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(), + (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 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 = typeSystem.isErrorOrUnknown(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.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())) + .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..2064047fd --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/axioms/TypeConversionAxioms.java @@ -0,0 +1,264 @@ +// 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.CelNumericBounds.MAX_INT64; + +import com.google.common.collect.ImmutableList; +import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.Expr; +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. */ +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)); + }) + .addOverloadTranslator( + Conversions.DOUBLE_TO_INT64.celOverloadDecl(), + createUninterpretedConversion(Conversions.DOUBLE_TO_INT64)) + .addOverloadTranslator( + Conversions.STRING_TO_INT64.celOverloadDecl(), + createUninterpretedConversion(Conversions.STRING_TO_INT64)) + .addUnaryOverloadTranslator( + Conversions.TIMESTAMP_TO_INT64.celOverloadDecl(), + (ctx, typeSystem, sink, arg) -> + Optional.of(typeSystem.wrapInt(typeSystem.getTimestamp(arg)))) + .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)); + }) + .addOverloadTranslator( + Conversions.DOUBLE_TO_UINT64.celOverloadDecl(), + createUninterpretedConversion(Conversions.DOUBLE_TO_UINT64)) + .addOverloadTranslator( + Conversions.STRING_TO_UINT64.celOverloadDecl(), + createUninterpretedConversion(Conversions.STRING_TO_UINT64)) + .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)) + .addOverloadTranslator( + Conversions.INT64_TO_DOUBLE.celOverloadDecl(), + createUninterpretedConversion(Conversions.INT64_TO_DOUBLE)) + .addOverloadTranslator( + Conversions.UINT64_TO_DOUBLE.celOverloadDecl(), + createUninterpretedConversion(Conversions.UINT64_TO_DOUBLE)) + .addOverloadTranslator( + Conversions.STRING_TO_DOUBLE.celOverloadDecl(), + createUninterpretedConversion(Conversions.STRING_TO_DOUBLE)) + .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)) + .addOverloadTranslator( + Conversions.INT64_TO_STRING.celOverloadDecl(), + createUninterpretedConversion(Conversions.INT64_TO_STRING)) + .addOverloadTranslator( + Conversions.UINT64_TO_STRING.celOverloadDecl(), + createUninterpretedConversion(Conversions.UINT64_TO_STRING)) + .addOverloadTranslator( + Conversions.DOUBLE_TO_STRING.celOverloadDecl(), + createUninterpretedConversion(Conversions.DOUBLE_TO_STRING)) + .addOverloadTranslator( + Conversions.BOOL_TO_STRING.celOverloadDecl(), + createUninterpretedConversion(Conversions.BOOL_TO_STRING)) + .addOverloadTranslator( + Conversions.BYTES_TO_STRING.celOverloadDecl(), + createUninterpretedConversion(Conversions.BYTES_TO_STRING)) + .addOverloadTranslator( + Conversions.TIMESTAMP_TO_STRING.celOverloadDecl(), + createUninterpretedConversion(Conversions.TIMESTAMP_TO_STRING)) + .addOverloadTranslator( + Conversions.DURATION_TO_STRING.celOverloadDecl(), + createUninterpretedConversion(Conversions.DURATION_TO_STRING)) + .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)) + .addOverloadTranslator( + Conversions.STRING_TO_BYTES.celOverloadDecl(), + createUninterpretedConversion(Conversions.STRING_TO_BYTES)) + .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)) + .addOverloadTranslator( + Conversions.STRING_TO_DURATION.celOverloadDecl(), + createUninterpretedConversion(Conversions.STRING_TO_DURATION)) + .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)) + .addOverloadTranslator( + Conversions.STRING_TO_TIMESTAMP.celOverloadDecl(), + createUninterpretedConversion(Conversions.STRING_TO_TIMESTAMP)) + .addUnaryOverloadTranslator( + Conversions.INT64_TO_TIMESTAMP.celOverloadDecl(), + (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 = + CelZ3FunctionAxiom.newBuilder(StandardFunction.BOOL.functionDecl()) + .addUnaryOverloadTranslator( + Conversions.BOOL_TO_BOOL.celOverloadDecl(), + (ctx, typeSystem, sink, arg) -> Optional.of(arg)) + .addOverloadTranslator( + Conversions.STRING_TO_BOOL.celOverloadDecl(), + createUninterpretedConversion(Conversions.STRING_TO_BOOL)) + .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 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(), + new Sort[] {typeSystem.celValueSort()}, + typeSystem.celValueSort()); + Expr res = ctx.mkApp(funcDecl, arg); + + switch (conversion.celOverloadDecl().resultType().kind()) { + case INT: + sink.accept(ctx.mkOr(typeSystem.isInt(res), typeSystem.isError(res))); + sink.accept( + ctx.mkImplies( + typeSystem.isInt(res), + ctx.mkNot(typeSystem.checkIntOverflow(typeSystem.getInt(res))))); + break; + case TIMESTAMP: + sink.accept(ctx.mkOr(typeSystem.isTimestamp(res), typeSystem.isError(res))); + sink.accept( + ctx.mkImplies( + typeSystem.isTimestamp(res), + ctx.mkNot(typeSystem.checkTimestampOverflow(typeSystem.getTimestamp(res))))); + break; + case DURATION: + sink.accept(ctx.mkOr(typeSystem.isDuration(res), typeSystem.isError(res))); + sink.accept( + ctx.mkImplies( + typeSystem.isDuration(res), + ctx.mkNot(typeSystem.checkDurationOverflow(typeSystem.getDuration(res))))); + break; + case UINT: + sink.accept(ctx.mkOr(typeSystem.isUint(res), typeSystem.isError(res))); + sink.accept( + ctx.mkImplies( + typeSystem.isUint(res), + ctx.mkNot(typeSystem.checkUintOverflow(typeSystem.getUint(res))))); + break; + case DOUBLE: + sink.accept(ctx.mkOr(typeSystem.isDouble(res), typeSystem.isError(res))); + sink.accept( + ctx.mkImplies( + typeSystem.isDouble(res), ctx.mkNot(ctx.mkFPIsNaN(typeSystem.getDouble(res))))); + break; + case STRING: + sink.accept(ctx.mkOr(typeSystem.isString(res), typeSystem.isError(res))); + break; + case BYTES: + sink.accept(ctx.mkOr(typeSystem.isBytes(res), typeSystem.isError(res))); + break; + case BOOL: + sink.accept(ctx.mkOr(typeSystem.isBool(res), typeSystem.isError(res))); + break; + default: + throw new IllegalArgumentException( + "Unsupported uninterpreted conversion result type: " + + conversion.celOverloadDecl().resultType()); + } + + 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)); + }; + } + + private TypeConversionAxioms() {} +} 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..1d2c7569e --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/tools/BUILD.bazel @@ -0,0 +1,81 @@ +load("@rules_java//java:defs.bzl", "java_binary", "java_library") +load("//publish:cel_version.bzl", "CEL_VERSION") + +package( + default_applicable_licenses = [ + "//:license", + ], + default_visibility = [ + "//publish:__pkg__", + "//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 = [ + "CelVerifierRepl.java", + "CelVerifierTool.java", + "CelVerifierToolCore.java", + "FormatUtils.java", + "VerificationOptions.java", + ":generate_version", + ], + tags = [ + "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", + "//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/CelVerifierRepl.java b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierRepl.java new file mode 100644 index 000000000..82a93c3d7 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierRepl.java @@ -0,0 +1,445 @@ +// 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) + .option(LineReader.Option.DISABLE_EVENT_EXPANSION, true) + .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, 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 "); + 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 new file mode 100644 index 000000000..a4210d246 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierTool.java @@ -0,0 +1,316 @@ +// 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 & REPL Tool", + subcommands = { + CelVerifierTool.CheckSatCommand.class, + CelVerifierTool.CheckValidCommand.class, + CelVerifierTool.VerifyEquivCommand.class, + CelVerifierTool.VerifyPolicyCommand.class, + CelVerifierTool.ReplCommand.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; + } + } + + @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) { + if (System.getProperty("z3.skipLibraryLoad") == null) { + System.setProperty("z3.skipLibraryLoad", "true"); + } + 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..51b7164e4 --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/tools/CelVerifierToolCore.java @@ -0,0 +1,165 @@ +// 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(variables, 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(variables, 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(variables, 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 Cel buildCel(Map variables) { + 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()); + } + 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(variables, 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..6e61a182d --- /dev/null +++ b/verifier/src/main/java/dev/cel/verifier/tools/VerificationOptions.java @@ -0,0 +1,248 @@ +// 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.OptionalType; +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(); + 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); + } + return ImmutableMap.copyOf(vars); + } + + 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(">")) { + // 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(">")) { + // 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( + "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); + } + + 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; + 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; + 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, timestamp," + + " duration, list, map, optional."); + } + } + + 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 new file mode 100644 index 000000000..55b9c24be --- /dev/null +++ b/verifier/src/test/java/dev/cel/verifier/BUILD.bazel @@ -0,0 +1,74 @@ +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 = [], + data = [ + "//testing:policy_test_resources", + ], + deps = [ + "//bundle:cel", + "//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", + "//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", + "//policy:validation_exception", + "@bazel_tools//tools/java/runfiles", + "@maven//:junit_junit", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "//:java_truth", + "@maven//:tools_aqua_z3_turnkey", + "//verifier", + "//verifier:canonicalization_optimizer", + "//verifier:numeric_bounds", + "//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 = 8, + sizes = [ + "small", + "medium", + ], + src_dir = "src/test/java", + deps = [":tests"], +) 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..d5f309c6d --- /dev/null +++ b/verifier/src/test/java/dev/cel/verifier/CanonicalizationOptimizerTest.java @@ -0,0 +1,665 @@ +// 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.CelExpr.ExprKind.Kind; +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_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( + "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_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( + "!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, 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\" && 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\""), + 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, 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, 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( + "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, x != true || int_var2 != 2)"), + + // 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"), + + // 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)"), + 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( + "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; + + 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)"); + } + + @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(); + } +} 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..5a9eaea02 --- /dev/null +++ b/verifier/src/test/java/dev/cel/verifier/CelPolicyVerifierImplTest.java @@ -0,0 +1,700 @@ +// 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.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; +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.policy.CelPolicyValidationException; +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.plannerCelBuilder() + .setOptions( + CelOptions.current() + .populateMacroCalls(true) + .enableHeterogeneousNumericComparisons(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 {2}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); + } + + @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( + "Invariant 'universal_no_unapproved_privileged_prod' 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 new file mode 100644 index 000000000..55bc5bccf --- /dev/null +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -0,0 +1,3193 @@ +// 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 java.util.stream.Collectors.joining; +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; +// 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.NullableType; +import dev.cel.common.types.OptionalType; +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.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("u1", SimpleType.UINT) + .addVar("u2", 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("string_var", 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)) + .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("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( + "string_int_list_map", + MapType.create(SimpleType.STRING, ListType.create(SimpleType.INT))) + .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 ProtoMessageTypeProvider TYPE_PROVIDER = + ProtoMessageTypeProvider.newBuilder() + .addDescriptors( + ImmutableList.of( + TestAllTypes.getDescriptor(), TestAllTypes.NestedMessage.getDescriptor())) + .build(); + + private static final CelVerifier VERIFIER = + CelVerifierFactory.newVerifier(CEL).setTypeProvider(TYPE_PROVIDER).build(); + + @Before + public void setUp() { + System.setProperty("z3.skipLibraryLoad", "true"); + } + + private enum IsSatisfiableTestCase { + 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", + "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 = \\{[^}]*,[^}]*\\}"), + 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; + final ImmutableList expectedFragments; + + IsSatisfiableTestCase(String expr, String... expectedFragments) { + this.expr = expr; + this.expectedFragments = ImmutableList.copyOf(expectedFragments); + } + } + + @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); + for (String fragment : testCase.expectedFragments) { + assertThat(result.message()).containsMatch(fragment); + } + } + + @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_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"); + } + + @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"), + 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; + + 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(); + + 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"), + 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"), + 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_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(CEL) + .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"; + CelAbstractSyntaxTree ast = CEL.compile(expr).getAst(); + + CelVerifier verifier = + CelVerifierFactory.newVerifier(CEL).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')"), + TYPE_TIMESTAMP_NOT_INT("type(timestamp('1970-01-01T00:00:00Z')) == int"), + 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"), + 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; + + 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"), + 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)"), + 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))"), + 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"), + 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 == " + 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]}"), + 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(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()"), + 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"), + 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)])"), + 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'})"), + 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"), + 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"), + 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"), + 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_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"), + 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( + "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_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"), + 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"), + 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"), + 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"), + 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_TIMESTAMP_FROM_INT("timestamp(1) == timestamp(1)"), + 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{}"), + 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('')"), + 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"), + 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"), + 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"), + 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; + + 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(CEL).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(CEL) + .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(CEL).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(CEL).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(celWithCustomFunc) + .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 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 = + 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); + } + + @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}"), + 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)"), + // 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; + + 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 = -?\\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+s'\\)"), + 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( + "(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 = \\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\\."), + CROSS_TYPE_SYMBOLIC_EQUALITY_NOT_ALWAYS_UNEQUAL_INT_UINT( + "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\\.", + "Counterexample input:", + "x = -?\\d+", + "u = \\d+u?"), + 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\\(Unknown\\)"), + OPTIONAL_ENTRY_DYN_VAR_TYPE_MISMATCH( + "[?dyn_var] == [?dyn_var] ? true : true", + "Condition is not always true\\.", + "Counterexample input:", + "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 = .*"), + 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 = .*"), + 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\\.", + "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 = (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\\.", + "Counterexample input:", + "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\\.", + "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 = (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\\)"), + 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 = \\d+u?"), + STRING_CONTAINS_IS_NOT_EQUALITY( + "role.contains('admin') ? role == 'admin' : true", + "Condition is not always true\\.", + "Counterexample input:", + "role = \".*admin.*\""), + STRING_OVERLAP_FALLACY( + "role.startsWith('A') && role.endsWith('B') ? role == 'AB' : true", + "Condition is not always true\\.", + "Counterexample input:", + "role = \"A.*B\""), + 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 = \"(admin.*|.*admin)\""), + STRING_CONTAINS_VS_STARTS_WITH( + "role.contains('admin') == role.startsWith('admin')", + "Condition is not always true\\.", + "Counterexample input:", + "role = \".*admin.*\""), + 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 = .*"), + DYNAMIC_NOT_TYPE_MISMATCH( + "!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\\.", + "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 = int\\{\\}"), + 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 = 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"), + 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:"), + UNINTERPRETED_CONVERSION_CAN_ERROR_BOOL_FROM_STRING( + "bool(string_var) == bool(string_var)", + "Condition is not always true\\.", + "Counterexample input:"), + ; + + 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()).containsMatch(fragment); + } + } + + 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') :" + + " 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')]"), + 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'))"), + 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"), + 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; + + 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); + } + + @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", + "int_list == [1, 2, 3, 4, 5, 6] ? (int_list.all(x, x > 0) || size(int_list) == 6) : true"), + 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']"), + 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; + + 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 { + 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"), + 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"), + 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"), + 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( + "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( + "[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"), + 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"), + 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"), + 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"), + 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"), + 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"), + MAP_COMPREHENSION( + "{'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_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( + "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)"), + 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()"), + 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"), + 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_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_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; + + 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"), + 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"), + 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()"), + 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"), + 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; + + 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(" {2}x = -?\\d+"); + assertThat(message).containsMatch(" {2}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" + + " {2}" + + 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(CEL); + 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 { + Cel customCel = + CelFactory.plannerCelBuilder() + .addVar("d1", SimpleType.DOUBLE) + .addVar("d2", SimpleType.DOUBLE) + .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 = + customCel + .compile( + "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 = + 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 < 20; i++) { + listLiteral.append("1"); + if (i < 19) { + listLiteral.append(", "); + } + } + listLiteral.append("]"); + + CelAbstractSyntaxTree ast = cel.compile("!(large_list == " + listLiteral + ")").getAst(); + CelVerifier verifier = + CelVerifierFactory.newVerifier(cel).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_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(cel).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 = + 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(cel).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(CEL).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(CEL).setComprehensionUnrollLimit(3).build(); + CelVerificationResult result = verifier.verifyEquivalence(astA, astB); + + 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(CEL).setComprehensionUnrollLimit(0).build(); + CelVerificationResult result = verifier.verifyEquivalence(astA, astB); + + 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 = + 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(customCel).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(customCel).setComprehensionUnrollLimit(0).build(); + CelVerificationResult result = verifier.verifyEquivalence(astA, astB); + + assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + } + + @Test + public void verifyEquivalence_timeoutReached_throwsCelVerificationException() throws Exception { + Cel customCel = + CelFactory.plannerCelBuilder() + .addVar("d1", SimpleType.DOUBLE) + .addVar("d2", SimpleType.DOUBLE) + .addVar("d3", SimpleType.DOUBLE) + .addVar("d4", SimpleType.DOUBLE) + .build(); + + CelVerifier timeoutVerifier = + CelVerifierFactory.newVerifier(customCel).setTimeout(Duration.ofMillis(1)).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"); + } + + @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(CEL).setComprehensionUnrollLimit(2).build(); + CelVerificationResult result = + ((CelVerifierZ3Impl) verifier) + .verifyImplication(assumeAst, assertAst, ImmutableMap.of(), "Implication"); + + assertThat(result.status()).isEqualTo(VerificationStatus.INCONCLUSIVE); + 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(CEL).build(); + CelVerificationResult result = + ((CelVerifierZ3Impl) verifier) + .verifyImplication(assumeAst, assertAst, ImmutableMap.of(), "Implication"); + assertThat(result.status()).isEqualTo(VerificationStatus.VERIFIED); + } +} + 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() {} +} 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/CelVerifierReplTest.java b/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierReplTest.java new file mode 100644 index 000000000..cbe5fffd5 --- /dev/null +++ b/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierReplTest.java @@ -0,0 +1,269 @@ +// 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 <=> "); + 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 + public void repl_varDeclarations() throws Exception { + String[] output = + runReplWithCommands( + ":var role string", + ":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("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]"); + } + + @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_equivDoubleNegation() throws Exception { + String[] output = runReplWithCommands(":var x int", "equiv !!(x == 10) <=> (x == 10)", ":quit"); + + 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_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(); + } + + @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/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..2d01e7a0f --- /dev/null +++ b/verifier/src/test/java/dev/cel/verifier/tools/CelVerifierToolTest.java @@ -0,0 +1,658 @@ +// 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.OptionalType; +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_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"); + } + + @Test + public void parseVariables_success() { + ImmutableMap vars = + VerificationOptions.parseVariables( + Arrays.asList( + "x:int", + "role:string", + "is_admin:bool", + "tags:list", + "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 + public void parseVariables_allTypesIncludingDyn() { + ImmutableMap vars = + VerificationOptions.parseVariables( + Arrays.asList( + "u:uint", + "d:double", + "fl:float", + "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)); + } + + @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, timestamp," + + " duration, list, map, optional."); + } + + @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 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 = + "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..e52868ea0 --- /dev/null +++ b/verifier/src/test/java/dev/cel/verifier/tools/VerificationOptionsTest.java @@ -0,0 +1,179 @@ +// 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.OptionalType; +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", + "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, + "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 + 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)); + } + + @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/BUILD.bazel b/verifier/tools/BUILD.bazel new file mode 100644 index 000000000..c3fe3fc32 --- /dev/null +++ b/verifier/tools/BUILD.bazel @@ -0,0 +1,81 @@ +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", +) + +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", +) + +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/README.md b/verifier/tools/README.md new file mode 100644 index 000000000..71570ad39 --- /dev/null +++ b/verifier/tools/README.md @@ -0,0 +1,218 @@ +# 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 + +### 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 (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 + +# 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 +# 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" \ + --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 +``` + +## 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` +* 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 "created_at:timestamp" --var "opt_flag:optional" +``` + +### 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). + +## 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. + 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